参考URL
#ソース
https://github.com/0zawa/scheme_in_48_hours/blob/master/building_repl.hs
#REPL
REPLとはRead-Eval-Print-Loopの略です。
その名の通り、読んで、評価して、表示、を繰り返すプログラムです。
まず、「読む」部分を作っていきます。
#Read
flushStr :: String -> IO ()
flushStr str = putStr str >> hFlush stdout
readPrompt :: String -> IO String
readPrompt prompt = flushStr prompt >> getLine
flushStrは文字列をputStrで表示した後、
バッファに残っている文字列を標準出力に書き出します。
readPromptはpromptをflushStrで表示させた後、
getLineで標準入力から一行読み込みます。
promptには"Lisp>>> "のような入力を促すための
文字列が渡されます。
#Eval
次に評価部分を作ります。
evalString :: String -> IO String
evalString expr = return $ extractValue $ trapError (liftM show $ readExpr expr >>= eval)
evalString関数は複雑なので少しずつみていきます。
readExpr expr >>= eval
まず引数で渡された文字列をreadExprで
ThrowsError LispValに変換します。
続いてevalで評価して、結果のThrowsError LispValを得ます。
liftM show $ readExpr expr >>= eval
evalの結果に対しliftM showを適用し、
ThrowsError Stringを得ます。
liftMはThrowsErrorの中身に対して、与えられた
関数を適用します。
trapError (liftM show $ readExpr expr >>= eval)
trapErrorはエラーを文字列に変換するために適用します。
この結果必ずRight値を持ったThrowsErrror Stringになります。
extractValue $ trapError (liftM show $ readExpr expr >>= eval)
extractValueはThrowsErrorのRight値を取り出す関数です。
trapErrorの結果のThrowsError Stringから
Stringを取り出します。
evalString :: String -> IO String
return $ extractValue $ trapError (liftM show $ readExpr expr >>= eval)
最後に結果の文字列をreturnでIOに包んで返却します。
次にこの結果を表示する関数を定義します。
evalAndPrint :: String -> IO ()
evalAndPrint expr = evalString expr >>= putStrLn
#Loop
最後に、これまで作った処理をループさせてREPLを完成させます。
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
until_関数はまず、promptで渡されたアクションを実行して、
結果をactionに格納します。
promptには先ほど定義したreadPrompt関数が渡されます。
readPrompt "Lisp>>> "
actionにはreadPromptの実行結果が格納されるので、
標準入力から一行読み込んだ文字列が格納されることになります。
続いて、結果をpredに渡して真偽値を判定します。
predには
== "quit"
が渡されます。
読み込んだ文字列が"quit"だった際はreturn ()で
REPLを抜けます。
それ以外の場合は結果をactionに渡して、その後
until_を再帰的に呼び出します。
actionには
evalAndPrint
が渡されます。
evalAndPrintに標準入力から取得した文字列が渡され、
その文字列が評価されて、最後に結果が表示されます。
until_を呼び出す関数も定義します。
runRepl :: IO ()
runRepl = until_ (== "quit") (readPrompt "Lisp>>> ") evalAndPrint
最後にrunRepl関数をmainから呼び出すようにして完成です。
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"
プログラムに引数が渡された場合は、その文字列を評価、表示し、
渡されなかった場合は、REPLを実行します。
引数が2つ以上渡されていた場合はエラーにして終了させます。