I often find myself wanting to write something like this:
enum Event:
case Click
case Hover
case KeyPress(code: Int)
def handle(e: Event) = e match
case Click | Hover | k @ KeyPress(code) if code > 100 => doSomething()
case _ => doDefault()
The intent is clear: Click and Hover match unconditionally, while KeyPress only matches when code > 100. But Scala rejects this ā guards apply to the entire or-pattern, not individual alternatives.
The workaround
The standard approach is to split into separate cases:
case Click | Hover => doSomething()
case k @ KeyPress(code) if code > 100 => doSomething()
case _ => doDefault()
I have some issues with this proposal (but thank you for proposing it nonetheless!)
Iām not sure I agree, I find it surprising we test something which is not always defined, some examples which make it worse:
// alternatives can be reordered,
// this can break the visual link between declaration and usage
case k @ KeyPress(code) | Click | Hover if code > 100 => doSomething()
val code = 99
// what is the default behavior, using the `code` in scope, or ignoring the condition ?
case Click | Hover | KeyPress(code) if code > 100 => doSomething()
// how do we extend it to multiple captures ?
// I would expect one of the two following,
// but I wouldn't be able to tell you which from intuition
case MouseClick(click) | KeyPress(code) if click == "left" && code > 100 => doSomething()
case MouseClick(click) | KeyPress(code) if click == "left" || code > 100 => doSomething()
This proposal is also in tension with Bind variables for alternative patterns (also linked by @spamegg1):
That proposal forces all alternatives to share the same variables, whereas this one does not.
I think if we want to do something like that, we should use parentheses to group the condition with the pattern:
case Click | Hover | (KeyPress(code) if code > 100) => doSomething()
This fixes my above issues:
// reordering doesn't break the link:
case (KeyPress(code) if code > 100) | Click | Hover => doSomething()
// scoping is clear
val code = 99
// to ignore the one in scope:
case Click | Hover | (KeyPress(code) if code > 100) => doSomething()
// to use the one in scope:
case (Click | Hover if code > 100) | (KeyPress(code) if code > 100) => doSomething()
// we forbid the following to avoid ambiguity:
case (Click | Hover | KeyPress(code)) if code > 100 => doSomething()
case Click | Hover | KeyPress(code) if code > 100 => doSomething()
// multiple captures
case (MouseClick(click) if click == "left") | (KeyPress(code) code > 100) => doSomething()
// cleanly combines with "Bind variables for alternative patterns":
case (A(x, y) | B(y, x) if y == "yes") | (C(x, z) | D(x, z) if z == 1) => x //extracted
// all alternatives in the same parenthesized group *must* capture the same variables
Overall, this feels very noisy, so I feel like the cleaner approach remains: