DEFAULT now usable in CASE, SWITCH (!)

In what I think is an impressive design point, you can now write:

case [
     condition1 [...]
     condition2 [...]
     default [
         ...code if neither condition1 or condition2 were truthy...
     ]
]

...as well as...

switch thing [
    match1 [...]
    match2 [...]
    default [
        ...code if thing <> match1 and thing <> match2...
    ]
]

How it works

It's an interesting trick, that doesn't involve making DEFAULT a keyword that CASE or SWITCH recognize. And it isn't disruptive to the existing usage of DEFAULT for updating variables with no value, which looks like:

>> x: _
>> x: default [10 + 20]
>> print x
30

>> x: default ["already has value, won't take this one"]
>> print x
30

The mechanism actually hinges on the Fallout Feature of SWITCH, which was later added to CASE:

switch 10 [
    1 + 2 ["three"]
    3 + 4 ["seven"]
    uppercase copy "something else"
 ]

So that would evaluate to "SOMETHING ELSE". Each switch match is evaluative, and if it has an evaluation followed by no BLOCK! to run, it just drops the evaluation out.

Really, all DEFAULT is doing is taking advantage of this. While it looks like it's a condition and then a block of code to run on the match, the whole condition is default [...code...]. You could have also said (...code...) or do [...code...] and it would have worked. But DEFAULT cues the reader to know what you meant, which is important; and it also checks to make sure you don't put more stuff after it.

switch 10 [
    1 + 2 ["three"]
    3 + 4 ["seven"]
    do ["something else"] (print "this will run")
 ]

switch 10 [
    1 + 2 ["three"]
    3 + 4 ["seven"]
    default ["something else"] (print "this causes an error")
 ]

Supplement to ELSE

While this appears to do the same thing as an ELSE, it's actually different for a few reasons. A DEFAULT will be run even in CASE/ALL where no other cases matched, while ELSE only runs if one did:

case/all [
    1 < 2 [print "yep"]
    2 < 3 [print "yep"]
    default [print "this will run"]
]

case/all [
    1 < 2 [print "yep"]
    2 < 3 [print "yep"]
] else [
   print "this will not run"
]

It should also be noticed that indentation-wise, an ELSE statement's code would be outdented one level from the contents of a DEFAULT. So DEFAULT is most useful when you have a short default item that fits on one line, that you want to cleanly keep inside the CASE's block without introducing another outer block.

But the big mechanical reason one might favor DEFAULT is because ELSE forces completion of its left hand side. So you have to use a GROUP! (or <-) to prevent it from doing that with things like RETURN:

return (case [
    ...
] else [
   ...
])

; or

return <- case [
    ...
] else [
    ...
]

Otherwise, it would attempt to run the ELSE after (return case [...]) and that would be too late.

At the moment, this form of DEFAULT and the SWITCH/CASE fallout allow a NULL result. So you can make a DEFAULT case that signals NULL and use that to control a THEN and ELSE. I don't know if this is a good idea or not, but since ELSE itself allows a null return, it seems to at least be consistent.

Now-outdated forms

In the past people have done this:

case [
    condition1 [...]
    condition2 [...]
    true [...]
]

To carry the intent a little better, some people would use a REFINEMENT! there, which is also truthy--but can make it a little more legible:

case [
    condition1 [...]
    condition2 [...]
    /else [...]
]

Both of those will still work, but I'd suggest not doing either of them anymore.

Since those tricks wouldn't work for SWITCH, Rebol had historically offered a /DEFAULT refinement:

switch/default [
    match1 [...]
    match2 [...]
][
    ...code for no match...
]

With the introduction of ELSE and "switch fallout", that refinement was deprecated and removed from SWITCH. But now, DEFAULT offers another choice, and maybe it will appeal to some people.

In going over CASE's implementation, I've been looking at the desirable feature of "predicates", where you can give CASE a function to run. Without worrying about alternate forms for predicate-passing, let's assume you're just using a normal refinement just so the issues are treated separately:

case/predicate [
    1 < 2 [print "matched not 1 < 2"]
    3 > 4 [print "matched not 3 > 4"]
] :not

Here we are imagining a function used instead of the typical "truthy" test of each condition, to actually test for what is falsey. So it would print matched not 3 > 4

That raises a question of what the impact would be on DEFAULT... or "case fallout" in general. What should this do?

case/predicate [
    10 [print "no match"]
    20
] func [x] [
    print ["testing!"]
    return x > 15
]

How many times would you expect that to print "testing"? Once or twice? Should the "fallout" result be 20, or should it be "true"?

And what if your predicate could actually take more than one argument?

case/predicate [
    1 2 [print "1 greater than 2"]
    4 3 [print "4 greater than 3"]
] :greater?

That seems very useful, and there is no seemingly easy way to graft that in with DEFAULT or fallout here.

What's more important: DEFAULT or predicates?

It's neat that DEFAULT could have a quoted left parameter it looks for, and decays to acting like DO if it's not there... to give people a comfortable syntax trick that looked like defaulting in languages they were familiar with.

