Why does SWITCH Evaluate Clauses? (Rebol2/Red Do Not)

In Ren-C, we evaluate expressions in SWITCH:

x: 304
switch x [
   300 + 4 [
       print "this prints in Ren-C"
   ]
   ...
]

That is not the case in Rebol2/etc. You get instead the likes of:

 switch '+ [
     300 + 4 [
         print "this will print in R3-Alpha/Rebol2/Red, and it is misguided"
     ]
     ...
 ]

In My View, the Choice Should Be Obvious

For the class of language that Rebol is, SWITCH should evaluate its clauses. Being able to use the evaluator is a force multiplier.

 x: 10
 switch type of x [
     integer! [print "Don't you think this should run?"]
 ]

Mechanically speaking, it is possible for SWITCH to discern a BLOCK! it evaluated, from one that is referenced directly in the body.

not-literal-wont-print: [print "subtlety"]
switch [print "this need not print"] [
    ([print "subtlety"]) not-literal-wont-print
    ([print "subtlety"]) [print "...but this *will* print"]
]

Carl Believed Rebol2 Should Be Challenged

Carl questioned the issue when asking if SWITCH should be made a native:

The switch function is used quite frequently in programs, but it presents challenges to new users, due to it's non-evaluated case labels.

He also said, circa 2014:

Having taken a long break from Rebol development, I guess these days I'm more in favor of disruption and going down a better path for new users. As I've said many times, Rebol 3 is still in alpha. That means we get to fix stuff and make it better. Sorry if it's a bit disruptive. There are always a few bumps in the development road, but you can['t (sic)] be afraid to keep moving forward.

You Can Of Course Write A Compatibility Version

Here's a bit of emulation from the "Redbol" module (that adds /DEFAULT back in too!)

switch: emulate [
    enclose (augment :switch [
        /default "Default case if no others are found"
            [block!]
    ]) lambda [f [frame!]] [
        f.cases: map-each c f.cases [
            match block! c else [quote c]  ; suppress eval on non-blocks
        ]
        let def: f.default  ; the DO expires frame right now (for safety)
        (eval f else (def)) else [@none]
    ]
]

So this code is from the rebolbook

cd: func [
    "Change directory (shell shortcut function)." 
    [catch] 
    'path "Accepts %file, :variables and just words (as dirs)"
        [file! word! path! unset! string! paren!] 
][
    if paren? get/any 'path [set/any 'path do path] 
    switch/default type?/word get/any 'path [
        unset! [print what-dir] 
        file! [change-dir path] 
        string! [change-dir to-rebol-file path] 
        word! path! [change-dir to-file path]
    ][
        throw-error 'script 'expect-arg reduce ['cd 'path type? get/any 'path]]
    ]

I hope it's going to look a lot better now!

I hope it’s going to look a lot better now!

That function is old, it manually "soft-quotes". if paren? get/any 'path [set/any 'path do path] is an unnecessary line--by having the parameter being 'path it gets what the intended behavior was--actually soft quoting is richer than that (supports GET-PATH! and GET-WORD! for instance, not just GROUP!/"paren!"). So axe that first line...being able to do so is supported in Ren-C, and also both R3-Alpha and Red.

Further, since the system is doing the soft quoting, you don't have to do your own type checking on reduced products. Meaning the THROW-ERROR is superfluous, you only get the types in the spec. Again, that's in R3-Alpha and Red too.

But other than that, since you're using datatypes here, evaluative is fine. The TYPE OF a NULL gives an error, but TRY TYPE OF NULL is NULL.

cd: func [
    "Change directory (shell shortcut function)"
    return: [<opt> file!] 
    @path "Accepts %file, :variables and just words (as dirs)"
        [<end> file! word! path! string!] 
][
    change-dir switch try type of get $path [
        null [print what-dir, return]
        file! [path] 
        string! [local-to-file path] 
        word! path! [to file! path]
    ]
]

