I've certainly felt the frustration at voidification. If I'm writing something like:
wrapper: func [x y] [
case [
x > y [wrapped x + y]
x < y [wrapped x - y]
default [0]
]
]
If the wrapped function returns a NULL, it is a pain to get that passed through if it's going to get voided. So this is something where an answer is needed.
Originally all the control constructs had an /OPT refinement so you ended up with CASE
and CASE*
, and IF
and IF*
, with the *
denoting the specialization that passed through NULL unscathed. But this broke the usual rule I had for the *
functions, which meant "lower level function from which the higher level 'user friendly' version is built". Because you can't tell from the outside of a null-passthru CASE*
whether a branch was evaluated or not when you see a NULL. It's something the construct itself has to be told to do.
But refinements aren't as flexible as decorating specific branches. That way you can call out which ones you want to subvert voidification and which ones you don't. And it keeps you from complicating the interface with things like /OPT refinements that lead to strange decorated specializations...
wrapper: func [x y] [
case [
x > y :[wrapped x + y]
x < y :[wrapped x - y]
default [0]
]
]
Conceptually, it ties along with the Rebol black-box credo: if you're just passing it through and don't know what the range of values is, you should use a GET-WORD! or GET-PATH!, in case it's an ACTION! or a VOID!. So it's not the most nonsensical thing. Though it would maybe make more "holistic sense" if plain word access of NULL gave back VOID!, while GET-WORD! access gave null. (?!) But I don't think that's a good idea.
Tangent aside: this does mean that if your branch is in a variable, you'll have to DO it:
>> branch: [print "nulling!" null]
>> compose [case [true :[do branch]]]
nulling!
; null
Or COMPOSE. Fortunately, GET-GROUP!s compose to "getify" their argument, if a GET-type exists.
>> branch: [print "nulling!" null]
>> code: compose [case [true :(branch)]]
nulling!
== [case [true :[print "nulling!" null]]]
>> do code
; null
Or maybe GET-GROUP! itself is in on the trick, just as-is:
>> branch: [print "nulling!" null]
>> case [true :(branch)]
nulling!
; null
But it makes one wonder a bit about using GET-WORD! and GET-PATH! if this is a "soft-quoted branch". If the other GETs did nulling, do they also? For today, case [true :branch] is a synonym for case [true (:branch)] as is usual with soft-quoting. So it might seem odd to make it also mean that if branch is a null variable, or a block of code that evaluates to NULL, then you get the NULL and all bets are off as far as voidification. The only way you could get voidification in that case would be with the (:branch) as above, since the GROUP! is plain.
There are questions, but this provides a possibly coherent train of thought about an issue that I agree is a thorn. It ties into the other benefits of soft-quoted branching, like being able to see the quote mark and not have it disappear before the control construct sees it....