TL;DR: This establishes a very simple rule for THEN and ELSE. If you don't like the consequences of the rule, you make an object that spells out what you want the THEN and ELSE behavior to be. That object can also say how it will vaporize itself when not used with THENs or ELSEs...and when it does vanish in a puff of smoke it can turn into a multiple-return-value pack.
Building on the emerging art of ~[multi return packs]~ and isotopic objects, I'm coming up with what may be a resolution to the THEN and ELSE issues surrounding NULL and VOID.
-
As a default behavior, a "pure null" and a "pure void" are not reacted to by THEN. It will pass them through and allow an ELSE to act on them.
-
If a NULL or VOID appear as part of a multi-return parameter pack, they do not count as "nothing having happened". So THEN will react to a multi-return parameter pack--even if its first element is null or void.
-
This means those writing multi-return functions intended to work with ELSE and THEN should not set their multi-returns if the overall result is VOID or NULL.
-
I've implemented this as the default behavior for return void and return null when using the auto-proxying features of function. FUNC will not do the proxying--just as it does not do when you return a definitional (raised) error.
- You can think of this as being consistent with NULL being thought of as "soft failure"... if the function "fails" then the outputs are not written.
-
The base case of parameter packs of ~[_]~
and ~[~]~
being single packs of "meta null" and "meta void" are also THEN-reactive and not ELSE-reactive, regardless of not having other parameters in the pack:
>> if true [null]
== ~[_]~ ; isotope
>> x: if true [null]
; null
>> if true [null] then [print "NULL-in-multi-pack is not a NULL to THEN"]
NULL-in-multi-pack is not a NULL to THEN
-
These cheap mechanical rules should work for most cases... but lazy objects (object isotopes) can step in to fill in the gaps if there's a good ergonomic reason to bend these rules.
Solving A Tricky Case: TRAP
The way I had defined TRAP, it's a good example of something that didn't want to follow these new rules.
TRAP was a multi-return function, whose main result is an ERROR!, or NULL if not an error. But in the case of it being not an error, then a secondary return result would come back from it:
>> [error result]: trap [1 / 0]
== make error! [...zero-divide...]
>> result
; null
>> [error result]: trap [1000 + 20]
; null
>> result
== 1020
Expecting that breaks the rule: you can't return any multi-return results if you are returning pure null. But if it returned a heavy NULL you couldn't write code like this:
trap [1 / 0] then e -> [print ["Had an error" e.id"]]
Because if your code didn't have an error, it would still run the THEN branch:
>> trap [1000 + 20]
== ~[_]~ ; isotope
>> trap [1000 + 20] then [print "No error, but we still get TRAP THEN"]
No error, but we still get TRAP THEN
So to get a TRAP that will work in this fashion, you need a lazy object. And you can even make a lazy object that if asked to resolve itself, creates a parameter pack so you get the [error result]: assignability.
Here we can build that on top of ENTRAP (which returns a single value that's either an ERROR!, or the ^META result value)
trap: func [
return: [<opt> any-value!]
code [block!]
<local> result
][
if error? result: entrap code [
return/forward pack [result null] ; /FORWARD stops decay to result
]
return isotopic make object! [
else: branch -> [
(pack [unmeta result]) then (:branch) ; packs null, so it would run
]
reify: [pack [null unmeta result]]
]
]
Believe it or not, this actually does work. With THEN and ELSE:
>> trap [1000 + 20] then e -> [print ["E" e.id]] else r -> [print ["R" r]]
R 1020
>> trap [1 / 0] then e -> [print ["E" e.id]] else r -> [print ["R" r]]
E zero-divide
And for unpacking the values:
>> [error result]: trap [1 / 0]
== make error! [
type: 'Math
id: 'zero-divide
message: "attempt to divide by zero"
near: [1 / 0 **]
where: [/ entrap trap+ console]
file: _
line: 1
]
>> result
; null
>> [error result]: trap [1000 + 20]
; null
>> result
== 1020
So @rgchris can be happy because that is usable in a historically conventional way:
either [error result]: trap [
... your code here ...
][
... code that reads ERROR here...
][
... code that reads RESULT here...
]
What's great here is how ENTRAP is an agnostic building block, which can then be wrapped and packaged to one's liking.
Not Trivial To Understand, But Also Not Rocket Science
These are concerns for the implementers of things like TRAP and PARSE--not for the users. So it's important to see it in that light. It's power for those who need it to get the usage patterns they want.
The RETURN/FORWARD is simply to address the normal behavior of decay of multi-return packs to the first item.
When you write x: some-function ... then you're only getting the first parameter in a pack. We don't want return some-function ... to automatically turn your function into a multi-return function just because the SOME-FUNCTION you call happened to offer more outputs.
(I use the term /FORWARD in the sense of "forwarding"... whatever the results were, return them verbatim.)
Hopefully I don't discover any fatal flaws, because it's a really neat design--and I think these examples are only scratching the surface.