Backpedaling on non-BLOCK! branches

We've had considerable time now to mess around with the option of letting IF take non-block branches.

As the person who advocated for it initially, I can say that I've been deeply on the fence about it...increasingly so, now to the point of thinking we should revert it.

The conundrum we wound up with is that BLOCK! branches are treated fundamentally differently from other kinds of values...they are executed as code. So if you see an expression that isn't a literal used as a branch, you are disoriented. As a reader, you are uncertain whether that expression's evaluation is the end of the evaluation...or if another will happen. And as a writer, you are going to be prone to making mistakes.

This problem is not new, and hits every new user when PRINT treats blocks differently.

>> x: 10
-- lots and lots of code --
>> print x
10

>> x: [format hard drive]
-- lots and lots of code --
>> print x
-- oops --

But I've long believed that is a weakness, and a fairly serious one at that. So we should go more toward finding solutions to that category of problem, vs. propagating it more widely and making the system more unpredictable on the whole...for limited value.

I know @IngoHohmann has spoken up for non-block branches, and @rgchris has too. Yet I'm afraid I'm going to recommend we abandon non-block branches, because they cause too much trouble for what they have truly gained us.

UPDATE: At the time this post was written, there were some symbolic constructs for using branches literally. Those symbols are retaken for other purposes, and soft-quoted branching provides a superior alternative, e.g. if condition '{string}.

I will also recommend CASE be changed to disallow non-block branches:

block: [print "this will be legal, too"]
x: 17
case/all [
    1 > 0 [print "this will be legal"]
    2 > 0 block
    3 > 0 x ;-- not legal
]

Instead, one can use the CHOOSE primitive, which will never double-evaluate, and returns blocks as-is:

x: choose [
    1 < 2 [print "returned as value"]
]

>> x
[print "returned as value"]
1 Like

I'm afraid this decision would affect all branch-like things, including default.

So where before you could say:

 x: default 10

You will have to say

x: default [10]

While this looks like a deep aesthetic loss, I think that the point was that people would have a hard time understanding that:

x: default 10 + 20

...would run the addition regardless of whether x had a value or not. And if a function was called with side-effects, that gets much worse and bites people harder. The allure of the simple case makes one forget the challenges posed by the more complex cases.

I'd attempted to design protections that would allow the x: default 10 and disallow x: default 10 + 20. But these protections become very inconvenient for abstractions...to the point of becoming very hard to work with. In the balance of overall simplicity vs. source clutter, I think the slightly-less-elegant source needs to win here.

There is one possibility here that the "core" versions, IF*, DEFAULT*, etc. might tolerate the non-block branches...as they might be assumed to be for "experts". But that's not really the purpose of these functions, as it regards their treatment of voids and blanks. I don't know if it's a great idea, but I'll put it out there. Yet is:

x: default* 10

Really better than:

x: default [10]

...?

I found that non-block! branches were a little confusing for me so agree with regression.

2 Likes

I think making CASE only take block branches--beyond the reasons of double-evaluation clarity--will also make it a bit less error prone. If your expressions are out of sync, any non-blocks that wind up in branch slots will fail noisily.

CASE has been shaping up in other ways, for anyone who hasn't been following:

CASE follows Stricter Structure (must pair, etc.)

@BrianH would semi-frequently use non-block cases, in particular blank ones (none, in R3-Alpha). I suppose the theory was that it kept the total series count down, and you wouldn't recurse in the evaluator.

I feel like making empty blocks execute fast probably covers it. I've also kind of wondered if Rebol really needs an "identity guarantee", or if all locked/read-only empty blocks could just point to the same empty block node. This is a fairly deep question, so I don't know...but currently you cannot do lookups by identity (in maps, blocks, etc) only by equality. So SAME? is kind of an outlier operation that you can't really use unless you have the things right in your hand.

We could, also, accept blank branches, as well as blocks. But that doesn't feel right to me.

Note that one thing we will probably continue to accept are functions in branch slots. 0 arity functions will be run normally, arity-1 functions will receive the evaluated condition that triggered the branch. Allowing this seems a bit more comfortable knowing that the number of types people are trying to put into branches are more limited.

The deed is done.

I forgot to mention one additional angle, which was actually predicted by Maxim, and that is it's not only CASE that benefits from the structure...but also your branches.

I've really only had the bug happen once. But it was something along the lines of:

 unspaced [
     either condition [
         ...
         lots of code, scrolls off screen
         ...
     ]

    "foo"
]

This happened when an EITHER got effectively changed into an IF, and it wound up consuming the "foo" that wasn't meant for it.

Anyway, I just wanted to bring this up in case anyone needs another reason to get behind the reversion.

^-- 2 years later? Sigh, time flies, eh.

I want to give an update on this thread to mention that soft-quoted branching offers an efficient alternative that doesn't look too bad:

x: default '10

It has the same cost profile as x: default 10 (no series allocation needed), and helps avoid the double-evaluation ambiguity mentioned. You can do this with x: default '[a b c] as well, which is lighter than x: default [[a b c]]...visually, and in terms of memory and runtime.

1 Like

I was thinking a bit today how it would be nice to allow literal strings:

print ["RETURNS:" return-note else "(undocumented)"]

But disallow variables, for the reasons given in the thread above:

msg: "(undocumented)"
print ["RETURNS:" return-note else msg]

It's a little bit cleaner than having to use a tick mark, which is hard to see without syntax highlighting unless you use the braced form of string:

print ["RETURNS:" return-note else '"(undocumented)"]
print ["RETURNS:" return-note else '{(undocumented)}]

...but This Makes Legal Branches Different from DO

I've been developing this sort of theory that what branches run are what the core provides for a main DO function...let's say it is called DO*.

I mentioned in a post about efficiency that a QUOTED! should respond in most ways the way that an ACTION! that produces a single value would respond. This gives you an efficient way of passing around a value that can do anything an action does that doesn't require it to have an identity. And one of those things is DO. This ties in to the branching.

That covers the soft-quoted branching behavior case, and makes sense...because it's one of the good reasons we want it for soft-quoted branching. Removing a quote level is computationally trivial and doesn't require any allocations.

But if we were to allow something like a plain TEXT! string to act as a branch, that would be different than what we'd want high-level DO to offer:

>> if true "print {We don't want this.}"
We don't want this.

I think when we add up all the rationales in this thread with that, it makes sense for us to say: there is no action taken on a branch of a conditional that wouldn't match what DO would do for a value of that type.

This gives more hope and clarity to those trying to implement their own branching constructs.

1 Like