(Modern binding notes: @path required to get a binding of the literal parameter ('path would not be bound). By a similar token, get $path is needed instead of get 'path because 'path would create an unbound word.)

1 Like

Well that looks a lot less cryptic than the original!

Beyond the anticipated benefits, there have been several cool surprises:

You can switch on NULLs

There's no such thing as a null literal. Nulls are antiforms and can't be put in blocks. You have to use evaluation to get them, but since they can appear you might want to switch on them.

e.g. the TYPE OF a NULL is an error, but TRY TYPE OF NULL is NULL:

 switch try type of get word [
     null [...]
     ...
 ]

Allows for Fallout

SWITCH has a newfound ability to just let whatever comes last "fall out" of the switch, which has turned out to be a great trick. So technically you don't need it there, you could just use a GROUP!:

switch thing [
     'foo [...]
     'bar [...]
     (...)
 ]

This works because only literal blocks are considered as switch branches, so if your code evaluates to a BLOCK! then it's compared normally:

 switch [a b c] [
     ([a b c]) [print "This will run"]
 ]

You can FAIL without invoking a defaulting pattern

It's easier than ever to have a failure message if a switch case doesn't match. No /DEFAULT or DEFAULT or ELSE clause needed:

switch thing [
     'foo [...]
     'bar [...]
     fail ["Thing must be foo or bar, not" thing]
 ]

(Note: This pattern also works in CASE--but it would have always worked there, and you can use it in R3-Alpha or Red too, but with do make error! instead of FAIL(tm))

All in all, quite the win.

2 Likes

This is full of win. Thank you for the switching on NULL -- that's going to be incredibly helpful, I expect.

1 Like

Now that there's generic quoting, we have nice things like being able to switch on literal blocks or groups:

minus: [a - b]
code: [e * f]
switch code [
    minus [print "Subtracting A and B"]
    (elide print "Wasn't the minus case... it's something else")
    '[c + d] [print Adding C and D"]
    (reverse copy [f * e]) [print "Multiplying E and F"]
]
then item -> [
    print ["A branch ran and gave" mold item]
    print "ELIDE, THEN/ELSE, NULL isotopes, and lambdas...-wow-"
    print "(Don't forget predicates...)"
]

But... Multiple Clauses Means No Soft Quoted Branches

Rebol's SWITCH took a cue from C's switch in letting you have multiple matches for a single clause

switch value [
    1 2 3 [print "some cases"]
    4 5 6 [print "some more cases"]
]

This means that SWITCH literally has to look for blocks at each step. Note that in the current logic, blocks that are part of expressions are not candidates...only blocks that are the start of new expressions:

switch value [
   first [a b c] [print "This runs if value = a"]
]

That's a little confusing, but CASE has the same character.

In any event, all this adds up to mean you can't do an abbreviated form of switch with quoted branches, because it thinks all of these are things to match:

>> switch 2 [
    1 'a
    2 'b
    3 'c
]
== 2   (follows "fallout" rule since no block, but imagine I wanted b)

Should We Have An Operator That Presumes Alternation?

I've been wondering if we need a variant of SWITCH that is still evaluative, but assumes a precise alternation of values to match and what's picked.

When I've imagined it, I've called it CHOOSE:

>> choose 2 [
   1 [print "This would print if 1"]
   2 'b
   1 + 2 '[literal block if 3]
]
== b

But usually when messing with CHOOSE scenarios, I don't need branches to run. So it's like it could be a more static thing, like SELECT...possibly using a COMPOSE:

>> select 2 compose [
   1 a
   2 b
   (1 + 2) [literal block if 3]
]
== b

This runs up against some questions we've had about the nature of SELECT, where @rgchris has wanted it to treat the data as a table and only consider odd slots for the keys...while I've been wondering about something more freeform that treats SET-WORD! (and SET-BLOCK!?) as keys.

Either way, a SELECT wouldn't have the attribute of running clauses on a match. How important is it to have an alternating form that does?

It Comes Up When Using The API

The cases where it does are usually not evaluative, most of them are just transformations from word-to-integer or something like that. Right now that's laborious and forces you to make BLOCK!s...more typing and more cost:

rebUnboxInteger("switch", rebQ(word), "[",
    "'some-word [", rebI(SOME_C_CONSTANT), "]",
   ...

I use SELECT, but with the current worries that a value slot might be treated as a key:

rebUnboxInteger("select just", word, "[",
    "'some-word", rebI(SOME_C_CONSTANT),
    ...

There's a lot of degrees of freedom here, but I just wanted to point out how the CHOOSE described is different from a SELECT, and wonder a bit more about the nature of SWITCH...

3 Likes

I don't know if this is mentioned or a good idea, but evaluative SWITCH can also be used as an alternative to case:

switch true [
    expression-1 [...]
    expression-2 expression-3 [...]
]

It does have the marginal advantage of dispensing with ANY in such CASE statements:

case [
    expression-1 [...]
    any [
        expression-2
        expression-3
    ] [...]
]

Benefits from expression barrier, be it comma or other:

switch true [
    expression-1 [...]
    expression-2,
    expression-3 [...]
]

Again, not saying this is a good or bad pattern, just that it is now possible.

3 Likes

So on this note: what if we presume values and branches alternate, unless COMMA! is used...in which case each element of the comma clause is assumed to be a value to match?

That would avoid the explicit search for BLOCK!, allowing QUOTED! and GROUP! branches:

>> x: 5
>> switch x [
   1 [2 3]
   4, 5 '[6]
]
== [6]

You'd be able to switch on blocks:

>> x: [a b]
>> switch x [
    [a b] [print "first match!"]
 ]
first match!

>> x: [a b]
>> switch x [
    [a b], [c d] [print "first match!"]
 ]
first match!

>> x: [a b]
>> switch x [
    [a b], [c d] '[print "first match!"]
 ]
== [print "first match!"]

You'd still be able to match commas, though we might have to make it legal to say ',, ... for now it works if you space it out:

>> x: ',
>> switch x [
   1, ', , 2 [print "match!"]
]
match!

Weird, but, if it comes up you could express it. For those concerned with legibility, there's other ways since it's an evaluative slot:

switch x [
    1, first [,], 2 [print "match!"]
]

This seems like it might be a good compromise, and normal cases should look pretty good:

switch type of x [
    integer!, block! [
        print "It was an integer or a block"
    ]
 ]

The same feature could be implemented for CASE. Since commas cannot appear internally to single expressions, the only place they could have appeared would have been right before the branch anyway.

case [
     x > 5, x < 20 [
         print "This would be kind of cool, eh?"
     ]
     y > 10,
     y < 1000 [
        print "Looks good with multiple lines too."
    ]
]

But in that situation you could have done it before with an ANY . Still, it looks pretty nice.

2 Likes

A post was split to a new topic: MATCH in Rust vs. SWITCH