If you already know basic Scheme programming, expand your skills by learning how to use macros. We won't cover macros in class, but here are some examples and sources to look at to get started:
Paul Graham's On Lisp provides an excellent and readable introduction to macros, what they can and should do, and how to think about them. His book uses different macro syntax than what I explain below, but gives a wonderful intro nonetheless. The macro material starts in chapter 7.
Search for "macros" in the DrScheme HelpDesk. This brings up far more information than you need (and likely more than you can process if you are new to macros), but it is there for reference.
If you have a macro question while working on this assignment, don't hesitate to ask. I'm not an expert on Scheme macros, but I should be able to help with whatever you'll encounter on this assignment.
Macros differ from functions in that evaluating a function yields a value, wihle evaluating a macro yields an expression. Macros are useful for many reasons. Among them, they let you introduce new syntax into your programs, and they you alter how Scheme would otherwise evaluate an expression if it were a function call instead of a macro.
or operator provides a classic example of the
latter. Consider the expression
(or true (+ 4 'a)). As
or short-circuits in Scheme, this program should return
true. Assume we wrote
or as a function:
(define (or e1 e2) (if e1 true e2))
(or true (+ 4 'a)) would yield an
error because Scheme evaluates its arguments before evaluating the
body of the function. Thus, we cannot implement
a function. We could, however, implement it as a macro using the
(define-syntax my-or (syntax-rules () [(my-or e1 e2) (if e1 true e2)]))
(my-or true (+ 4 'a)) returns true,
rather than an error, as it should.
The general form of a Scheme macro is:
(define-syntax macro-name (syntax-rules ([concrete-literals]) [input-pattern output-pattern] ...))
The concrete-literals indicate parts of the syntax that should be
matched exactly, rather than binding to some other expression. For
example, if I wanted to be able to write more verbose if-statements
(my-if (= 5 6) then 2 else 3) (where then and
else are keywords), I could define the following macro:
(define-syntax my-if (syntax-rules (then else) [[my-if test then e1 else e2] (if test e1 e2)]))
The above example illustrates how we can add new syntax to Scheme using macros.
The two examples I've shown you so far illustrate forms where the
number of items in the macro expression is fixed. In practice, we
often wish to write macros where the syntax follows a pattern, but the
number of instances of that pattern is not fixed. A good example of
this is Scheme's
(let ([x 4] [y 9]) (+ x y))
let introduces local variables. You can specify as
many let variables as you want, then use them in the single expression
in the body of the
Let is really
equivalent to a lambda expression. For example, the following
expression implements the above
((lambda (x y) (+ x y)) 4 9)
Here is a macro for let. To handle the arbitrary
number of variables, we use ellipses after the first use of the
[var val]. In the output pattern, we use the
ellipses again to say that all items in var position should become
parameters to the lambda, and all items in val position should become
arguments in the function call.
(define-syntax my-let (syntax-rules () [(let ([var val] ...) body) ((lambda (var ...) body) val ...)]))
Consider an operator
time that takes a single
expression as an argument and returns the time (in seconds) that the
expression took to run (assume you have an operator (current-seconds)
to give you the current time). Should you implement
as a function or a macro? Justify your answer (in a sentence or two)
and provide code for an implementation.
Write a macro for for-loops. Your macro should support the following expression format:
(for i 1 5 do (printf "~a~n" i))
When run, this example should print the numbers 1 through 5, each number on its own line.
Hint: look up the Scheme form
letrec in HelpDesk.
Above, I gave you a macro for binary
expressions. Write a macro for
or expressions that take
an arbitrary number of arguments (including 0). The arguments should
be evaluated left to right, and the expression should return true as
soon as it encounters an expression that evaluates to true.
(multi-or) = false
(multi-or (= 3 5) (> 3 1) (+ 'a 4)) = true
(multi-or (= 3 5) (< 3 1) (+ 'a 4)) = an error
let form I showed above does not allow one
variable in a single let clause to refer to another. For example, the
(let ([x 5] [y (+ x 1)]) (+ x y))
Would yield an error that
x is an unbound identifier
(from the lambda definition, it should be clear why this happens).
There are times, however, when we would like to write let-style
expressions that allow each local var to refer to ones defined before
it. This form is called
let* in Scheme:
(let* ([x 5] [y (+ x 1)]) (+ x y)) yields 11
Develop a macro for
my-let* (same as
let*, but that
name is already in use). Don't use letrec, as that is more powerful
Suppose you work for a hardware design firm and have been asked to develop a simulator for state machines. You need to provide a way to specify state machines and to run them on a list of input characters. A run should return a boolean indicating whether the sequence of input characters corresponds to a valid run through the machine. (An invalid run would be on in which there was no transition specified for some input symbol).
Implement the state machine simulator without using macros. You'll need to define a representation for state machines and a way to run them on a sequence of input symbols. The result of your run should be a boolean as described above.
Implement the state machine simulator using macros. As an (obligatory traffic light) example, here's a proposed syntax for state machines and how you might run it (the machine expects that input symbols occur in the pattern (red* green* yellow*)*):
(define traffic-check (automaton see-red (see-red : (green -> see-green) (red -> see-red)) (see-green : (yellow -> see-yellow) (green -> see-green)) (see-yellow : (red -> see-red)))) > (traffic-check (list 'red 'red 'red 'green)) #t > (traffic-check (list 'red 'green 'red 'green)) #f
In this example,
see-red is the initial state (follows
"automaton"), and the rest of the code specifies states and their
outgoing transitions. For example, in state
the current input symbol is
yellow, then the next state
In a few sentences, contrast your two solutions. What are the advantages and disadvantages of each? Can you characterize the difference between the two implementations in technical languages terms?