Gauche Devlog

< Regexp read-write invariance | Records and util.match >


Extended formals

One thing I miss most when I hop back to Scheme from CL is CL's lambda list for optional and keyword arguments. I feel the CL's spec is too complicated for my taste (CLHS specifies 10 different kind of lambda lists), nevertheless I know it is useful.

Gauche has been providing argument parsing utility macros, let-optionals*, let-keywords and let-keywords* (ref:let-optionals*, ref:let-keywords*). These cover enough functionalities to deal with optional and keyword arguments. Yet they are different from being able to specify those arguments directly in the formals. One thing is that those macros makes code longer. Another thing is that I feel specifying them directly in the formals somewhat makes them more a part of the public contract of the procedure. They stand out in the source code, claiming that the procedure takes two required argments and three optional arguments, something like that. It makes easier to read the source. Which is precious.

So I've been secretly experimenting the CL-like extended formal list for almost two years. Now I'm convinced that it has enough advantages to be in officially.

Actually, there's a SRFI for extended formals (srfi:89). It introduces new forms, define* and lambda*, that recognize extended syntax for optional and named (keyword) arguments.

Providing new forms is a polite way---it leaves original Scheme intact, and won't step on existing code accidentally. It is highly desirable for a portable library, and understandable that srfi-89 took that path.

However, for this feature, I rather opted to extend define and lambda. Support of extended formals is an upper compatible change (I mean, even with this extention, proper R5RS programs runs just fine), and having different forms only to be polite makes the language unnecessarily complex. Another important advantage is that, by extending existing forms, extended formals will be available for the macros that expands into defines or lambdas. If we were to have different forms, we'd need to change such macros around to make the extended feature available.

This is a kind of decision highly depends on the target of the implementation. I think it is bad to conflate standard syntax if the implementation is for education; students could confuse the language itself and the implementation's specifics. But that's not the target of Gauche.

For what's worth, this extension is attached to define and lambda of the gauche module. The original semantics is still kept if you import null module (corresponds to (null-environment)) or scheme module (corresponds to (scheme-report-enviornment 5)). In other words, Gauche's lambda has different syntactic binding from R5RS's lambda.

★ ★ ★

Gauche's extended formal syntax is similar to Common Lisp's, but I use keywords :optional, :key, :allow-other-keys and :rest instead of CL's &optional, &key, &allow-other-keys and &rest. I saw no point adding extra reserved symbols.

(define (foo x :optional  (y 0) (z 1)) (list x y z))

gosh> (foo 9)
(9 0 1)
gosh> (foo 9 10)
(9 10 1)
gosh> (foo 9 10 11)
(9 10 11)
gosh> (foo 9 10 11 12)
*** ERROR: too many arguments for (lambda (x :optional (y 0) (z 1)) (list x y z))
(define (foo x :key (y 0) (z 1)) (list x y z))

gosh> (foo 9 :z -1)
(9 0 -1)
gosh> (foo 9 :z -1 :zz 3)
*** ERROR: unknown keyword :zz
(define (foo :key (x 0) :allow-other-keys) x)

gosh> (foo :z 9)
gosh> (foo :x 8 :y 9)

If no default value is given, the variable is bound to #<undef>. (which can be tested with undefined?, but that hasn't been documented. I'll make it public in 0.9.1, too.)

(define (foo :key x) x)

gosh> (foo)

#<undef> is first-class value, so we can't be sure if the argument isn't provided, or the argument is provided but its value happens to be #<undef>. CL solves this problem by allowing extra parameter, supplied-p-parameter, that binds to a boolean value indicating whether the argument is provided. Gauche doesn't support that feature yet.

Internally, these lambdas with extended formals are expanded into the base lambdas and let-optionals*/let-keywords*.

One twist I added is an optional parameter after :allow-other-keys.

(define (foo :key x y :allow-other-keys others) 
   (list x y others))

The parameter others is bound to a keyword-value list that didn't consumed by :key parameters.

gosh> (foo :w 0 :x 1 :y 2 :z 3)
(1 2 (:z 3 :w 0))

It is handy to a procedure that wraps another procedure, and that wants to filter out whatever extra keyword argument it gets.

★ ★ ★

After implementing extended formal list, I rewrote argument parsing macros in my code with this feature. Interestingly, I found let-keywords* etc. weren't obsoleted competely by extended formals. They are still useful when you factor out common option processing:

(define (some-api x y z . options)
  (check-common-options options)

(define (another-api a b . options)
  (check-common-options options)

(define (check-common-options options)
  (let-keywords* options ((key1 init1) (key2 init2) ...)

In CL, I would use destructuring-bind. But lack of that, it has a merit to have argument parsing feature separately from lambda syntax.

Tags: 0.9.1, formals, define, lambda

Post a comment