Facing the facts: SWITCH must evaluate clauses

I know the audience for my findings is, a small and self-selected one. But still...

don't shoot the messenger:

x: 304
switch x [
   300 + 4 [
       print "if things worked right, this would print"
   ]
   ...
]

Of course 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"
     ]
     ...
 ]

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"]
]

I think it is becoming beyond question:

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

For the class of language that Rebol is, SWITCH should evaluate its clauses. It is a force multiplier, and the right answer.

Now comes the question: how much will this break, and how well can we be told what the breaks are?

My idea is a shim, that checks to see if SWITCH evaluates a value to a WORD! that does not look up to a datatype...and errors if so. This would be in effect for the indefinite future.

Perhaps we would have a switch/quote for the current behavior, aliased as switchq or something? I don't know.


I have been conflicted for a while, about the tradeoffs, but when I think about things long enough, I solve them. I'm now sure about this.


Looking at these seemingly "big" issues--which to be honest--aren't actually big (in terms of shaking language foundations) I think it's interesting to quote Carl 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.

So "we're sorry" but the right answers must emerge. And we need to have a way that your code can pin itself to a set of definitions, which I say we must make a hybrid Rebol2/R3-Alpha/Red dependency. If you say that's what you depend on, then SWITCH must use the old meaning. We need to be sure this legacy idea is easy to use and works for those who can't handle the truth. :slight_smile:

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 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 type of get 'path [
        null [print what-dir | return]
        file! [path] 
        string! [local-to-file path] 
        word! path! [to file! path]
    ]
]
1 Like

Well that looks a lot less cryptic than the original!

We're a bit downstream of Facing the facts: SWITCH must evaluate clauses. I think it has been a huge success.

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 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 NULL:

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

I noticed a remark from Carl 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.

So the non-evaluated case labels were something that was under scrutiny in the first place. We now have ample evidence that bringing in the evaluator is critical--and differentiating to bring in expressive power that other languages don't have, and being able to match the clarity they do have.

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

We're a couple years downstream of the SWITCH change to evaluative clauses. I no longer have qualms about it.

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...)"
]

Generic quoting also makes a shim for the Rebol2 behavior a piece of cake. Just add a quote level to everything that isn't a block!

switch: adapt :switch [
    body: map-each item body [either block? :item [item] [quote :item]]
]

(It would be nice to have some way of phrasing that which didn't need to say "item" twice. Like a version of MAP-EACH that assumed the original value if the body returned NULL. UPDATE-EACH?)

So I have no regrets at this point about the evaluativeness of the clauses. And at this point, my presumption is nobody else does either.

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