Go to the first, previous, next, last section, table of contents.

The Read-Eval-Print Loop

(This section could be skimmed if you're not interested in the read-eval-print-loop, which is just a simple command interpreter that acts as a "front end" to the evaluator.)

When you're interacting with Scheme by typing text, you're interacting with a Scheme procedure called the read-eval-print loop. This procedure just loops, accepting one command at a time, executing it, and printing the result.

The three steps at each iteration of the loop are:

  1. calling read to read the characters that make up a textual expression expression from the keyboard input buffer, and construct a data structure to represent it,
  2. calling eval to evaluate the expression--intuitively, eval "figures out what the expression means," and "does what it says to do," returning the value of the expression--and
  3. calling write to print a textual representation of the resulting from eval, so that the user can see it.

(More generally, we might read expressions from a file rather than the keyboard buffer. We'll ignore that for now.)

You can write your own read-eval-print loop for your own programs, so that users can type in expressions, and you can interpret them any way you want. Later, I'll show how to write an evaluator, and this will come in handy. You can start up your read-eval-print loop (by typing in (rep-loop)), and it will take over from the normal Scheme read-eval-print loop, interpreting expressions your way.

Here's a very simple read-eval-print loop:

(define (rep-loop)
   (display "repl>")      ; print a prompt
   (write (eval (read)))  ; read expr., pass to eval, write result
   (rep-loop))            ; loop (tail-recursive call) to do it again

(Notice that the expression (write (eval (read))) does things in the proper read-eval-print order, because the argument to each procedure call is computed before the actual call. In Scheme, as in most languages, nested procedure calls expressions are done "from the inside out.")

I've coded the iteration recursively, rather than using a looping construct. The procedure is tail-recursive, since all it does at the end is call itself. Remember that Scheme is smart about this kind of recursion, and won't build up procedure activation information on the stack and cause a stack overflow. You can do tail recursion all day. Since nothing happens in a given call to the procedure after the tail-call, Scheme can avoid returning to it at all, and avoid saving any state to return to.

The above read-eval-print loop isn't very friendly, because it loops infinitely without giving you any chance to break out of it. Let's modify it to allow you to stop the tail recursion by typing in the symbol halt.

(define (rep-loop)
   (display "repl>")              ; print a prompt
   (let ((expr (read)))           ; read an expression, save it in expr
      (cond ((eq? expr 'halt)     ; user asked to stop?
             (display "exiting read-eval-print loop")
             (newline))
            (#t                   ; otherwise,
             (write (eval expr))  ;  evaluate and print
             (newline)
             (rep-loop)))))       ;  and loop to do it again

Notice that this is still tail recursive, because the branch that does the recursive call doesn't do anything else after that.

This read-eval-print loop could be improved a little. By using the symbol halt as the command to tell the loop to stop, we prevent people from being able to evaluate halt as an expression. We could get around this by ensuring that the halt command doesn't have the syntax of any expression in the language, but we won't bother right now.

Another improvement would be to make it possible to use different interpreters with the same read-eval-print loop. The rep-loop procedure above assumes that it should call a procedure named eval to evaluate an expression. We'd like to write a rep-loop that works with different evaluators, so instead of having it call eval by name, we'll hand it an argument saying which evaluator to use. Since procedures are first class, we can just hand it a pointer to the evaluation procedure.

(define (rep-loop evaluator)
   (display "repl>")                 ; print a prompt
   (let ((expr (read)))              ; read an expression, save it in expr
      (cond ((eq? expr 'exit)        ; user asked to stop?
             (display "exiting read-eval-print loop")
             (newline))
            (#t                      ; otherwise,
             (write (evaluator expr)) ;  evaluate and print
             (rep-loop evaluator))))) ;  and loop to do it again

Here I just made three changes. I added an argument our-eval, which is expected to be a procedure. Then we changed the call to eval to call our-eval, i.e., whatever evaluator was given. Then we changed the recursive call to rep-loop to pass that argument on to the next recursive call.


Go to the first, previous, next, last section, table of contents.