CS 536 Homework 1: Macros

Due: September 15 in class (hardcopy)


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:

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.

Basics of Scheme Macros

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.

The 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))

Then evaluating (or true (+ 4 'a)) would yield an error because Scheme evaluates its arguments before evaluating the body of the function. Thus, we cannot implement or with a function. We could, however, implement it as a macro using the following code:

(define-syntax my-or
  (syntax-rules ()
    [(my-or e1 e2) 
     (if e1 true e2)]))

Now, evaluating (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 such as (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 construct:

(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. Let is really equivalent to a lambda expression. For example, the following expression implements the above let expression:

((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 pattern [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 ...)]))

Exercises

  1. 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 time as a function or a macro? Justify your answer (in a sentence or two) and provide code for an implementation.

  2. 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.

  3. Above, I gave you a macro for binary or 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.

    Examples:
    (multi-or) = false
    (multi-or (= 3 5) (> 3 1) (+ 'a 4)) = true
    (multi-or (= 3 5) (< 3 1) (+ 'a 4)) = an error

  4. The let form I showed above does not allow one variable in a single let clause to refer to another. For example, the code

    (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 than let*.

  5. 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).

    1. 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.

    2. 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 see-green if the current input symbol is yellow, then the next state is see-yellow,

    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?


Back to the Assignments page