(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 5Often 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)) 5Only if all arguments return #f does or return #f:
> (or (> 2 3) (= 2 3)) #fWe 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)) #fThere 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 3We 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 3We 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 3I 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.