Lambda bodies in Scheme (by )

So, if you look at a recent Scheme standard such as R7RS, you'll see that the body of a lambda expression is defined as <definition>* <expression>* <tail expression>; zero or more internal definitions, zero or more expressions evaluated purely for their side-effects and the results discarded, and a tail expression whose evaluation result is the "return value" of the resulting procedure.

I used to find myself using the internal definitions as a kind of let*, writing procedures like so:

(lambda (foo) (define a ...some expression involving foo...) (define b ...some expression involving a and/or foo...) ...some final expression involving all three...)

But the nested defines looked wrong to me, and if I was to follow the specification exactly, I couldn't intersperse side-effecting expressions such as logging or assertions amongst them. And handling exceptional cases with if involved having to create nested blocks with begin.

For many cases, and-let* was my salvation; it works like let*, creating a series of definitions that are inside the lexical scope of all previous definitions, but also aborting the chain if any definition expression returns #f. It also lets you have expressions in the chain that are just there as guard conditions; if they return #f then the chain is aborted there and #f returned, but otherwise the result isn't bound to anything. I would sometimes embed debug logging and asserts as side-effects within expressions that returned #t to avoid aborting the chain, but that was ugly:

(and-let* ((a ...some expression...) (_1 (begin (printf "DEBUG\n") #t)) (_2 (begin (assert (odd? a)) #t))) ...)

And sometimes #f values are meaningful to me and shouldn't abort the whole thing. So I often end up writing code like this:

(let* ((a ...) (b ...)) (printf "DEBUG\n") (assert ...) (if ... (let* ((c ...) (d ...)) ...) ...))

And the indentation slowly creeps across the page...

However, I think I have a much neater solution!

First, I'll demonstrate what it looks like, then get into how it works.

(block (/let a 1) (begin (printf "DEBUG LOGGING\n")) (/assert (odd? a)) (/let b 3) ;; Final value expression (+ a b))

block is my new macro, although I'd like to redefine lambda to wrap its body in an implicit block rather than invoking it directly like this. As is hopefully obvious, (/let A B) defines A to the value of B thereafter in the block, begin can be used to wrap some code to run purely for side-effects, /assert just checks an assertion (shorthand for (begin (assert ...)) really), and the final expression is returned from the block.

For convenience, /let is defined in terms of Chicken's match-let, so it's destructuring - you can write:

(block (/let (a b) '(1 2)) (+ a b))

...and get 3. I've also defined a /flet that's shorthand for writing a procedure, much like you can with define. /flet actually uses a letrec under the hood so the procedure can recursively call itself:

(block (/flet (odd? x) (cond ((zero? x) #f) ((eq? 1 x) #t) (else (odd? (- x 2))))) (odd? 5))

My /let just defines one thing at a time, and if you have lots of definitions, you might miss the behaviour of traditional let. But the good news is, you can still use it, just leave the body empty:

(block (let ((a 1) (b 2))) (+ a b))

You can also use if to early abort, creating an effect sort of like a cond:

(define (odd? x) (block (if (zero? x) #f) (if (eq? 1 x) #t) (odd? (- x 2))))

This may have given you a hint as to what's going on here - all the block macro does is to convert its body into a kind of macro-level continuation passing style:

(block (A A1 A2) (B B1 B2) C) => (A A1 A2 (B B1 B2 C))

So when we use things like let and if in there, and begin for that matter, we're just wrapping the rest of the body into the final part of the let/if/begin form. /let just rewrites into a match-let with the final argument as its body:

(/let VAR VAL REST) => (matched-let ((VAR VAL)) REST)

Where VAR and VAL come from the (/let ...) expression inside the block, and REST is provided by the block macro itself.

This avoids out-of-control indentation for any form that nests a "body" as its final part. For instance, handle-exceptions:

(block (handle-exceptions exn (printf "An error happened!\n" #f)) ... #t)

Any exceptions after the handle-exceptions in the block will be handled as described. For convenience, I've also defined a /finally that contains one or more expressions that will be executed after the rest of the block, whether it's an exceptional or normal exit - a bit like Go's defer.

Sometimes you want to use some syntax that doesn't have the body at the end, so I've got a macro for that, which I called ->. It's like a sort of syntactic let/cc, bundling the final body argument into a thunk that it binds to a chosen name:

(block (-> ok (if X (ok) #f)) ...)

If the if succeeds it will "continue" and execute the rest of the block by calling ok, otherwise the whole expression returns #f. This is equivalent to:

(block (if (not X) #f))

Which reminds me, once can use short-circuiting and and or in a block, although I think it looks a little confusing!

Conclusions

I'm not sure about the names. Ideally, I think I'd re-bind begin instead of block, and also redefine lambda and things that wrap it to create block structures (like define) to have an implicit block as their body.

The /... convention is just for things I've redefined to have a final "body argument" so they fit the block pattern nicely; chosen purely because / is an easy key to press on my keyboard and the glyph isn't too jarring, no better reason than that.

I don't like the name ->, but have yet to think of a good, succinct, name for the operation!

Try it yourself...

You can view the syntax-highlighted source for Chicken 5, or download the raw source code and try it yourself. Please tell me what you think! I'd like to iterate the design a bit, tidy it up, then publish it as a Chicken egg (and maybe as an SRFI with a portable reference implementation, if there's interest?)

No Comments

No comments yet.

RSS feed for comments on this post.

Leave a comment

WordPress Themes

Creative Commons Attribution-NonCommercial-ShareAlike 2.0 UK: England & Wales
Creative Commons Attribution-NonCommercial-ShareAlike 2.0 UK: England & Wales