Write Yourself a Scheme in 48 Hours/Building a REPL
|←Evaluation, Part 2||Write Yourself a Scheme in 48 Hours
Building a REPL
|Adding Variables and Assignment→|
So far, we've been content to evaluate single expressions from the command line, printing the result and exiting afterwards. This is fine for a calculator, but isn't what most people think of as "programming". We'd like to be able to define new functions and variables, and refer to them later. But before we can do this, we need to build a system that can execute multiple statements without exiting the program.
Instead of executing a whole program at once, we're going to build a read-eval-print loop. This reads in expressions from the console one at a time and executes them interactively, printing the result after each expression. Later expressions can reference variables set by earlier ones (or will be able to, after the next section), letting you build up libraries of functions.
First, we need to import some additional IO functions. Add the following to the top of the program:
Next, we define a couple of helper functions to simplify some of our IO tasks. We'll want a function that prints out a string and immediately flushes the stream; otherwise, output might sit in output buffers and the user will never see prompts or results.
flushStr :: String -> IO () flushStr str = putStr str >> hFlush stdout
Then, we create a function that prints out a prompt and reads in a line of input:
readPrompt :: String -> IO String readPrompt prompt = flushStr prompt >> getLine
Pull the code to parse and evaluate a string and trap the errors out of main into its own function:
evalString :: String -> IO String evalString expr = return $ extractValue $ trapError (liftM show $ readExpr expr >>= eval)
And write a function that evaluates a string and prints the result:
evalAndPrint :: String -> IO () evalAndPrint expr = evalString expr >>= putStrLn
Now it's time to tie it all together. We want to read input, perform a function, and print the output, all in an infinite loop. The built-in function
interact almost does what we want, but doesn't loop. If we used the combination
sequence . repeat . interact, we'd get an infinite loop, but we wouldn't be able to break out of it. So we need to roll our own loop:
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
The underscore after the name is a typical naming convention in Haskell for monadic functions that repeat but do not return a value.
until_ takes a predicate that signals when to stop, an action to perform before the test, and a function-returning-an-action to do to the input. Each of the latter two is generalized over any monad, not just
IO. That's why we write their types using the type variable
m, and include the type constraint
Monad m =>.
Note also that we can write recursive actions just as we write recursive functions.
Now that we have all the machinery in place, we can write our REPL easily:
runRepl :: IO () runRepl = until_ (== "quit") (readPrompt "Lisp>>> ") evalAndPrint
And change our main function so it either executes a single expression, or enters the REPL and continues evaluating expressions until we type
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"
Compile and run the program, and try it out:
$ 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 $