Hello
I've got a general Scheme programming style question. Since there may
be different preferences around a particular Scheme implementation,
I'm asking here. Actually, depending on the decision on which style to
choose, I would vote for an efficiency improvement in gambit (see more
below on keyword arguments).
Especially since scheme is using runtime type checking, there are
frequently "error cases" when accessing data structures, i.e. the data
to be read is not available or has the wrong type. There are multiple
approaches to handle this:
a) call the current error handler, which usually transfers control to
a captured continuation.
b) (optionally) call a user-provided function instead, which may do
the same. (i.e. the error function is lexically provided instead of
dynamically).
c) return a special value (optionally user-provided), which the caller
has to check for.
With solution a, the user has to install a new error handler if he
wants to catch this, which is both tedious and relatively expensive.
Solution b improves upon this by using the more efficient (and
slightly less inconvenient) approach of providing the error handler
through a lexical parameter instead of a dynamic parameter. But,
unless also a success handler can be given, the user still has to use
call/cc to differentiate between success and failure.
Solution c may be the one looking as the most efficient, and most
simple from the perspective of the writer of the library. But the
library user then always needs to check the error values, it's a
duplication of branches (first, there are branches in the library code
to decide upon the value to be returned, then there have to be
branches to dispatch upon them). Maybe with a nice pattern matching
language, that issue at least doesn't appear to be a problem on the
surface anymore? (But there may also be cases where multiple kinds of
errors can happen--how to solve this, provide multiple special values
as arguments? Or, introduce an error object hierarchy instead, where
the caller can ask (exception? value) and handle depending on the
result (errors as first-class values). Of course returning errors or
other items meant to make the caller dispatch on them, might interfere
with storing those objects validly into the data structure (example:
hash tables). (How do other languages (Haskell?) solve this?))
There is also the case, where the library wants to return multiple
values. The implementation of R5RS values/call-with-values in Gambit
is basically a tuples approach (afaik the same approach as taken by ML
and Haskell). This makes it more elegant (in my opinion) than Schemes
which require continuations to expect exactly the number of values
being returned to them, since it's more flexible ("normal" generic
one-value handling Scheme code can handle them). But since they
allocate memory (I think ML can optimize that away partially), this is
not very efficient. And, this is intertwined with the above question
of how to dispatch on data errors: a function should have a way to
return to different places, some of them taking multiple values, some
of them one, some of them none ("manual CPS style").
For these reasons, I think I would happily dispense with
multi-value-bind (expect for compatibility reasons) and provide the
continuations manually. My only issues with this are:
- I want that to be as "standard" as possible (syntax-wise), over all
of my code and as much as possible other people's code. This means,
using the same function naming conventions and the same ordering of
arguments.
- Using keyword arguments may make it easier to achieve that goal:
something like:
(foo-bar/baz value
on-success: (lambda (bar baz)
... )
on-error: (lambda (error-object)
...))
could be used as a standard approach.
But this is (currently) not very efficient in Gambit: for one, the
keyword parsing code doesn't seem to be very fast, and worse, it
prevents the compiler from doing inlining (and maybe other
optimizations?) in block-compilation mode.
- Does specifying two (or maybe more) such continuations necessarily
require multiple closure allocations (unless optimized away in
block-compilation mode)? That would make it possibly slower than the
allocate-a-values-tuple approach in some cases.
- Instead of using keyword arguments, settle on a function naming approach:
Using the with- naming as hint that this function will take a
success continuation as it's second argument, and optionally an
error continuatoin as third:
(with-foo-bar a-foo
(lambda (bar) ...)
(lambda (error-object) ..))
Without the with-, expect it to take an optional error continuation
as the second argument, and optionally a success continuation as
third:
(let ((bar (foo-bar a-foo
(lambda (error-object) ...)))) ..)
or
(foo-bar a-foo
(lambda (error-object) ...)
(lambda (bar) ....))
as the user desires.
Below, I've pasted an excerpt of a library I'm working on for handling
Gambit syntax objects, which finally gave me the motivation to ask
about this style question here.
(Maybe writing such libs could be made easier with the help of some
macros, once a 'standard' is decided.)
Thanks for your suggestions
Christian.
(define (syntax-pos syn) ;; combined line/col
(vector-ref syn 3))
(define (make-syntax-error message)
(lambda (erroneous-object)
(error message erroneous-object)))
(define identity (lambda (v)
v))
(define (syntax-line syn
#!optional
(error (make-syntax-error "syntax-line: no position info:"))
(success identity))
(cond ((syntax-pos syn)
=> (lambda (pos)
(success (+ 1 (bitwise-and pos 65535)))))
(else
(error syn))))
(define (syntax-col syn
#!optional
(error (make-syntax-error "syntax-col: no position info:"))
(success identity))
(cond ((syntax-pos syn)
=> (lambda (pos)
(success (+ 1 (quotient pos 65536)))))
(else
(error syn))))
(define (with-syntax-line/col syn
success
#!optional
(error (make-syntax-error "with-syntax-line/col: no position info:")))
(cond ((syntax-pos syn)
=> (lambda (pos)
(success (+ 1 (bitwise-and pos 65535))
(+ 1 (quotient pos 65536)))))
(else
(error syn))))