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))))
Afficher les réponses par date