複数行REPL

d:id:cadr:20080229で実装したREPLは式を1行で書かないときちんと評価してくれなかった。つまり,

(let ((x 1)
      (y 2))
   (+ x y))

は,次のように書く必要があった。

(let ((x 1) (y 2)) (+ x y))

まあ,この程度なら問題ないが,もっと複雑な式を書く場合に複数行に分けて書けると便利であるのは明らかだと思う。というわけで実装してみた。
が,良い方法が思いつかない。1行なら改行のところが式の終わりだとわかるが,複数行に式がまたがる場合はどこが式の終わりになるのか?
単純な方法としては,式の終わりの印として特別な文字列を入力するようにする。そうすれば式の終わりが簡単にわかる。現在,評価の部分は,字句解析,構文解析,式評価の3つのパスからできているのだが,この方法なら字句解析の段階で式の終わりを認識できる。読み込んだ1行の中に式の終わりを表す文字列がなければ,あらたなプロンプトを表示し,もう1行読み込む。式の終わりを表す文字列があったら,その部分は取り除いて構文解析に渡す(字句解析した結果を)。ただこの方法はあまりスマートでない。
もう一つの方法として,構文解析で式の終わりかどうかを判定するというのが考えられる。入力されたプログラムの括弧の対応がきちんととれていれば,正しい式でありここが式の終わりだと認識できる。右括弧“)”が少なければまだ式は終わりではないと判断し,新たなプロンプトを表示する。今回はこちらを採用した。
プログラムはこんな感じ。基本はhttp://halogen.note.amherst.edu/~jdtang/scheme_in_48/tutorial/repl.html

flushStr :: String -> IO ()
flushStr str = putStr str >> hFlush stdout

readPrompt :: String -> IO String
readPrompt prompt = flushStr prompt >> getLine

readPromptCont :: String -> String -> IO String
readPromptCont prompt str = flushStr prompt >> liftM (str ++) getLine

evalString :: String -> [Val]
evalString = eval . parse . lex
printVal :: [Val] -> IO ()
printVal val = mapM_ (putStrLn . show) val

until_ :: (String -> Bool) -> IO String -> (String -> b) -> (b -> IO ()) -> IO ()
until_ pred r e p = do
    result <- r
    if pred result
       then return ()
       else catch ((p . e) result >> until_ pred (readPrompt ">> ") e p)
              (\ex -> case ex of
                       (ErrorCall "Parsing:InParsing") -> until_ pred (readPromptCont ".. " (result ++ " ")) e p
                       _ -> putStrLn "Error: unrecognized" >> until_ pred (readPrompt ">> ") e p
              )

runRepl :: IO ()
runRepl = until_ (== "quit") (readPrompt ">> ") evalString printVal

まず,構文解析器に手を加えて,構文解析した結果,右括弧が少なかったら“error "Parsing:inParsing"”を飛ばすようにした。そしてREPLの本体であるunitl_(もはや汎用性は失われているのでこの名前もどうかと思うが)でその例外をキャッチするようにし,キャッチした場合はあらたな1行を読み込む。このとき,readPromptではなく,新たに定義したreadPromptContを用いる。readPromptContは読み込んだ1行と引数にあたえた文字列を連結した結果を返す。つまり,引数としてその前に読んだ行を渡せば,括弧の対応がとれた正しい式が記述された文字列を得ることができる。
実行すると,

>> (let ((x 1)
..       (y 2))
..    (+ x y))
3
>> 

こんな感じ。ちゃんと動いた。
ただ,この方法だと2行目以降を入力したときも1行目から字句解析と構文解析を行う必要があり,かなり無駄。まあ,動くからとりあえず良しとする。

モナドと例外をもう少し勉強する必要があることを実感。