The Case for Literal (or soft-quoted?) branches in CASE

Rebol2, R3-Alpha, and Red are all quite a bit of a smattering of behaviors for CASE. Ren-C has progressively racheted up the control and semantics, which is particularly important given how free-form case statements are (enforcing pairs of conditions and branches).

But one loose aspect that is still in CASE is the ability to run a block branch evaluatively:

x: true
code: [print "This runs because X is true"]
case [
    false [print "This doesn't run"]
    x code
]

One has slightly less confidence that code is a branch than if it said x [do code]. But really, only slightly less...given that X could be a FUNCTION! either way.

So it seems to just tie into the freeform nature of Rebol, all things being equal. If there's no reason to prohibit it, why prohibit it?

But there may be a good reason...

There's a common pattern in case statements where if you reach a certain point in the processing, you want to run some code non-conditionally. With CASE/ALL, you can do this with TRUE in a branch slot, which it will run the branch as it passes by:

case/all [
    condition1 [...]
    condition2 [...]
    true [
        ;-- some code that runs at this point, always
        ;-- (assuming there wasn't a throw/return/fail...)
    ]
    condition3 [...]
    condition4 [...]
]

It's a clunky way to express it... kind of like having a TRUE condition as your last branch in an ordinary CASE is a clunky way of saying default. But, it works, and people used it.

But what if you have just a normal case? How could you run some processing after some number of cases didn't match, and then resume matching the following conditions?

Technically speaking, you can do it...but in a kind of ugly way that splices the code before the next condition in a GROUP!, so the result of that evaluation is thrown away, and then the result of the group is the condition you actually wanted:

case [
    condition1 [...]
    condition2 [...]
    (
        ;-- some code that runs at this point, if condition1
        ;-- and condition2 didn't match

        condition3 ;-- now process the condition
    ) [...]
    condition4 [...]
]

That's extremely inelegant. So much so that people don't do it...they fiddle some variable, exit the case, check the variable and start a new case.

But with the rise of ELIDE, I thought it could save the day:

case [
    condition1 [...]
    condition2 [...]
    elide (
        ;-- some code that runs at this point, if condition1
        ;-- and condition2 didn't match
    )
    condition3 [...]
    condition4 [...]
]

Beautiful! But there's just one problem. Since the blocks are in evaluative slots, they force the hand of ELIDE so it runs when the block after condition2 is processed. This happens whether the branch is taken or not. So you could wind up running the code if condition2 is taken and it never makes it to condition3. :frowning:

(If you don't understand the mechanics of why the left-to-right evaluation forces ELIDE in this way, read about it here)

If CASE only accepted literal blocks for branches, the problem would go away. And you'd be able to use the ELIDE pattern.

Having pushed the various degrees of freedom around and seeing the tradeoffs, it seems to me that having to say case [true [do code]] instead of case [true code] is a small price to pay.

  • CASE structure becomes clearer (though not 100% so, as not all blocks may be assumed to be branches, they could be arguments to a function in the condition)
  • Performance goes up...not having to call into the evaluator potentially twice per branch.
  • You get the awesome use of ELIDE, and it works in CASE/ALL too (so you don't need to use the TRUE condition "hack")

I think it's a winning proposition. Any objections?

Note that CHOOSE exists now if you are just picking values:

x: 20
result: choose [
    x < 10 <thing>
    x > 10 [literal block]
]

That gets you result = [literal block]. I'd argue this should not be evaluative for the same reasons, and if you need the thing you're choosing to have some evaluation before choosing use COMPOSE.

text: "thing"
x: 20
choose compose [
   x < 10 (to tag! text)
   x > 10 [literal block]
]

We could compromise, with soft quoted slots...

If the slots were soft quoted, then they would evaluate only if a GROUP! or GET-PATH! or GET-WORD!. That would give some flexibility, without needing to worry about participation "in the stream" of the evaluation beyond that single element. This might be a good tradeoff, since as mentioned, the "it's a literal block" doesn't actually guarantee you it's a branch anyway.

(Basically, this just means you'd not need to say COMPOSE in the example above... and you couldn't get literal GROUP!s or GET-WORD!s or GET-PATH!s, you'd have to put them in GROUP!s and quote them.)

This might even suggest an idea for branching in general for IFs, etc...to soft quote the branch slots, which might be safer overall while permitting most of the expressivity people would need.

With my decision to endorse SWITCH fallout, the idea of "CASE fallout" has come up:

>> case [
       1 > 2 [...]
       3 > 4 [...]
       10 + 20
   ]
== 30

One of the things that makes it more palatable is the change needed to support elide...basically that you will be "seeing" the branches in the form of blocks (or ACTION! literals, if you composed them in, but you probably knew what you were doing if you did that).

But then, there was this question:

This lets you say:

 block1: [...]
 case [
     1 > 2 (block1)
     3 > 4 (func [x] [...])
     10 + 20
 ]

You could do that with a COMPOSE (or a COMPOSEII). Which would be a slight performance cost, but arguably more clear...and we're trying not to stress out too much over performance at that level.

But the point is that it makes one a little less comfortable with the CASE fallout, because the blocks aren't as obvious in the structure. Of course, blocks can be arguments to the expressions in a case, so it's not a slam dunk anyway.

An argument against soft quoting would be based on what you would want CHOOSE to do with GET-PATH!, SET-PATH!, and group:

choose [
    1 > 2 [literal block]
    3 > 4 <tag>
    5 < 6 (10 + 20)
]

Should that be (10 + 20) the literal group, or a way of choosing 30? If you want it to be the literal group, that seems to strongly suggest the branches in a CASE should not be soft quoted. And I kind of think I like the literal GROUP! outcome for CHOOSE, it feels more "solid" and predictable to really let you pick anything out of that slot.

I think I want to say that you need to use literal blocks in CASE, and if you have a non-literal one that should be done with COMPOSE. The performance argument doesn't hold up under scrutiny, because if you just use a BLOCK! with CASE that is going to be nearly as fast (within the noise, compared to the COMPOSE which would actually cost slightly more).