Kernel is strict about boolean types
Kernel is the Scheme variant by John Shutt that I've been blogging about.
One thing about Kernel is that it does not let non-booleans be used as tests in conditionals. Ie, it doesn't use what other Lisps call the extended boolean - every object other than false is deemed true.
In a sense, I agree with this. There is virtue in keeping the types straight. But OTOH, it blocks some Scheme conveniences, which then need to be reinvented.
So it can't use #f as an out-of-band object
A common Scheme (and Lisp) idiom is to use the false object as an out-of-band object. For instance, a search that find nothing returns #f.
This is made easy by the latent typing that Scheme has. The #f object can go anywhere unless you explicitly check for type, and still it is easily checked for. But that's never been a perfect plan. There's always the question of what you do when the search (or w/e) gives the #f object itself. Sometimes there's also the question of how to distinguish different non-fatal failures.
But in Kernel that's not available because #f and #t aren't suppsoed to mix with other objects, lest they cause $if to exhibit hard-to-find bugs.
Kernel does provide a special object called #inert for "no
information" returns. John discusses this in the rationale for
assoc
, contemplating making assoc return a pair of a boolean and
either the answer of #inert. For instance, a successful search could
return:
(#t . The-Result)
and a failed search
(#f . #inert)
These are easily treated because Kernel makes multiple binding available everywhere.
But then he goes on to argue that under the hood, assoc
will have to
deal with a result object that is () on failure, and that the user can
easily re-represent it as bool+result if desired. He therefore
concludes that assoc
should just return either () or the found
object.
I'm not entirely sure I agree. It seems like this situation is
particular to assoc
(and assq
and a few others), not generally
available. Won't other search functions still have to return
bool+result? One could argue that it's more important to make assoc
uniform with other searchers, and the nil-on-failure version could be
provided also.
Also, it has always struck me that assoc
was often more difficult
than it needed to be, because instead of returning the value that's
usually sought, which is the cdr of the cell, it always returned the
whole cell. It does this for two reasons:
- Sometimes one wants the whole cell.
- It needed to have a means of indicating failure. If it returned the cdr, that would sometimes be (), the same as the failure result.
Kernel's multiple bindings almost solves this problem. It solves it in the success case. But if assoc's failure value is (), multiple bindings won't help. One can't bind the result's cdr, because () doesn't have one. So there's another argument for assoc returning bool+result.
So no and-let
One piece of convenience that this denies is and-let*
. And-let*
is a sort of hybrid of and
and let*
. It binds symbols
successively to values, like let*
does, but if any of those values
is #f it terminates, like and
does. and-let*
makes it very
convenient to alternate between getting values and testing that those
values are suitable. I find that this situation comes up a lot.
But since one can't (or shouldn't) mix booleans and non-booleans,
there won't be many #f results to terminate it. They could only come
from functions that return booleans, and bindings their results makes
little sense, since if and-let*
succeeds the values can only be #t.
What to replace and-let* with
IMO the best replacement for `and-let*' is a pattern-matching `let*'.
I wrote something like that as part of "match.el" in Emtest. It would
go beyond Kernel's what multiple bindings can do and be a true pattern
language. Constructor functions such as cons
and list
would have
matching destructurers. It'd be something like:
(pattern-let* ( ;;This clause has the same interpretation as in let (a value1) ;;Same interpretation as in Kernel's let but not Scheme's ((b c) value2) ;;Like ((d e) value3) in Kernel, because ~list~'s matcher ;;would accord with ~list~. (,(list d e) value3) ;;Call a matcher associated with the constructor ~foo~ to ;;destructure value4 and bind its parts to f and g (,(foo f g) value4) ;;Result is not bound but must be 12 (12 value5) ;;Result is not bound but must be #t (#t value6)) ;;In the body, a thru g are all bound. (do-stuff a b c d e f g))
This is clearly quite powerful. It probably belongs in a pattern-matching module.
No type signatures
The non-mixing of booleans and non-booleans suggests to me that there should be some sort of a type signature in Kernel, at least to distinguish booleans from everything else. But there isn't.
The name #inert
Kernel also has a special object called #ignore. ISTM #ignore and #inert are too similar in name and role, and would be easily confused. So I have some suggestions for renaming #inert:
- #n/a
- #not-applicable
- #not-available
- #no-info