Natively support pattern matching on integer ranges

Hi !
I’m unsure whether this has been discussed before.
Here’s a proposal to support pattern matching on number ranges, such as for example:


1 match {
  case (Integer.MinValue until 0) => "negative" 
  case (0 to Integer.MaxValue) => "positive"
}

Along with exhaustivity warnings, of course.
Perhaps one would include the possibility to skip specifying limiting bounds (“0 and upwards” instead of “0 to MaxValue”).

The idea was first formulated by @reibitto on Twitter.

5 Likes

This is already doable by using pattern guards, but it feels unnecessarily bloated, hence the proposal.

1 Like

More specifically what kicked off the conversation was how Rust pattern matches work:

let x = match n {
    i32::MIN..=-1 => false,
    1..=i32::MAX => false,
    0 => true
};

Rust can check both unreachable/redundant patterns as well as non-exhaustive ones at compile-time when written this way. I thought it would be nice if Scala could do the same (when not using arbitrary guards using if).

I don’t know what the syntax would look like but I guess reusing until / to would be preferable than having whole new syntax for it?

3 Likes

Clearly, the syntax presented above is problematic because (Integer.MinValue until 0) and (0 to Integer.MaxValue) already have defined semantics, as IndexedSeq.

Reusing the keywords would certainly be nice, though.

What about case in (a to b) instead of case (a to b) ? I don’t think this syntax can break existing valid code ?

This is somewhat doable with Scala 3

3 match {
  case InRange[Int.MinValue.type, 0](v) => "negative"
  case InRange[1, Int.MaxValue.type](v) => "positive"
  case 0 => "zero"
}

where:

class InRange[S <: Int, E <: Int](
  implicit s: ValueOf[S], e: ValueOf[E]
) {
  def unapply(v: Int): Option[Int] =
    Some(v).filter(v => v >= s.value && v < e.value)
}

I’m not sure if this works by design or by accident.

9 Likes

I like the idea, working with literal types always amazes me haha !
However, I dont think this addresses the issue of match exhaustiveness, for example running the code

object App:
  class InRange[S <: Int, E <: Int](
    implicit s: ValueOf[S], e: ValueOf[E]
  ) {
    def unapply(v: Int): Option[Int] =
      Some(v).filter(v => v >= s.value && v < e.value)
  }

  def x = 0 match {
    case InRange[Int.MinValue.type, 0](v) => "negative"
    case InRange[1, Int.MaxValue.type](v) => "positive"
  }

  @main def main() = println(x)
end App

will just explode at runtime, because it couldn’t tell that the match wasn’t exhaustive.
In a sense, this is not doing much better than what we can do with pattern guards.

1 Like

I wonder if sealed types could help here. It looks promising but I haven’t tried to put both ideas together yet.

4 Likes

Pattern matching of integer ranges would seem like the 101 use case for sealed types indeed, very interesting!
Throwing the SIP proposal in here just in case.