Write Yourself a Scheme in 48 Hours/Building a REPL

Write Yourself a Scheme in 48 Hours
 ← Evaluation, Part 2 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:

import System.IO

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 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"

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)
Lisp>>> (cons this '())
Unrecognized special form: this
Lisp>>> (cons 2 3)
(2 . 3)
Lisp>>> (cons 'this '())
Lisp>>> quit

Write Yourself a Scheme in 48 Hours
 ← Evaluation, Part 2 Building a REPL Adding Variables and Assignment →