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

>> 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've added in some wild protections, to try and only let you use unevaluated non-block literals as branches. I think it's cool that "evaluated bit" mechanism has been developed, and it is a useful tool. But in my own experience, this has created a hassle when writing one's own custom conditionals that are layered on top of Rebol's conditionals.

(e.g. the user has passed you something that it wants to act as a branch, same as other Rebol constructs. You want to treat that branch as a black box, but it complains when you say (if modified-condition :branch) when branch is not a block, because although the user called you with a literal, you're now passing it through via a GET-WORD! evaluation...)

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 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 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.


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


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.

FIRST OFF: 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.

SECONDLY: I want to mention an exemption I've added for BLANK!. If you use a BLANK! you will get the same effect as a null in conditionals. This turned out to make sense when implementing the /DEFAULT emulation on SWITCH, because when the refinement is not supplied it is a blank... and it's nice to not have to worry about that:

switch-d: enclose (augment 'switch [
     /default "Default case if no others are found"
 ]) func [f [frame!]] [
     let def: f/default
     do f else (def)  ; def will be BLANK! if /default not specified

If BLANK! is tolerated, then that DEF can act as it should; if you don't specify the /default and it runs, it "opts out" and resolves to a NULL.

But do note that is according to the rules of ELSE if a branch produces NULL. Should you use a BLANK! as the branch on an IF, and it runs, you will get a VOID!... because that is what it would have done if the branch returned NULL.

(There's a method to the madness, really there is. :crazy_face:)

1 Like