null_radix

SRFI-89 support of Guile

by mjbecze 11-Aug-2020

Recently I implemented SRFI-89 for Guile. You can check out the code here. If you use Guix you can install it with guix install guile-srfi-89

Why?

The motivation for me was that I wanted to write more portable scheme code. Some say Scheme has a problem with a fractured ecosystem. One way to combat a fractured ecosystem is to write portable scheme code. SRFIs help with this.

SRFI-89 - Optional positional and named parameters

Guile has a rather nice extended define know as define* It is close to the define* of SRFI-89 but there are some differences. Guile's define* allows you to add optional and named parameters to your procedures. It works like this

(define* (optional-proc a b #:optional c d . e) '()
  (list a b c d e))

Here, all parameters after #:optional - that is c d and e - are optional and don't have to be used when the procedure is called. The parameters a b c d are known as positional parameters, since the position of the values used when calling the produced determines what value is bound to which variable. Parameters can also have names. For example:

(define* (sir-yes-sir #:key action how-high)
  (list action how-high))`

Here, any parameter occurring after #:key is a key that can be used as follows:

(sir-yes-sir #:action 'jump #:how-high 10)

The order of the keys doesn't matter, so this is equivalent to the above:

(sir-yes-sir #:how-high 10  #:action 'jump)

Keys can be used in any order and can have default values. This is really convenient when you have a procedure that may have many options.

Guile and SRFI-89 Differences

Named Parameters

SRFI-89's define* has nearly the same capabilities as Guile but there are a few differences.

Instead of using #:keyword <keywords> ... SRFI-89 uses the following syntax

(define* (sir-yes-sir (#:action action) (#:how-high how-high))
  (list action how-high))`

Each keyword is specified by being a keyword object paired with a variable that it will bind to in the body of the defined procedure.

Optional Parameters

Optionals also work differently. For example:

(define* (optional-proc a b (c #f) (d #f) . e) 
  (list a b c d e))

With SRFI-89's every optional must have a corresponding expression. With Guile, optionals always default to #f. This is also true for named parameters.

Rest

The . rest parameter catches any tailing arguments in a procedure. For example:

(define* (test a b . c)
  (list a b c))

(test 1 2 3 4 5 6)
-> (list 1 2 (4 5 6))

When using the rest syntax with named parameters in Guile the named parameters are also bound to the rest variable. For example:

(define* (test a #:key b . c) (list a b c))

(test 1 #:b 'z 2 3)
-> (list 1 'z (#:b 'z 2 3))

SRFI-89 on the other hand does not do this. In SRFI-89's world we would have the following:

(define* (test a (#:b b). c) (list a b c))

(test 1 #:b 'z 2 3)
-> (list 1 'z (2 3))

Forms

Guile's define* has one basic form which is:

(define* proc-name <positional> | <optionals> | <named> | <rest>)

While SRFI-89 has two forms:

(define* proc-name <positional> | <optionals> | <named> | <rest>)

or

(define* proc-name  <named> | <positional> | <optionals>  | <rest>)

This means you can write a procedure as:

(define* (test a-positional (an-optional-positional 1) (#:named name #f) . rest)
  (list a-positional an-optional-positional name rest))

or

(define* (test  (#:named name #f) a-positional (an-optional-positional 1) . rest)
  (list a-positional an-optional-positional name rest))

Implementation

To implement SRFI-89 I wanted to be as lazy as possible, performant and hygienic. The original implementation of SRFI-89 was not so performant on Guile compared to the native define* and was not hygienic. To start with I didn't know much about macros so to get up to speed I read Guile's documention and Writing Hygienic Macros in Scheme with Syntax-Case which I highly recommend. The latter has a bunch of nice examples that were very helpful. Also Oleg Kiselyov's page on marcos is quite fun.

To accomplish the goals of performance and being lazy I just reused Guile's define*. Lets see what the macro produces.

Here is an example of the form <positionals>|<named>|<rest>:

(define* (test a (b #f) (#:c c') (#:d d #t) . r)
  (list a b c))

Which results in:

(define test
  (guile:lambda*
    (a #:optional (b #f)
       #:key (c (guile:error "key c is required")) (d #t)
       #:rest t-8ae9909742848ae-ae4)
 
     (let ((r (remove-keywords t-8ae9909742848ae-ae4))
       (c' c)
       (d d))
     
       (list a b c))))

It's a quite straightforward mapping I think. The statement (c (guile:error "key c is required")) prevents c from defaulting to #f which is guile:lambda* default behavior. The let provides the mapping from keywords to internal variable names. And (remove-keywords t-8ae9909742848ae-ae4) removes the keywords from rest.

The other form <named>|<positionals>|<rest> is a bit more tricky. Here is an example:

(define* (test  (#:a a') (#:b b #t) c (d #f) . r)
  (list a b c))

Which results in:

(define test
  (guile:lambda*
    (#:key (a (guile:error key a is required)) (b #t)
     #:rest t-8ae9909742848ae-c77)
       (let ((a' a)
             (b b))
         (apply (guile:lambda* (c #:optional (d #f) #:rest r)
            (list a b c))
            (remove-keywords t-8ae9909742848ae-c77)))))

Since guile:lambda* doesn't support this form at all we need an extra guile:lambda*. The outer guile:lambda* processes the named parameters and leaves the positional parameters which can be accessed the #:rest. The inner guile:lambda* the processes the positional arguments after removing the keywords from rest.

comments/corrections?