Write Yourself a Scheme in 48 Hours

A Haskell Tutorial

By Jonathan Tang

Download in .tar.gz format
Download in .zip format

Contents

  1. Overview
  2. First Steps: Compiling and running
  3. Parsing
    1. A Simple Parser
    2. Whitespace
    3. Literal Numbers and Strings: Return Values
    4. Lists, Dotted Lists, and Quoted Data: Recursive Parsers
  4. Evaluation, Part 1
    1. Displaying Values: Show and Typeclasses
    2. Evaluating Primitive Values: Pattern Matching
    3. Evaluating Primitive Functions: First-class Functions
  5. Intermezzo: Error Checking & Exceptions
  6. Evaluation, Part 2
    1. Additional Primitives: Partial Application
    2. Conditionals: Pattern Matching 2
    3. List Primitives: car, cdr, and cons
    4. Equal? and Weak Typing: Heterogenous Lists
  7. Building a REPL: Basic I/O
  8. Adding Variables and Assignment: Mutable State in Haskell
  9. Defining Scheme Functions: Closures and Environments
  10. Creating IO Primitives: File I/O
  11. Towards a Standard Library: Fold and Unfold
  12. Conclusion & Further Resources

7. Building a REPL

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 IO hiding (try)
We have to hide the try function (used in the IO module for exception handling) because we use Parsec's try function.

Next, we define a couple 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
              otherwise -> putStrLn "Program takes only 0 or 1 argument"
Compile and run the program, and try it out:
debian:/home/jdtang/haskell_tutorial/code# ghc -package parsec -fglasgow-exts -o lisp listing7.hs
debian:/home/jdtang/haskell_tutorial/code# ./lisp
Lisp>>> (+ 2 3)
5
Lisp>>> (cons this '())
Unrecognized special form: this
Lisp>>> (cons 2 3)
(2 . 3)
Lisp>>> (cons 'this '())
(this)
Lisp>>> quit
debian:/home/jdtang/haskell_tutorial/code#
Next: Adding Variables and Assignment