48時間でSchemeを書こう/REPLの作成

出典: フリー教科書『ウィキブックス(Wikibooks)』

今まで、私たちはコマンドラインから一つだけ式を取って評価し、結果を表示して終了することで満足してきました。これは計算機にしては上出来ですが、人々の思うところの「プログラミング」っぽくはないですね。新しい関数や変数を定義し、後でそれを参照したりしたいものです。でもその前に、終了せずに複数の式を実行できるシステムを作らなければなりません。

プログラム全体を一度に実行してしまうのではなく、私たちはread-eval-print loopを作ることにします。これは対話的にコンソールからの式をひとつづつ読み込んで実行し、式それぞれの後に結果を表示します。後で入力された式はそれより前で定義された変数を参照でき(次の章の後には)、それによって関数のライブラリを作りあげることができるようになります。

始めに、いくらか追加のIO functionsをインポートする必要があります。次の行をプログラムの冒頭に加えてください。

import System.IO

次に、私たちの行うIOタスクを簡単にするためいくつかの補助関数を用意します。文字列を表示してストリームをすぐにフラッシュする関数が要ります。出力がバッファに溜め込まれたままになって、ユーザはついにプロンプトや結果を見ることがないようなことがあると困りますから。

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

そして、プロンプトを表示して入力から一行読み込む関数を作ります。

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

mainから文字列をパースして評価しエラーを補足する部分を抜き出して一つの関数にします。

evalString :: String -> IO String
evalString expr = return $ extractValue $ trapError (liftM show $ readExpr expr >>= eval)

そして文字列を評価して結果を表示する関数を書きます。

evalAndPrint :: String -> IO ()
evalAndPrint expr = evalString expr >>= putStrLn

それではこれら全てを一緒にしましょう。入力を読み、関数を実行し、結果を表示するという過程を無限ループするのが目標です。組み込み関数interactほぼ私たちの目的通りのことをしますが、ループしてくれません。sequence . repeat . interactという組み合わせを使えば無限ループはできますが、今度はそれから抜け出すことができなくなります。そこで自前でループを定義しましょう。

until_ :: Monad m => (a -> Bool) -> m a -> (a -> m ()) -> m ()
until_ pred prompt action = do 
    result <- prompt
    if pred result 
       then return ()
       else action result >> until_ pred prompt action

最後にアンダースコアの付いた名前は、Haskellにおける一般的な命名規約に沿ったもので、繰り返すが値を返さないモナド関数を表します。until_は終了を判断する述語、その前に実行するアクション、そして入力に対して行うアクションを返す関数を引数に取ります。後者2つはIOのみならずどんなモナドに対しても働くように一般化されています。そのため、それらの型は型変数mを使って表され、Monad m =>という型制約を付けてあります。

再帰的な関数のみならず、再帰的なアクションも書くことができるのに注目してください。

今や必要なものが全て調ったので、REPLを簡単に書くことができます。

runRepl :: IO ()
runRepl = until_ (== "quit") (readPrompt "Lisp>>> ") evalAndPrint

そしてmain関数を編集して、一つだけ式を実行するか、REPLに入ってquitと打つまで式の評価を続けるようにします。

main :: IO ()
main = do args <- getArgs
          case length args of
              0 -> runRepl
              1 -> evalAndPrint $ args !! 0
              otherwise -> putStrLn "Program takes only 0 or 1 argument"

コンパイル・実行して試してみましょう。

% ghc -package parsec -fglasgow-exts -o lisp [../code/listing7.hs listing7.hs]
% ./lisp
Lisp>>> (+ 2 3)
5
Lisp>>> (cons this '())
Unrecognized special form: this
Lisp>>> (cons 2 3)
(2 . 3)
Lisp>>> (cons 'this '())
(this)
Lisp>>> quit
%