But when CASE is generalized, the hack shows its weakness. ELSE is a stronger and more uniform tool. AUGMENT can be used to throw a /DEFAULT refinement on for anyone who finds that to be compelling.

We may not need to "kill" the feature per-se, but I don't think we should cater to it, crippling CASE. And maybe not advocate it. It has been a good test of skippable quoted left arguments, and an interesting thought experiment. But ELSE is the stronger story. And DEFAULT is a cool feature in its main purpose, just for defaulting a SET-WORD! or SET-PATH! on its left if it doesn't already have a value.

How can we not "kill" it, and still use predicates?

So right now, the default predicate is DID. The problem is that if we run DID on a default branch, we won't get the value the branch gave us... but either #[true] or #[false].

This matches something like Rebol2 and R3-Alpha, which didn't error on a "stray" condition. But dropped out the value of the truthy or falseyness evaluation:

rebol2>> case [false [print "nope"] 10]
== true

rebol2>> case [false [print "nope"] false]
== none

That looks a bit like giving the output of the predicate (turned to none if falsey), and not the output of the value itself.

But if CASE's baseline predicate was IDENTITY, and then it acted on the truthiness or falseyness of that...this would approximate the same usefulness as the previous fallout. Using your own predicates would make this useless with the default hack:

>> case/predicate [
    true [print "nope"]
    default [100]
] func [x] [print "testing" | not x]
testing
testing
== #[true]

But at least DEFAULT would be compatible with plain CASE, still. (Assuming VOID! was tolerated in the fallout conditional situation...otherwise you couldn't have a DEFAULT branch do something like just PRINT.)

Like I say, though... this really suggests advising people not to do it this way. Building upon CASE's assurance that it will return NULL if-and-only-if no branches were run is the better choice. Do it with ELSE or IF NULL? or IF VALUE?... or augment with a /DEFAULT refinement if you feel thusly compelled.

I think you mean 4 < 3 in all three places.

Yes... :tired_face:

Revisiting CASE for a stackless rethinking is a pretty nuanced. It's funny that the question of "fallout" came from trying to bolt down the behavior early on. To this day, Red replicates Rebol2/R3-Alpha:

red>> case [false print "hi" true print "Bye"]
== true

red>> case [true print "hi" false print "Bye"]
hi

e.g. the false cases skip one value, interpreting like:

 case [false ['print] "hi" [true] print "Bye"]

The true cases run one expression, interpreting like:

 case [true [print "hi"] false ['print] "Bye"]

Getting it to cohere is harder than it might seem on the surface, especially when the role of invisibles comes into play. It's hard to be forced to review all the code when stackless is already such a big change. :-/ But I feel like there's at least a logical framework for figuring out what's good or bad, and the new CASE is actually looking pretty solid.

Now that they exist, I can say for sure... predicates.

In fact, someone just asked the following on Red Gitter:

is there any way to rewrite next code to switch?

if find "asdf" "a" [result: 'aaa]
if find "asdf" "b" [result: 'bbb ]
if find "asdf" "c" [result: 'ccc ]
; if not all [ .... ] [] ; some logic if not all are true

We should be able to easily beat their offered answers. Here's how we'd do it with predicates and CASE:

data: "asdf"
result: case .find.data [   ; more generically .(:some-func)
    "a" ['aaa]
    "b" ['bbb]
    "c" ['ccc]
]

Or if you prefer the cases could just be the plain QUOTED!s:

data: "asdf"
result: case/predicate [
    "a" 'aaa
    "b" 'bbb
    "c" 'ccc
] (<- find data)  ; another way to pass predicate argument

My plan is that if your predicate takes more than one argument, you'd be able to make that work too:

case .greater? [
    1 2 [print "This wouldn't print"]
    4 3 [print "This would print"]
]

Time to cut DEFAULT's Experimental Alter Ego

It was probably a bad idea to use the same name as the left-quoting "default value if not already a value" construct. But it tested some pretty interesting polymorphism...to have the DEFAULT do double duty.

Today I'd probably call it OTHERWISE to distinguish it. But when measuring this in the whole of the battles that need fighting, I say just kill it off completely. It doesn't work with predicates... but ELSE does.

If you want to use "true" for the branch, and adapt that as appropriate for your default with other predicates (false if your predicate is .NOT) then you can do that too.

2 Likes

To be clear, does this mean DEFAULT is no longer usable in CASE, SWITCH?

It still works (hasn't been disabled yet) but avoid using it.

There may be other answers. It might be that since it's evaluative we can support infix operations inside the case/switch.

 n: 2
 case [
     n = 1 [10]
     else [20]
 ]

For such things to work, CASE would have to become complicit in telling the infix operation there was a result for it to process (so the ELSE didn't see this as a "nothing to the left" circumstance").

This has the advantage that the ELSE doesn't have to know anything about the triggering condition of the case, so it would work just as well on negated situations:

 n: 1
 case .not [
     n = 1 [10]
     else [20]
 ]

But there'd be a lot of questions to answer. Anyway, fun to think about potential ideas here.

For now, avoid DEFAULT. I think the overloading with default's other purpose is bad anyway. Even if it worked we should probably call it OTHERWISE or something like that.