原文晾蜘。
https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours/Building_a_REPL
目前為止伐蒂,我們已經(jīng)能夠計算從命令行中讀取的單句表達式并將結(jié)果打印出來了。這對一個計算器來說已經(jīng)夠了秃臣,但離大部分心中所想的“編程”還有一些距離沸枯。我們希望能夠定義函數(shù)和變量并在之后進行使用肤视。但在這之前徙赢,我們需要構(gòu)建一個能夠執(zhí)行多個語句而不退出程序的系統(tǒng)字柠。
與之前一次性執(zhí)行整個程序不同,我們這次需要構(gòu)建一個REPL(讀取->求值->打印的循環(huán))狡赐。它會以交互的方式每次都會從控制臺讀入一個表達式并將執(zhí)行得到的結(jié)果打印在表達式之后窑业。后面的表達式能夠引用之前表達式中設(shè)置的變量(在下一章之后我們就能夠這么做了),讓你能夠這樣來構(gòu)建你自己的函數(shù)庫枕屉。
首先我們需要導(dǎo)入一些額外的IO函數(shù)数冬。在程序開頭添加以下內(nèi)容:
import System.IO
接下來,我們定義一些輔助函數(shù)來簡化我們的IO任務(wù)搀庶。我們需要一個打印出字符串然后立刻將輸出流清空的函數(shù);不然的話铜异,輸出的內(nèi)容依然會保存在緩沖區(qū)中這樣用戶就不會看到提示符或是結(jié)果:
flushStr :: String -> IO ()
flushStr str = putStr str >> hFlush stdout
接下來哥倔,我們創(chuàng)建一個打印出提示符然后讀入一行輸入的函數(shù):
readPrompt :: String -> IO String
readPrompt prompt = flushStr prompt >> getLine
現(xiàn)在我們讓代碼對字符串進行解析和求值,將主函數(shù)中的錯誤捕獲起來并在函數(shù)內(nèi)部進行處理:
evalString :: String -> IO String
evalString expr = return $ extractValue $ trapError (liftM show $ readExpr expr >>= eval)
然后寫一個對字符串進行計算然后打印出結(jié)果的函數(shù):
evalAndPrint :: String -> IO ()
evalAndPrint expr = evalString expr >>= putStrLn
現(xiàn)在是時候把它們都串聯(lián)起來了揍庄。我們想要在一個無限的循環(huán)中每次都讀取輸入咆蒿,調(diào)用函數(shù),然后將結(jié)果打印出來蚂子。內(nèi)置的interact函數(shù)基本上能做到除了循環(huán)以外的所有事情沃测。但如果使用一個sequence . repeat . interact
的組合,我們就會得到一個無限的循環(huán)食茎,但卻沒辦法從中退出去了蒂破。所以我們需要在其中啟動我們自己的循環(huán):
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里的一個典型的命名規(guī)范,表達不斷重復(fù)卻沒有返回值的Monad函數(shù)别渔。until_讀取一個用來表示何時停止的斷言附迷,一個在test前會執(zhí)行的操作,以及一個會返回對輸入進行處理的操作的函數(shù)哎媚。后兩個參數(shù)可以對任意的Monad適用喇伯,而不僅僅限制于IO Monad,這就是我們使用一個帶有類型限制Monad m =>
的變量m來表示它的類型的原因拨与。
注意稻据,和遞歸函數(shù)一樣,我們也能夠定義遞歸的操作买喧。
現(xiàn)在所有的部件都已經(jīng)準(zhǔn)備完畢了捻悯,我們可以輕松寫出我們的REPL:
runRepl :: IO ()
runRepl = until_ (== "quit") (readPrompt "Lisp>>> ") evalAndPrint
修改我們的主函數(shù)匆赃,讓它既能夠執(zhí)行單個的表達式,也能夠進入一個會連續(xù)對輸入的表達式進行計算直達我們輸入quit才退出的REPL:
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
$