Multiple Returns and Branching: Could It Unseat Voidification?

This thread from November 2020 is a notable historical moment, as it was the genesis of "isotopes".

"Perhaps NULL has an "isotope" that shares its not-able-to-be-put-in-blocks property, but that certain functions treat distinctly. And perhaps it decays, so you can't end up transferring the property into variables besides where the state originates."

It's interesting to note that it originated from a crazy experiment to pipe multi-returns through conditional chains--an unfit idea that is far surpassed by today's ideas. Sometimes you have to try something crazy just to articulate what's wrong with it...and in doing so, you might steer yourself to a novel solution.

Imagine that IF had multiple return values, and one of those values allowed it to indicate if it took a branch:

>> [value branched]: if true [null]
; null

>> value
; null

>> branched
== #[true]  ; it could theoretically even return which branch, e.g. `== [null]`

If somehow this piece of "/BRANCHED" (or /SUCCESS, or whatever) could make it through to ELSE, it could be used as the trigger.

For something like this to work, a finished function's frame would stick around long enough to see if a following function needed it...instead of just having the function's output cell. It would not be particularly easy to rig up...but not impossible.

The main design point would be how to know this is what you wanted. Is it just a matter of ELSE taking an argument /BRANCHED, and IF having an output / the names line up and it assumes you want them connected?

ELSE could still be willing to run if the /BRANCHED refinement was not provided by the left-hand side, in which case it would be purely NULL-reactive.

I'm not exactly sure what you'd do with:

if true [null] else [2] then [3]

The ELSE would get the first as having /BRANCHED, but then it didn't it's saying /BRANCHED is false. It would mean a THEN after an ELSE would only run if the ELSE ran. So:

>> if true [print "A", 1] else [print "B", 2] then [print "C", 3]  ; yay comma!
== 1

>> if false [print "A", 1] else [print "B", 2] then [print "C", 3]
== 3

That's different from today's value-driven (as opposed to branch-taken-driven) logic. It means there's a bit more invisibility to it, but it's explainable...probably more explainable than voidification.

This seems promising enough to try out, despite the likely implementation difficulty. Like I say, it doesn't mean NULL-reactivity can't stick around for ELSE in just would give conditional structures a new way to solve a frustrating problem.


So here are some technical issues that come up from further thinking about this promising-seeming idea. :hammer_and_wrench:

Currently, multiple return values must be requested by the caller. A value that is not requested might not be calculated...and the semantics of the function can be different based on how many values you request.

This is good. Because changing the semantics and getting additional outputs usually go hand-in-hand. You don't need separate named input parameters to instruct that you want an output, that you already have to name anyway.

Example: consider how PARSE now expects to go to the end by default:

