在 48 小时内编写您自己的 Scheme/构建 REPL
到目前为止,我们已经满足于从命令行评估单个表达式,打印结果并在之后退出。对于计算器来说,这很好,但这不是大多数人认为的“编程”。我们希望能够定义新的函数和变量,并在以后引用它们。但在此之前,我们需要构建一个可以执行多个语句而不会退出程序的系统。
我们不会一次执行整个程序,而是要构建一个读-评估-打印循环。这将从控制台一次读取一个表达式并交互地执行它们,在每个表达式之后打印结果。后面的表达式可以引用前面设置的变量(或者在下一节之后可以这样做),让您构建函数库。
首先,我们需要导入一些额外的 IO 函数。将以下内容添加到程序顶部
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_
接受一个表示何时停止的谓词,一个在测试之前要执行的操作,以及一个返回操作的函数。后面两个都针对任何单子进行了泛化,而不仅仅是 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
_ -> 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 $