(define (work x)
(display "work called: ")
(display x)
(newline)
(+ x 1))
In Scheme, you define a macro using a special form called define-syntax. It's
general form is:
(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 (< 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 (work 3) (= 2 3))
work called: 3
work called: 3
3
We get two occurrences of "work called: 3" because the expression is evaluated
twice. The built-in or has no such problem:
> (or (work 3) (= 2 3))
work called: 3
3
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 (work 3) (= 2 3))
work called: 3
3
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.