8.4. Examples


8.4. Examples

This section presents a series of illustrative syntactic extensions defined with either syntax-rules or syntax-case, starting with a few simple but useful syntactic extensions and ending with a fairly complex mechanism for defining structures with automatically generated constructors, predicates, field accessors, and field setters.

The simplest example in this section is the following definition of rec. rec is a syntactic extension that permits internally recursive anonymous (not externally named) procedures to be created with minimal effort.

 (define-syntax rec   (syntax-rules ()     (( x e) (letrec ((x e)) x)))) (map (rec sum        (lambda (x)          (if (= x 0)              0              (+ x (sum (- x 1))))))      '(0 1 2 3 4 5))  (0 1 3 6 10 15) 

Using rec, we can define the full let (both unnamed and named) as follows.

 (define-syntax let   (syntax-rules ()     (( ((x v) ...) e1 e2 ...)      ((lambda (x ...) e1 e2 ...) v ...))     (( f ((x v) ...) e1 e2 ...)      ((rec f (lambda (x ...) e1 e2 ...)) v ...)))) 

Of course, we can also define let directly in terms of letrec, although the definition is a bit less clear.

 (define-syntax let   (syntax-rules ()     (( ((x v) ...) e1 e2 ...)      ((lambda (x ...) e1 e2 ...) v ...))     (( f ((x v) ...) e1 e2 ...)      ((letrec ((f (lambda (x ...) e1 e2 ...))) f) v ...)))) 

These definitions rely upon the fact that the first pattern cannot match a named let, since the first subform of a named let must be an identifier, not a list of bindings. The following definition uses a fender to make this check more robust.

 (define-syntax let   (lambda (x)     (syntax-case x ()       (( ((x v) ...) e1 e2 ...)        (syntax ((lambda (x ...) e1 e2 ...) v ...)))       (( f ((x v) ...) e1 e2 ...)        (identifier? (syntax f))        (syntax ((rec f (lambda (x ...) e1 e2 ...)) v ...)))))) 

Of course, to be completely robust, the ids? and all-ids? checks employed in the definition of unnamed let in Section 8.3 should be employed here as well.

Both variants of let are easily described by simple one-line patterns, but do requires a bit more work. The precise syntax of do cannot be expressed directly with a single pattern because some of the bindings in a do expression's binding list may take the form (var val) while others take the form (var val update). The following definition of do uses syntax-case internally to parse the bindings separately from the overall form.

 (define-syntax do   (lambda (x)     (syntax-case x ()       (( (binding ...) (test res ...) exp ...)        (with-syntax ((((var val update) ...)                       (map (lambda (b)                              (syntax-case b ()                                ((var val)                                 (syntax (var val var)))                                ((var val update)                                 (syntax (var val update)))))                           (syntax (binding ...)))))         (syntax (let doloop ((var val) ...)                   (if test                       (begin (if #f #f) res ...)                       (begin exp ... (doloop update ...)))))))))) 

The odd looking expression (if #f #f) is inserted before the result expressions res in case no result expressions are provided, since begin requires at least one subexpression. The value of (if #f #f) is unspecified, which is what we want since the value of do is unspecified if no result expressions are provided. At the expense of a bit more code, we could use syntax-case to determine whether any result expressions are provided and to produce a loop with either a oneor two-armed if as appropriate. The resulting expansion would be cleaner but semantically equivalent.

As mentioned in Section 8.2, ellipses lose their special meaning within templates of the form ( template), This fact allows syntactic extensions to expand into syntax definitions containing ellipses. This usage is illustrated by the definition below of be-like-begin.

 (define-syntax be-like-begin   (syntax-rules ()     (( name)      (define-syntax name        (syntax-rules ()          (( e0 e1 (... ...))           (begin e0 e1 (... ...)))))))) 

With be-like-begin defined in this manner, (be-like-begin sequence) has the same effect as the following definition of sequence.

 (define-syntax sequence   (syntax-rules ()     (( e0 e1 ...)      (begin e0 e1 ...)))) 

That is, a sequence form becomes equivalent to a begin form so that, for example:

 (sequence   (display "Say what?")   (newline)) 

prints "Say what?" followed by a newline.

The following example shows how one might restrict if expressions within a given expression to require the "else" (alternative) subexpression by defining the local if in terms of the top-level if.

 (let-syntax ((if (lambda (x)                    (syntax-case x ()                      (( e1 e2 e3)                       (syntax (if e1 e2 e3)))))))   (if 1 2 3))  2 (let-syntax ((if (lambda (x)                    (syntax-case x ()                      (( e1 e2 e3)                       (syntax (if e1 e2 e3)))))))   (if 1 2))  error 

Although this local definition of if looks simple enough, there are a few subtle ways in which an attempt to write it might go wrong. If letrec-syntax were used in place of let-syntax, the identifier if inserted into the output would refer to the local if rather than the top-level if, and expansion would loop indefinitely.

Similarly, if the underscore were replaced with the identifier if, expansion would again loop indefinitely. The if appearing in the template (if e1 e2 e3) would be treated as a pattern variable bound to the corresponding identifier if from the input form, which denotes the local version of if.

Placing if in the list of literals in an attempt to patch up the latter version would not work either. This would cause syntax-case to compare the literal if in the pattern, which would be scoped outside the let-syntax expression, with the if in the input expression, which would be scoped inside the let-syntax. Since they would not refer to the same binding, they would not be free-identifier=?, and a syntax error would result.

The conventional use of underscore () helps the programmer avoid situations like these in which the wrong identifier is matched against or inserted by accident. It is an error to generate a reference to an identifier that is not present within the context of an input form, which can happen if the "closest enclosing lexical binding" for an identifier inserted into the output of a transformer does not also enclose the input form. For example,

 (let-syntax ((divide (lambda (x)                        (let ((/ +))                          (syntax-case x ()                            (( e1 e2)                             (syntax (/ e1 e2))))))))   (let ((/ *)) (divide 2 1))) 

results in an error to the effect that / is referenced in an invalid context, since the occurrence of / in the output of divide is a reference to the variable / bound by the let expression within the transformer.

As noted in the description of identifier? in Section 8.3, singleton identifiers can be treated as syntactic extensions and expanded into arbitrary forms. Often, it is necessary to treat the case where an identifier appears in the first position of a structured expression differently from the case where it appears elsewhere, as in the pcar example given in the description for identifier?. In other situations, both cases must or may be treated the same. The form identifier-syntax defined below can make doing so more convenient.

 (define-syntax identifier-syntax   (lambda (x)     (syntax-case x ()       (( e)        (syntax          (lambda (x)            (syntax-case x ()              (id               (identifier? (syntax id))               (syntax e))              ((id x (... ...))               (identifier? (syntax id))               (syntax (e x (... ...))))))))))) (let ((x 0))   (define-syntax x++     (identifier-syntax       (let ((t x)) (set! x (+ t 1)) t)))   (let ((a x++))     (list a x)))  (0 1) 

The following example uses identifier-syntax, datum->syntax-object, and local syntax definitions to define a form of method, one of the basic building blocks of object-oriented programming (OOP) systems. A method expression is similar to a lambda expression, except that in addition to the formal parameters and body, a method expression also contains a list of instance variables (ivar ). When a method is invoked, it is always passed an object (instance), represented as a vector of fields corresponding to the instance variables, and zero or more additional arguments. Within the method body, the object is bound implicitly to the identifier self and the additional arguments are bound to the formal parameters. The fields of the object may be accessed or altered within the method body via instance variable references or assignments.

 (define-syntax method   (lambda (x)     (syntax-case x ()       ((k (ivar ...) formals e1 e2 ...)        (with-syntax (((index ...)                       (let f ((i 0) (ls (syntax (ivar ...))))                         (if (null? ls)                             '()                             (cons i (f (+ i 1) (cdr ls))))))                      (self (datum->syntax-object (syntax k) 'self))                      (set! (datum->syntax-object (syntax k) 'set!)))         (syntax           (lambda (self . formals)             (let-syntax ((ivar (identifier-syntax                                  (vector-ref self index)))                         ...)               (let-syntax ((set! (syntax-rules (ivar ...)                                    ((_ ivar e)                                     (vector-ref self index))                                    ...                                    ((_x e) (set! x e)))))                e1 e2 ...))))))))) 

Local bindings for ivar and for set! make the fields of the object appear to be ordinary variables, with references and assignments translated into calls to vector-ref and vector-set!. datum->syntax-object is used to make the introduced bindings of self and set! visible in the method body. Nested let-syntax expressions are needed so that the identifiers ivar serving as auxiliary keywords for the local version of set! are scoped properly. The examples below demonstrate simple uses of method.

 (let ((m (method (a) (x) (list a x self))))   (m #(1) 2))  (1 2 #(1)) (let ((m (method (a) (x)            (set! a x)            (set! x (+ a x))            (list a x self))))   (m #(1) 2))  (2 4 #(2)) 

In a complete OOP system based on method, the instance variables ivar would likely be drawn from class declarations, not listed explicitly in the method forms, although the same techniques would be used to make instance variables appear as ordinary variables within method bodies.

The next example defines a define-integrable form that is similar to define for procedure definitions except that it causes the code for the procedure to be integrated, or inserted, wherever a direct call to the procedure is found. No semantic difference is visible between procedures defined with define-integrable and those defined with define, except that a top-level define-integrable form must appear before the first reference to the defined identifier, and syntactic extensions within the body of the defined procedure are expanded at the point of call. Lexical scoping is preserved, the actual parameters in an integrated call are evaluated once and at the proper time, integrable procedures may be used as first-class values, and recursive procedures do not cause indefinite recursive expansion.

A define-integrable has the following form.

 (define-integrable name lambda-expression) 

A define-integrable form expands into a pair of definitions: a syntax definition of name and a variable definition of a generated name, residual-name. The transformer for name converts apparent calls to name into direct calls to lambda-expression. Since the resulting forms are merely direct lambda applications (the equivalent of let expressions), the actual parameters are evaluated exactly once and before evaluation of the procedure's body, as required. All other references to name are replaced with references to residual-name. The definition of residual-name binds it to the value of lambda-expression. This allows the procedure to be used as a first-class value. Within lambda-expression, wherever it appears, name is rebound to a transformer that expands all references into references to residual-name. The use of fluid-let-syntax for this purpose prevents indefinite expansion from indirect recursion among integrable procedures. This allows the procedure to be recursive without causing indefinite expansion. Nothing special is done by define-integrable to maintain lexical scoping, since lexical scoping is maintained automatically by the expander.

 (define-syntax define-integrable   (lambda (x)     (define make-residual-name       (lambda (name)         (datum->syntax-object name           (string->symbol             (string-append "residual-"               (symbol->string (syntax-object->datum name)))))))   (syntax-case x (lambda)     (( name (lambda formals form1 form2 ...))      (identifier? (syntax name))      (with-syntax ((xname (make-residual-name (syntax name))))        (syntax          (begin            (define-syntax name              (lambda (x)                (syntax-case x ()                  (_ (identifier? x) (syntax xname))                  ((_ arg (... ...))                   (syntax                     ((fluid-let-syntax                        ((name (identifier-syntax xname)))                        (lambda formals form1 form2 ...))                     arg (... ...)))))))           (define xname             (fluid-let-syntax ((name (identifier-syntax xname)))               (lambda formals form1 form2 ...)))))))))) 

Some Scheme compilers integrate procedures automatically when it is appropriate to do so. Compilers cannot normally integrate procedures bound at top-level, however, since code that assigns top-level variables can be introduced into the system (via eval or load) at any time. define-integrable can be used to force the integration of procedures bound at top-level, even if the integration of locally bound procedures is left to the compiler.

The final example of this section defines a simple structure definition facility that represents structures as vectors with named fields. Structures are defined with define-structure, which takes the form:

 (define-structure name field ...) 

where name names the structure and field names its fields. define-structure expands into a series of generated definitions: a constructor make-name, a type predicate name?, and one accessor name-field and setter set-name-field! per field name.

 (define-syntax define-structure   (lambda (x)     (define gen-id       (lambda (template-id . args)         (datum->syntax-object template-id           (string->symbol             (apply string-append                    (map (lambda (x)                           (if (string? x)                           x                           (symbol->string                             (syntax-object->datum x))))                     args))))))  (syntax-case x ()     (( name field ...)      (with-syntax        ((constructor (gen-id (syntax name) "make-" (syntax name)))         (predicate (gen-id (syntax name) (syntax name) "?"))         ((access ...)          (map (lambda (x) (gen-id x (syntax name) "-" x))               (syntax (field ...))))         ((assign ...)          (map (lambda (x) (gen-id x "set-" (syntax name) "-" x "!"))               (syntax (field ...))))         (structure-length (+ (length (syntax (field ...))) 1))         ((index ...) (let f ((i 1) (ids (syntax (field ...))))                        (if (null? ids)                            '()                            (cons i (f (+ i 1) (cdr ids)))))))      (syntax (degin                (define constructor                  (lambda (field ...)                    (vector 'name field ...)))                (define predicate                  (lambda (x)                    (and (vector? x)                         (= (vector-length x) structure-length)                         (eq? (vector-ref x 0) 'name))))                (define access (lambda (x) (vector-ref x index))) ...                (define assign                  (lambda (x update)                    (vector-set! x index update)))                ...))))))) 

The constructor accepts as many arguments as there are fields in the structure and creates a vector whose first element is the symbol name and whose remaining elements are the argument values. The type predicate returns true if its argument is a vector of the expected length whose first element is name.

Since a define-structure form expands into a begin containing definitions, it is itself a definition and can be used wherever definitions are valid.

The generated identifiers are created with datum->syntax-object to allow the identifiers to be visible where the define-structure form appears.

The examples below demonstrate the use of define-structure.

 (define-structure tree left right) (define t   (make-tree     (make-tree 0 1)     (make-tree 2 3))) t  #(tree #(tree 0 1) #(tree 2 3)) (tree? t)  #t (tree-left t)  #(tree 0 1) (tree-right t)  #(tree 2 3) (set-tree-left! t 0) t  #(tree 0 #(tree 2 3)) 

Since the bodies of the generated procedures are short and simple, it may be desirable to use define-integrable as defined above in place of define for some or all of the generated procedure definitions.




The Scheme Programming Language
The Scheme Programming Language
ISBN: 026251298X
EAN: 2147483647
Year: 2003
Pages: 98

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net