【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

F# Interactiveの改造

F# Interactiveはとても便利なのですが、セミコロンを2つ入力するのが面倒です。そこでセミコロンを省略して、インデントも自動化してみました。使用例は以下の通りです。空行でインデントから抜けています。

> let f x=
    - if x%2=0 then
        - "偶数"
        -
    - else
        - "奇数"
        -
    -

val f : int -> string

> f 23
val it : string = "奇数"
> f 44
val it : string = "偶数"

console.fsを作り直しましたので、丸ごと差し替えます。

#light 

namespace Microsoft.FSharp.Compiler.Interactive

open System
open System.IO
open System.Text
open System.Collections.Generic

type ReadLineConsole(complete: (string option * string -> seq<string>)) as this =
    let history = new List<string>()
    let log = Path.Combine(Directory.GetCurrentDirectory(), "log.fsx")
    let mutable complete = complete
    
    do
        if File.Exists(log) then this.AppendLog("")
        this.AppendLog("// " + DateTime.Now.ToString())
    
    member x.Complete with set(v) = (complete <- v)
    member x.Prompt = "> "
    
    member x.ReadLine() =
        let count(s : string, ch : char) =
            let mutable ret = 0
            for c in s do if c = ch then ret <- ret + 1
            ret
            
        let getWords(s : string) =
            let ret = new List<string>()
            let rec getWords(p1, p2) =
                if p2 >= s.Length || not (Char.IsLetterOrDigit(s.[p2])) then
                    if p1 < p2 then ret.Add(s.Substring(p1, p2 - p1))
                    if p2 < s.Length then getWords(p2 + 1, p2 + 1)
                else
                    getWords(p1, p2 + 1)
            getWords(0, 0)
            ret

        let lines = ref 0
        let p1 = ref 0
        let p2 = ref 0
        let sb = new StringBuilder()

        let rec readLine(indent : string) =
            if indent <> "" || !lines > 0 then
                Console.Write(indent + "- ")
            let line = Console.ReadLine().Trim()
            if indent = "" && line = "#h" then
                for cmd in history do Console.WriteLine(cmd)
                Console.Write(x.Prompt)
                readLine(indent)
            else if String.IsNullOrEmpty(line) then
                ()
            else
                if sb.Length > 0 then sb.AppendLine() |> ignore
                sb.Append(indent + line) |> ignore
                lines := !lines + 1
                if line.StartsWith("//") then
                    readLine(indent)
                else
                    let d1 = count(line, '(') - count(line, ')')
                    let d2 = count(line, '[') - count(line, ']')
                    p1 := !p1 + d1
                    p2 := !p2 + d2
                    let words = getWords(line)
                    let w1 = if words.Count > 0 then words.[words.Count - 1] else ""
                    if (!p1 <> 0 && d1 <> 0) || (!p2 <> 0 && d2 <> 0)
                        || line.EndsWith("=") || line.EndsWith("->")
                        || w1 = "in" || w1 = "do" || w1 = "then" || w1 = "else"
                    then
                        readLine(indent + "    ")
                    let w2 = if words.Count > 0 then words.[0] else ""
                    if !p1 <> 0 || !p2 <> 0 || indent <> ""
                        || (line.EndsWith(";") && not(line.EndsWith(";;")))
                        || (w2 = "if" && not(words.Contains("then") && words.Contains("else")))
                        || w2 = "match"
                    then
                        readLine(indent)
        
        readLine("")
        let ret = sb.ToString()
        history.Add(ret)
        x.AppendLog(ret)
        if ret.EndsWith(";;") then
            ret
        else
            if !lines = 1 then
                ret + ";;"
            else
                ret + Environment.NewLine + ";;"
    
    member x.AppendLog(cmd : string) =
        use fs = new FileStream(log, FileMode.Append)
        use sw = new StreamWriter(fs)
        sw.WriteLine(cmd)

履歴管理はコマンドプロンプトの機能に任せているため、自動的に日本語にも対応しています。ASTを解析しているわけではないため、インデントの判断はかなりやっつけです。必要な部分でインデントされなかったり勝手に抜けたりすると思われるので、その都度修正しながら使うことになるでしょう。

自動的にlog.fsxというファイルにログを取ります。対話的に試行錯誤してから、最終的にソースとしてまとめるような使い方を想定しています。