Should or-patterns support per-alternative guards?

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()

This works, but it duplicates the body.

1 Like

Somewhat relevant and similar, I wonder if this can be incorporated into that? (I’m still waiting on that one :crossed_fingers: )

1 Like

I found I needed this recently in a PR of pekko, otherwise I have to write two cases instead of one. I checked Rust and Kotlin too, none support this.

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:

5 Likes