CSE341 Notes for Friday, 2/23/07

I spent most of the time demonstrating my solution to homework 7. It would be difficult to recreate that in notes, so I won't attempt it. Instead, I volunteer to answer any questions via email or on the message board if you don't understand something in the assignment writeup.

I spent the last part of class talking about an important facility that Scheme has for extending the language called macros. Section 3.1 of the Dybvig text is devoted to this, although the examples in the textbook are more detailed than I will cover in lecture.

Scheme was designed to have a small core set of operations from which everything else can be built. Being able to define procedures gives you much of this capability, but not enough. For example, we couldn't write something like Scheme's "if" with a simple procedure, as in:

        (define (if e1 e2 e3)
          ...)
If we defined if as a procedure, then when we call it, it would always fully evaluate all three of its arguments. As Nathan pointed out when he lectured on delayed evaluation, it is important that if first evaluates e1 and then evaluates only one of e2 and e3. We can't define this with a simple procedure.

Instead, Scheme has a special form called define-syntax that can be used to introduce new syntactic forms. It's general form is:

(define-syntax <name> (syntax-rules (<keywords>) (<pattern> <value>))) For example, suppose that we like the ML-style if/else construct with the keywords "then" and "else". We can write a macro to convert from that form to the built-in if. I called it "if2":

        (define-syntax if2
          (syntax-rules (then else)
            ((if2 e1 then e2 else e3)
              (if e1 e2 e3))))
This definition indicates that the new form will be called if2, meaning that we'll call it like this:

(if2 <something>) The syntax-rules clause begins by saying that it has special keywords "then" and "else". Then comes a list of two items. The first item indicates the pattern or general form that this will take. It begins with if2 and includes the keywords "then" and "else", but also includes three values identified as e1, e2 and e3. Scheme will recognize these as values to be provided when if2 is called. For example, a legal call would look like this:

        (if2 (< 3 4) then "foo" else "bar")
We saw that Scheme enforces this syntax, rejecting anything that doesn't fit the pattern that we provided in the call on define-syntax.

After the pattern in the define-syntax there is an expression indicating how to evaluate this. It translates a call like the one above into a call on the built-in if:

        (if e1 e2 e3)
So if you don't like Scheme's built-in syntax, you can define your own, including your own keywords.

Writing macros can be tricky. We looked at how to write a macro that would be equivalent in behavior to the built-in or. I asked people what or returns and some people seemed to think that it always returns either #t or #f. That's not quite right. The built-in or evaluates its arguments as long as they evaluate to #f or until one of them evaluates to something other than #f. If that happens, it returns that value. This is part of the general Scheme notion that #f represents false and everything else represents true. For example:

        > (or 2 3)
        2
        > (or (> 2 3) (+ 2 3))
        5
Only if all arguments return #f does or return #f:

        > (or (> 2 3) (= 2 3))
        #f
We can simulate this behavior with an if:

        (define-syntax or2
          (syntax-rules ()
            ((or2 a b)
             (if a a b))))
This provides a pretty good definition for or. It gets the same answer for all of the cases above:

        > (or2 2 3)
        2
        > (or2 (> 2 3) (+ 2 3))
        5
        > (or2 (> 2 3) (= 2 3))
        #f
There is a subtle problem with this, though. Someone said that it potentially evaluates its first argument twice. This can be a problem if the expression being evaluated has a side-effect like a call on set! or a call on display:

        > (or2 (begin (display "ha") (< 2 3)) (= 2 3))
        haha#t
We get two occurrences of "ha" because the expression is evaluated twice. The built-in or has no such problem:

        > (or (begin (display "ha") (< 2 3)) (= 2 3))
        ha#t
I didn't have time to show it, but we can fix this by using a let to store the result and evaluate just once:

        (define-syntax or3
          (syntax-rules ()
            ((or3 a b)
             (let ((aval a))
               (if aval aval b)))))        
This version behaves correctly:

        > (or3 (begin (display "ha") (< 2 3)) (= 2 3))
        ha#t

Stuart Reges
Last modified: Sun Feb 25 15:04:47 PST 2007