Search code examples
scala

Scala 3: Nonexhaustive match for Vector, but not for Seq


Using Scala 3.4.2:

Vector(1, 2, 3) match
  case Vector()  => println("empty")
  case _ :+ last => println(s"last: $last")

gives me an (in my opinion incorrect) nonexhaustiveness warning

[warn] -- [E029] Pattern Match Exhaustivity Warning: ...
[warn] 5 |  Vector(1, 2, 3) match
[warn]   |  ^^^^^^^^^^^^^^^
[warn]   |match may not be exhaustive.
[warn]   |
[warn]   |It would fail on pattern case: Vector(_, _*), Vector(_, _*), Vector(_, _*), Vector(_, _*), Vector(_, _*), Vector(_, _*)

If I use Seq instead of Vector, the code compiles fine.

It looks like this is the same thing for Scala 3 that has been discussed for Scala 2 in the bug https://github.com/scala/bug/issues/12240.

Also, it might relate to https://github.com/scala/bug/issues/12252 which is marked "fixed in Scala 3".

My questions are:

  1. Is this an actual bug in Scala 3?
  2. If yes, is it already known / documented somewhere, or should I create a new bug ticket?

Solution

    1. Since there is no Scala 3 specification released yet (AFAIK it is still in development), I'll use Scala 2.13 spec (2.13 spec + list of changes in Reference is the closest approximation of a Scala 3 specification that we currently have):

      If the selector of a pattern match is an instance of a sealed class, the compilation of pattern matching can emit warnings which diagnose that a given set of patterns is not exhaustive, i.e. that there is a possibility of a MatchError being raised at run-time.

      There is only a mention about sealed when mentioning exhaustive checks. For anything else, the behavior is not defined by the spec, so at best it's implementation-specific.

      Scala 2.13.4 release notes mentions -Xlint:strict-unsealed-patmat flag which may add more exhaustive checks.

      • There is a chance you have it enabled somewhere, e.g. if you do -Xlint with all options, or if you have something like sbt-tpolecat, which enables a lot of flags
    2. Vector is a sealed trait (on 2.13, and so on Scala 3 since it uses 2.13's standard library)

    3. Seq is just trait

    4. bugs you mentioned are about exhaustive checks of unapplySeq, so basically things like:

      Vector(1, 2, 3) match
        case Vector()  => println("empty")
        case Vector(multiple*) => println(s"last: ${multiple.last}")
      

      while +: was mentioned there as something that should be anylyzed by the compiler, it was more of a wish - it is NOT implemented and I expected it will never be because it's just an arbitrary

      object :+ {
        def unapply[A, CC[_] <: Seq[_], C <: SeqOps[A, CC, C]](t: C with SeqOps[A, CC, C]): Option[(A, C)] = ...
      }
      

      There is no way for the compiler to guess what this does internally and how it relates to other matches. Someone would have to adds a special case in the compiler... which could not be added for any other user-provided unapply so I don't expect it to happen.

    Putting it all together:

    • when you are using Seq compiler does not try to check for exhaustiveness by default (with some -X flag it might)
    • when you are using Vector it's a sealed so it will try to prove an exhaustive check
    • improvements to the compiler allow to prove exhaustive check when one uses unapplySeq (case Vector(...) =>)
    • neither +: nor :+ are Vector.unapplySeq so the improvements above does not concern them, and they are not exhausting any possibilities as far as the compiler is concerned.
    • what you observe most likely works as intended (after all there is no Scala 3 specification, so we cannot be certain without asking developers), it's unlikely a bug, even if it does not work as you'd hope it should.