Lambda bodies in Scheme (by alaric)
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?)