scheme – syntax-rules and :optional :keyword

Programming

scheme – syntax-rules and :optional :keyword

Edit. Interestingly, the final implementation offered below suffers a flaw. If the arguments supplied to let-syntax are unbalanced, the procedure throws an error. This occurs because let-keywords expects a list constructed only of :key value pairs. A potential solution to this is to filter the list with the predicate keyword? before operating on it.

Edit 2. See below for only-keywords function to solve the above.

This is a quick brain dump for a scheme problem I came across today. Suppose we are trying to define a begin-like form which prints something before and after the body arguments. A first pass attempt might look like the following:

> cat eg.scm
(define (print-me)
  (format #t "Hello!~%"))

(define (begin-these . these)
  (format #t "Start begin-these...~%")
  (begin these)
  (format #t "Finish begin-these...~%"))

(begin-these
  (print-me))

> gosh eg.scm
Hello!
Start begin-these...
Finish begin-these...

This didn’t work as expected because the (print-me) expression is evaluated before (begin-these). A solution to this is to use a macro.

> cat eg.scm
(define begin-these
  (syntax-rules ()
    ((_ form ...)
     (begin
       (format #t "Start begin-these...~%")
       (begin these)
       (format #t "Finish begin-these...~%")))))

> gosh eg.scm
Start begin-these...
Hello!
Finish begin-these...

Great that worked! Now what if I want to include some optional keyword arguments in to the mix? Gauche (to my knowledge) doesn’t support :keyword and :optional in the syntax-rules form. What we want is something that looks like this:

(define (begin-these :optional :key (num 3) :rest these)
   (format #t "Start begin-these [~a]...~%" num)
   (begin these)
   (format #t "Finish begin-these...~%"))

(begin-these :num 5
  (print-me))

… But without the early evaluation of print-me. We could try this with our macro definition and let-keywords:

> cat eg.scm
(define begin-these
  (syntax-rules ()
    ((_ form ...)
     (begin
       (let-keywords (list 'form ...)
                     ((num :num 3) . rest)
                     (format #t "Start begin-these [~a]...~%" num)
       form ...
       (format #t "Finish begin-these...~%")))))

(begin-these :num 12
  (print-me))

> gosh eg.scm
Start begin-these [12]...
Hello!
Finish begin-these...

Our macro match against form and forms (denoted by ...). In let-keywords, we reconstruct the list of arguments, quoting every element to avoid early evaluation. Note that only form needs quoting, the ellipsis repeats the quotation on the remaining arguments. The second argument to let-keywords defines the keywords and default values we are searching for, in our case we want to define the variable num, denoted by the keyword :num with a default value of 3. The rest part of this is the variable to assign the leftover arguments. Note that rest is mandatory for our use case, as we will have additional arguments after the keyword.

There is a problem though. let-keywords expects a balanced list containing key-value pairs. That is, if the number of elements in forms ... is odd, we get an error. The below gets around this by filtering the list given to let-keywords so that it contains only-keywords. take returns a list containing the first n elements, drop returns a list with the first n elements removed. Because our list now only contains keywords, we can remove the . rest. let-keywords will signal an error if it finds a key we have not specified.

(define (only-keywords args)
  (cond ((null? args) '())
	((keyword? (car args))
	 (append (take args 2) (only-keywords (drop args 2))))
	(else (only-keywords (cdr args)))))

(define begin-these
  (syntax-rules ()
    ((_ form ...)
     (begin
       (let-keywords (only-keywords (list 'form ...))
                     ((num 3))
                     (format #t "Start begin-these [~a]...~%" num)
       form ...
       (format #t "Finish begin-these...~%")))))

And that’s about it! The above does what we want.

Nice one.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.