(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:
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,
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
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.