CSCI 360 - Survey of Programming Languages

Spring 2011

Programming Assignment #3: Macros

Eliminating Code Duplication

Recall that all Racket programs are just lists. By allowing Racket programs to be transformed at compile time, we can reduce code duplication. Racket programs that transform other Racket programs at compile time are called macros.

Consider the following expressions with higher-order functions:

> (map (lambda (x) (+ x 1)) '(1 2 3 4 5))
'(2 3 4 5 6)
> (map (lambda (x) (* x 2)) '(1 2 3 4 5))
'(2 4 6 8 10)
These expressions each apply a function to every element in a list, generating a new list. But one could argue that the syntax could be cleaned up considerably. Consider the following alternative:
> ($ (x) '(1 2 3 4 5) (+ x 1))
'(2 3 4 5 6)
> ($ (x) '(1 2 3 4 5) (* x 2))
'(2 4 6 8 10)
The syntax here says, "For each x drawn from the list, perform the specified computation." This is an example of a list comprehension. In Racket, we can write a macro to implement this syntax as follows:
(define-syntax-rule ($ (x) lst expr) (map (lambda (x) expr) lst))
We can enhance the macro as follows. Let's say we only want to include mappings of values that meet a certain criterion. In this example, we want to multiply positive numbers by 2, but ignore the negative numbers:
> ($ (x) '(1 -2 -3 4 -5) (* x 2) (> x 0))
'(2 8)
We could write a macro for this variation as follows:
(define-syntax-rule ($ (x) lst expr p) (map (lambda (x) expr) (filter (lambda (x) p) lst)))
Racket allows us to combine multiple macros bound to the same keyword (with differing numbers of parameters) as follows:
(define-syntax $
  (syntax-rules ()
    [($ (x) lst expr) (map (lambda (x) expr) lst)]
    [($ (x) lst expr p) (map (lambda (x) expr) (filter (lambda (x) p) lst))]))

Combinatorial Explosions

A naive implementation of the Fibonacci sequence is an exponential-time algorithm:
(define (fib n)
  (if (<= n 2)
      1
      (+ (fib (- n 1))
         (fib (- n 2)))))
The trouble with this implementation is that certain recursive calls, with the same results, are repeated an excessive number of times.

A Solution

One way to resolve this is to ensure that each recursion is only called once, by using a hash table. If the function call is present in the table, its value can be retrieved rather than computed. This technique is called memoization or top-down dynamic programming.

The trouble with this solution is that the implementation of the function becomes polluted with the need to interact with the hash table:

(define (fib-hash n)
  (let ((fibs (make-hash)))
    (letrec 
        ((fib-recur 
          (lambda (n)
            (if (hash-has-key? fibs n)
                (hash-ref fibs n)
                (let ((value (if (<= n 2) 
                                 1
                                 (+ (fib-recur (- n 1)) 
                                    (fib-recur (- n 2))))))
                  (hash-set! fibs n value)
                  value)))))
    (fib-recur n))))
What we would ideally like to do would be to define the function in the naive manner, but with a flag indicating that it is to be memoized:
(define-memo (fib n)
  (if (<= n 2)
      1
      (+ (fib (- n 1))
         (fib (- n 2)))))
We can achieve this by creating a define-memo macro.

Assignment

For this assignment, you are to: