Facing the facts: SWITCH must evaluate clauses


#1

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:


#2

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!


#3

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

#4

Well that looks a lot less cryptic than the original!


#5

SWITCH was made to help avoid repetition. So if you have a structure like:

switch some-int [
    1 [print "code for 1 or 2"]
    2 [print "code for 1 or 2"]
]

You are given a shorthand:

switch some-int [
    1 2 [print "code for 1 or 2"]
]

This isn’t too surprising an ability (C switch statements can do the same). But C does it limited to only constant integers, while Rebol’s SWITCH might want to switch on any type.

If you could always trust that switch alternated matches and branches, you could use blocks.

switch some-block [
    [1] [print "code for [1] or [2]"] ;-- could work, at least in theory
    [2] [print "code for [1] or [2]"] ;-- if all clauses came in pairs
 ]

But the freeform nature mucks this up:

switch some-block [
    [1] [2] [print "code for [1] or [2]"]
 ]

The system can’t tell when such blocks should be run as code or if they are being matched.

Introducing another concoction

My current leaning is to have something for switch that is CONCOCT-like, where you tell it what the pattern is that your branches have. Then, historical SWITCH is just a specialization which says your branches look like plain [].

Let’s again give it a weird name we know we’re not going to use, just for the sake of having a way to refer to it in conversation:

swoosh [**] some-block [
    [1] [2] [** print "code for [1] or [2]" **]
]

Pretty powerful. I kind of feel like CASE statements–even though they’re always paired–might benefit from this too. If you’re using blocks in your conditions in the case, might you want something more distinctive for the branches to help with readability?

Impacts on evaluative clauses

Having such options makes me feel that it’s o.k. to allow evaluations to use blocks, even if it gives ambiguous looking things like:

switch some-int [
    first [1] [print "code for 1"]
    first [2] [print "code for 2"]
]

If we tried a rule like “no function arguments can contain a pattern that also matches a branch”, that introduces a performance hit. And it’s never been true for CASE statements, e.g. this works fine:

case [
    some-int = first [1] [print "code for 1"]
    some-int = first [2] [print "code for 2"]
]

And really…is it any more “risky” or “dangerous” than the state of programming pretty much any line of Rebol code, anywhere?

The answer that has emerged time and again is not to throw in artificial restrictions, but to expand the toolset so those who are bothered by it can do their own thing. SWOOSH-like things seem the better answer.


#6

I think I’d like to see less verbose options as a way of doing this.


#7

You have to consider specializations. What’s so verbose about switchII: specialize 'swoosh [pattern: [[]]] being in lib, and then you have:

switchII some-block [
    [1] [2] [[print "code for [1] or [2]"]]
]

The point is the mechanism.


TEXT! vs. STRING!
#8

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 DEFAULT to be non-literal

For purposes of readability, the DEFAULT construct is being tricky and evaluates as a kind of synonym for DO. You can write:

switch thing [
     'foo [...]
     'bar [...]
     default [...]
 ]

It’s taking advantage of SWITCH’s 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™)

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.


#9

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