CSE341 Notes for Wednesday, 5/20/09

I said that I wanted to finish up our discussion of Scheme by exploring some of the areas of the language that we hadn't had time to explore in detail.

I briefly discussed some of the applications people have made of Scheme and its predecessor, Lisp. Richard Stallman wrote the first version of emacs in Lisp and much of emacs is still written in Lisp. Stallman was the founder of the GNU project and an early pioneer of the free software movement. For example, here are some entries from my emacs initialization file (called .emacs):

        ;; set some defaults for text mode
        (setq default-major-mode 'text-mode)
        (setq initial-major-mode 'text-mode)
        (setq default-fill-column 79)
        
        ;; inhibit startup message
        (setq inhibit-startup-message 1)
        
        (setq load-path (cons "/Users/stuartreges/" load-path))
The setq function in Lisp is like the define procedure in Scheme.

Then we spent some time talking about the idea of macros. In an editor like emacs, you can define a macro that performs a sequence of editing commands. I personally use emacs macros all the time to get my work done. The commands for defining macros in emacs are fairly simple:

Emacs Command Description
CTRL-X ( begin defining a macro
CTRL-X ) end macro definition
CTRL-X e execute the macro
In programming macros are used to modify code. For example, the original C and C++ implementations did not include a boolean type, but you could "simulate" a boolean type by including a file called bool.h that included macro definitions that said that the word "bool" should be replaced with "int" and the word "false" should be replaced with "0".

It can be tricky to get macros right. For example, if you were going to change all occurrences of "true" to "1", you wouldn't want to change "construed" to "cons1d" and you wouldn't want to replace the word if it appeared in a quoted string or a comment. Any reasonable macro mechanism would be careful to avoid such obvious mistakes.

But there are lots of other tricky cases. Consider, for example, this C macro:

        #define SUM(x, y) x*x + y*y
The idea is that you might want to often find the sum of squares of two values and rather than use a function, you might want to have the expansion happen in-line in the code itself. I asked people where this might cause a problem and someone pointed out that you might pass a sum like "x + 1":

        z = SUM(x + 1, y);
This would be expaned by the macro into:

        z = x + 1*x + 1 + y*y;
That's not what you would expect. How do we fix it? By adding parentheses to the macro:

        #define SUM(x, y) (x)*(x) + (y)*(y)
Now our code expands to:

        z = (x + 1)*(x + 1) + (y)*(y);
When you write macros in C and C++, you often have to include a lot of parentheses. We saw that even this level of parentheses is not enough. For example, this expression:

        z = c * SUM(a, b);
is expanded into:

        z = c * (a)*(a) + (b)*(b);
So the macro should really be:

        #define SUM(x, y) ((x)*(x) + (y)*(y))
Even this isn't enough in some cases where someone might pass a value like x++. The situation gets even worse if you try to introduce a variable declaration because you might declare a variable like "foo" that shadows another variable called "foo". So people tend to declare very bizarre variable names and hope that there is no conflict.

The point is that macros can be tricky and the macro facility in C and C++ is very weak. Scheme, on the other hand, has a very powerful macro facility that doesn't require you to worry about adding extra parentheses or using weird variable names. The Scheme mechanism allows you to declare what programming language research refer to as hygienic macros.

In the last lecture we were working with this function that has a side effect that allows us to see when it is called:

        (define (work x)
          (display "work called: ")
          (display x)
          (newline)
          (+ x 1))
In the Pretty Big variant of Scheme, you define a macro using a special form called define-syntax. It's general form is:

(define-syntax <name> (syntax-rules (<keywords>) (<pattern> <value>))) We began by writing the equivalent of an if that is implemented by calling cond:

        (define-syntax if2
          (syntax-rules ()
            ((if2 test e1 e2)
             (cond (test e1)
                   (else e2)))))
In this form we don't introduce any keywords. We indicate that the pattern is the word if2 followed by three parameters. The cond indicates what value to return. We found that when you define this with a macro, Scheme delays the evaluation of the arguments. So we found that this macro had similar properties to if:

        > (if2 (< 2 3) (work 3) (work 4))
        work called: 3
        4
        > (if2 (> 2 3) (work 3) (work 4))
        work called: 4
        5
Often you want to include special keywords for your construct. 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 if3
          (syntax-rules (then else)
            ((if3 e1 then e2 else e3)
              (if e1 e2 e3))))
This definition indicates that the new form will be called if3, meaning that we'll call it like this:

(if3 <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 if3 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 if3 is called. For example, a legal call would look like this:

        (if3 (< 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.

Even in Scheme, 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
We were able to fix this by introducing a local variable with a let:

        (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
I then spent some time discussing how to simulate objects in Scheme. In a language like Java, we obtain an object by calling its constructor. In Scheme we can introduce a function that constructs the equivalent of an object:

	(define (make-foo)  ; simulates a constructor
          ...
When you construct an object, you allocate space for each of its fields. We can simulate thi with a let that defines some variables in an inner scope:

	(define (make-foo)  ; simulates a constructor
	  (let ((f1 e1) (f2 e2) (f3 e3) ... (fn en)) ; simulates fields
            ...
In addition to fields, an object has various methods that can act on those fields. We can simulate that by having a series of functions definined in the inner scope:

	(define (make-foo)  ; simulates a constructor
	  (let ((f1 e1) (f2 e2) (f3 e3) ... (fn en)) ; simulates fields
	    (define (m1 params) expr) ; simulates methods
	    (define (m2 params) expr)
	    (define (m3 params) expr)
            ...
But how do we gain access to one of several different methods? We do so by introducing a method that takes a method name as an argument and that calls one of the methods in the inner scope:

	(define (make-foo)  ; simulates a constructor
	  (let ((f1 e1) (f2 e2) (f3 e3) ... (fn en)) ; simulates fields
	    (define (m1 params) expr) ; simulates methods
	    (define (m2 params) expr)
	    (define (m3 params) expr)
	    (define (dispatch method-name)
	      (cond ((eq? method-name 'm1) m1)
	            ((eq? method-name 'm2) m2)
		    ...
		    (else (error "no such method"))))
            dispatch))
Notice that the dispatch function is what you are returned by the constructor. We looked at an example bankaccount.scm:

;;; bank account example - simulating object-oriented programming in Scheme
;;;
;;; (program adapted from one in 'Structure and Interpretation of
;;; Computer Programs')

(define (make-account)
   (let ((my-balance 0))

      ;; return the current balance
      (define (balance)
         my-balance)

      ;; make a withdrawal
     (define (withdraw amount)
        (if (>= my-balance amount)
           (begin (set! my-balance (- my-balance amount))
                   my-balance)
           "Insufficient funds"))

     ;; make a deposit
     (define (deposit amount)
        (set! my-balance (+ my-balance amount))
        my-balance)

     ;; the dispatching function -- decide what to do with the request
     (define (dispatch m)
        (cond ((eq? m 'balance) balance)
              ((eq? m 'withdraw) withdraw)
              ((eq? m 'deposit) deposit)
              (else (error "Unknown request -- MAKE-ACCOUNT"  m))))

      dispatch))


;;;   some commands to try:
;;;   (define acct1 (make-account))
;;;   (define acct2 (make-account))
;;;   ((acct1 'balance))
;;;   ((acct1 'deposit) 100)
;;;   ((acct1 'withdraw) 30)
;;;   ((acct1 'withdraw) 200)
;;;
;;;   ((acct2 'balance))  ; this is a different account from acct1!
I ended with some quotes about LISP/Scheme from Paul Graham. He has built a successful software career out of Lisp programming. He wrote the initial version of Yahoo! Store in Lisp and later sold the company to Yahoo.

My personal favorite quote is from Eric Raymond's essay "How to Become a Hacker":

Lisp is worth learning for the profound enlightenment experience you will have when you finally get it; that experience will make you a better programmer for the rest of your days, even if you never actually use Lisp itself a lot.

Stuart Reges
Last modified: Wed May 27 07:03:33 PDT 2009