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
> 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 ... Hello! Finish begin-these...
Our macro match against
forms (denoted by
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
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
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.