>> parse "aaab" [some "a"]
; null  (e.g. didn't make it to the end)

But if you ask for /PROGRESS, then it knows partial matches are of interest to you. Requesting the output implicitly indicates you are going to look at the amount of progress

>> [value pos]: parse "aaab" [some "a"]
== "aaab"

>> value
== "aaab"  ; this could be NULL if end not reached, if we thought that better...

>> pos
== "b"

There would be shorthands for this, and adjusting the return value:

>> [_ #]: parse "aaab" [some "a"]  ; opt-out value (_), opt-in progress (#)
== "b"

You should have less terse options than that if you want more literacy, though this all requires design work:

>> [/progress]: parse "aaab" [some "a"]
== "b"

Long Story Short: I don't think the design should sacrifice this ability to do semantic control of behavior of functions with requested returns.

But How Does IF Know ELSE Wanted The /BRANCHED Output?

ELSE runs after IF. So it would need to be able to connect its need for a branched output up to IF's parameterization before IF runs. :pleading_face:

Not only that, but in the current formulation IF would needs a place to write the /BRANCHED output to. It doesn't write to its output cell directly (the BRANCHED frame local), because that holds the WORD! or PATH! or NULL or # which signals what to do. So it needs a bound word location to write... and if that's in the ELSE frame, else's frame has to be built, and IF needs to have its /BRANCHED parameter hold a WORD! bound into that pending frame before it has run.

This also raises the question of what to do if there's competing demands for the branched flag.

[value branched]: if false ["A"] else ["B"]

The current design puts a WORD! in the frame slot of IF corresponding to /BRANCHED. Right now, when the evaluator sees SET-BLOCK!, the function to the right is partially specialized with that word and then retriggered.

But value needs to be set from the result of the overall expression. So might the branch come from the ELSE, instead?

What about take an input branch signal AND making an output signal

If we think of ELSE and THEN as things that chain, then what happens if we want to know if they branched? We certainly do, e.g.

all [...] then [...] else [...]

The ELSE needs to know if the THEN branched or not.

But if /BRANCHED is an output of ALL, and an input to does ELSE receive it too as an output from THEN? Quite a pickle. :cucumber:

No matter how you look at it, this is fairly daunting stuff.

However, the incentive is high to try to work it out.

1 Like

I've hacked up sort of a prototype to look at which managed to do:

>> if true [null] else [<woo>]
; null

>> if false [null] else [<woo>]
== <woo>

(Although the code is disastrous, getting it to work at all required cleaning up some other stuff to be better. So that aspect was good at least.)

It operated on the basic premise I suggested. IF has a named additional output called branched::

>> [value branched]: if true [null]
; null

>> value
; null

>> branched
== #[true]

The system does some implicit wiring to connect that to an input that ELSE can addition to the ordinary value that comes through.

The biggest thing that bothers me about the technique is how easily it is disrupted when the parts aren't fit together in exactly this way.

For instance, right now you'd break it the moment you put the IF into a GROUP!.

>> (if true [null]) else [<woo>]
== <woo>

Though this points to an issue of brittleness with the current concept of multi-returns in general. A SET-BLOCK! wouldn't work with a GROUP! either, since it has to know what function to inject the output requests into, and it doesn't look deeply into groups to find that:

>> [value branched]: (if true [<a>])
** Error

(I don't consider this aspect of multi-returns a deal-breaker. The failure mode is a clear error...and you can always use plain refinements or APPLY to get what you're looking for. The SET-BLOCK! is just a convenience.)

There are other combinations that would be weak; such as how >- is used to invoke enfix from PATH!. That operator doesn't have a mechanism to propagate these requests for additional information:

>> (if true [null]) >- lib/else [<woo>]
== <woo>  ; not involved in the multi-return trick

Without another mechanism, there is too much brittleness to accept.

I think we need to avoid any solution where these act differently:

if condition [...] else [...]

if condition [...] comment "Hi" else [...]

(if condition [...]) else [...]

(if condition [...] comment "Hi!") >- else [...]

(if condition [...]) >- else [...]

Voidification May Be Ugly, But At Least It Does That...

Exchanging a single concrete value with known properties works. And we do have the ability to do this now:

>> if true @[null]
; null

You just can't use null producing branches with ELSE, or when they produce NULL they'll run both branches (which may be what you want, in which case, great).

>> if true @[print "A", null] else [print "B"]

A Targeted Solution For JUST THIS PROBLEM May Be Worth It

I was trying to conceive an answer with multiple returns to try and avoid making something that applied only to IF / ELSE. But it got pretty weird pretty fast.

Given the importance of the issue, perhaps some answer that isn't really good for anything besides this conditional situation would be worth it.

Perhaps NULL has an "isotope" that shares its not-able-to-be-put-in-blocks property, but that certain functions treat distinctly. And perhaps it decays, so you can't end up transferring the property into variables besides where the state originates.

That would run into substitutability problems, such as these cases acting differently:

(if condition [...]) else [...]  ; isotope NULL passed to ELSE

(x: if condition [...]), (x) else [...]  ; isotope "decay" leading to difference

That's a little bit unsettling, but you'd have gotten the same kind of behavior out of the multiple return solution, without the ability to work across GROUP!. It's probably learnable that if you didn't use an ELSE in a direct chain from something conditional, then you get the effect.

I'm worried about any of this being hidden... In the spirit of the named voids, the console should show you something to help discern. One question to answer would be which was the "real null"...I say that should be the thing the most functions return that don't have to worry about this issue.

>> if true [null]
; null (branched)

>> if false [<a>]
; null

Or we could just go all in and say there's just negative and positively charged NULL, and you learn that they get corrupted from one to the other here and there when conditionals are involved. It's a bit like voidification, but not crossing the more obvious datatype barrier to do it:

>> if true [null]
; null+

>> if false [<a>]
; null-

>> null
; null-

>> null else [print "Negatively charged nulls run ELSE."]
Negatively charged nulls run ELSE.

>> x: if true [null]
; null+

>> x then [print "Positively charged nulls run THEN."]
Positively charged nulls run THEN.

That seems insane, and it really makes me long for the comfort of voidification.

All Answers Must Be Balanced Against Voidification

The concreteness of voidification is still a strong asset, and the @ branches can get those cases where NULL is actually intended covered. So really everything needs to be weighed, here.

I think that naming the voids so you understood why they were voided was a big step in making voidification more accessible and learnable.

Just going to have to keep balancing the equations here as what's possible gets explored. Stay tuned...

1 Like

Wild. I'm trying to wrap my head around the isotype approach. Very cool that you're able to get a prototype isotype system working. :wink: