MATCH in Rust vs. SWITCH

We've discussed how SWITCH should deal with the "rhythm" of things matched vs. branches to take.

I was programming in Rust a bit, and wanted to point out their MATCH...which is like SWITCH, where it uses | how comma has been proposed:

match number {
    1 => println!("One!"),
    2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
    13..=19 => println!("A teen"),
    _ => println!("Ain't special"),
}

Compared to the proposal:

switch number [
    1 [print "One!"]
    2, 3, 5, 7, 11 [print "This is a prime"]
    ; no equivalent yet proposed for ranges
] else [
    print "Ain't special"
]

It's worth thinking about how ranges and range testing are supposed to be expressed.

But the bigger point is that I think I like the comma for meaning a list of alternates in this context...enough to say that SWITCH should require it if you want multiple conditions for the same branch. This would solve asymmetries in CASE and SWITCH and let them share more code.

4 Likes

Something that has occurred to me is that SWITCH feels a bit weak when it comes to wanting to run a function besides equality on something.

Which gave me a thought. Let's imagine at least some form of SWITCH required you to put in a => between the thing you're matching and the clause:

 x: 10

 switch/all x [
     match integer! => [print "It's an integer!"]
     "bobcat" <> => [print "It's not a bobcat"]
     <something> => [print "Basic matches would still work"]
 ]

The idea here would be that if the left hand side of the => has too few arguments when it hits the =>, then the missing argument is supplied as the thing that you're SWITCH-ing on.

This seems so much more powerful than traditional SWITCH that I am seriously tempted to make this SWITCH, and then make what we've been calling SWITCH be "CHOOSE".

An Agnostic SWITCH That Engages Dialecting

This was inspired by thinking about the issue of range-checking, and going... "uh, does SWITCH really want to get involved in that? Like, seeing 1..10 as a TUPLE! and interpreting that?"

This way you can have a general function that is dialected to do that whenever you need it. Imagine it is called IN-RANGE. And you could use it anywhere:

>> value: 5

>> in-range 1..10 value
== true

Then... if SWITCH supplies expressions an argument if one is missing, you can make a switch clause just in-range 1..10. Omit the value, but switch supplies it.

switch value [
    ...
    in-range 1..10 => [<whatever>]
    ...
]

Now you haven't rigged SWITCH to be a bizarre dialect, but you get all the benefit.

What is Seen Cannot Be Unseen :eyes:

Today's simpleminded switch should be CHOOSE.

CHOOSE is a suckier word for a suckier--but still useful and brief--idea.

2 Likes

So I decided I actually think I like vertical bar for this:

switch number [
    1 => [print "One!"]
    2 | 3 | 5 | 7 | 11 => [print "This is a prime"]
    in-range 13..19 => [print "A teen"]
    print "Ain't special"  ; leveraging "fallout"...last expression as default
]

Even more rustlike. It has parity with PARSE expressing "alternates", and it leaves comma available for separating clauses, especially important in CASE/CHOOSE when you're putting things on one line.

>> choose (10 / 2) [1 | 2 [print "small"], 3 | 4 | 5 '<big>]
== <big>

Actually... Commas might be good for AND'ing vs. OR'ing

switch number [
    > 20, even? [print "Greater than 20 -and- even"]
    < 20 | odd? [print "Less than 20 -or- odd"]
]

Hard to think of another symbol for that. It puts the AND clauses in sequence--kind of like a parse rule--where all would have to apply.

3 Likes

Because I want to really be throwing Ren-C up against non-trivial examples, I thought I'd give this a shot.

My first draft actually seemed to work pretty well!

switch2.control.test.reb

It uses the fledgling ability to turn a block of code into a FRAME!. It needs some work, but it's reasonably all right:

>> parameters of :match
== [test value]

>> make frame! make varargs! [match integer!]
== make frame! [
    return: '
    test: #[datatype! integer!]
    value: ~end~
]

If you notice something interesting there, when you tried to MAKE FRAME! on an "incomplete sentence" it didn't give you an error in the moment. Instead, it gave you a frame with an ~end~ isotope in the missing slot.

The trick--you may remember--is that frames can't actually take isotopes as arguments. So had there been an actual literal ~end~ there it would be an error condition...it would have to be a ^META parameter, in which case it would show up in the argument as a quoted ~end~ BAD-WORD!.

In any case, what my primitive test does is just look to see if there's an ~end~ isotope in a plain parameter slot, and fills the first one it sees with the switch value.

It's Probably Too Sketchy to Use This Way...

Looking at it realistically, I think it's probably bad to be making such assumptions. It's probably better to have some sort of explicit marker, like a BLANK! or otherwise to show your intent:

switch/all (...some-long-expression...) [
    match integer! _ => [print "It's an integer!"]
    "bobcat" <> _ => [print "It's not a bobcat"]
    <something> => [print "Basic matches would still work"]
]

It seems especially important if there's a fallthrough to plain equality matching.

But still--the FRAME! tricks are pretty good. And I want to keep going after these demos...trying to clean up the rough edges until they really read right.

This example gave me some insights into UPARSE. I decided that the new ANY [...] is just too clear and good to need an @ on the block (like ANY @[...]), just to try and warn you that the block wasn't going to be interpreted in the normal ways by the BLOCK! combinator. That's Rebol's m.o....you're always in new contexts and the rules change. It's comprehensible, so no reason to make it look bad!

<sigh>

Things evolved to where "stable isotopes" are legal as function arguments...and WORD! isotopes are stable. If they weren't, you'd have to use ^META arguments for ~null~, ~true~, and ~false~ isotopes (among other things, like splices)...which would create pain. Things like equality operators (for instance) would have to take their arguments as meta.

The weird "edge" that remains is simply isotopic void, a.k.a. "trash" or the ~ isotope. Despite being stable (e.g. storable in variables) it signifies an unfulfilled parameter. But unfulfilled parameters in FRAME! become null upon execution.

The weirdness is that you can't say if var = ~ [...] because the equality operator does not take meta parameters, hence it cannot receive a trash as an actual argument. To see if a variable is unset, you have to write if unset? 'var [...] or if trash? :var [...] and it reaches its conclusion through other mechanisms... or you can do a meta-comparison against the quasiform and take it one level up:

if ^var = '~ [...]  ; quoted tilde evaluates to quasi void, not isotopic void

Can End be Implemented With another "Weird Edge"?

At first glance, I'm not particularly thrilled with special treatment for ordinary ~end~ when other word isotopes are legal in frames, but then again... they've shaped up to where ~null~ and ~true~ and ~false~ have baked in behavior. (It's made me wary of assigning a truthy or falsey status to any other isotopic words).

We could effectively say all word isotopes are reserved for system use, and let people who want weird error-triggering use stable isotopic tags or something of that sort instead:

>> widget: ~<main not called yet>~

>> widget
** Error: widget is isotopic tag: ~<main not called yet>~

~true~ and ~false~ and ~null~ could sneak by with non-meta forms, and the likes of ~end~ could be used in the meaning as above:

>> make frame! make varargs! [match integer!]
== make frame! [
    test: &(integer)
    value: ~end~
]

You'd get a frame that you can't actually invoke unless you turn those ~end~ into something else. Just as with none (isotopic ~) you would have the blind spot of functions needing meta-parameters to handle end, with workarounds like doing a meta-access of variables you wanted to compare:

if ^frame.value = '~end~ [...]

Or a specialized function which is tailored to do the test. Presuming ~end~ is still "get-friendly":

if end? frame.value [...]

So END? would have to take a ^META argument... and it should not be endable...unless you want:

>> (end?)
== ~true~  ; isotope... but you don't want this, you want an error!

All other word isotopes would be illegal in FRAME! un-meta'd...but only at the moment of execution. They'd be reserved for future use and meaning by the system.

If we're willing to go down this route, it may open up some interesting possibilities. Like ~trash~ isotopes could intentionally transform into trash arguments without having to make them meta:

 unset: specialize :set [value: ~trash~]

If it were understood that "you shouldn't really trust word isotopes unless you meta them", you'd have a rigorous path (use a ^META parameter) as well as the path of convenience.

Hmmm. :thinking: