I ran into this issue involving pattern matching Enumerations. It was quite puzzling at first and took me a few minutes to realize what went wrong. To demonstrate, let’s consider an example of building a generic set operation that can handle intersection and complement (i.e. subtraction): an Enumeration is defined to represent the set operator, and a function computes the result given an operator and a sequence of sets as input.
Here is the SetOp
enum:
object SetOp extends Enumeration {
type SetOp = Value
val intersect, complement = Value
}
The function requires exactly two sets for complement and two or more sets for intersection:
import SetOp._
def doSetOp(op: SetOp, sets: Seq[Set[Int]]) = (op, sets) match {
case (intersect, s) if s.size > 1 => s.reduce((s1, s2) => s1.intersect(s2))
case (complement, Seq(s1, s2)) => s1.filterNot(s2.contains)
case _ => throw new IllegalArgumentException("bad args")
}
To test it:
import SetOp._
val set1 = Set(1, 2, 3)
val set2 = Set(2, 3, 4, 5)
println(doSetOp(intersect, Seq(set1, set2)))
println(doSetOp(complement, Seq(set1, set2)))
One would expect the follwoing output:
However, try it in Scala REPL, and you will get this:
It’s as if complement
was matched to intersect
! … and indeed it was, but why?? Play with the code a bit and see what you can find…
The problem lies in the case
patterns: intersect
and complement
are intended to be constant patterns, i.e. the SetOp
values that argument op
would be matched to, but the way the code is written, Scala actually treats them as variable patterns, i.e. they are val
s bound to the value of op
, whatever it is. On top of that, had op
been the only value being matched and the alternatives were simply intersect
and complement
, like this:
def doSetOp(op: SetOp, sets: Seq[Set[Int]]) = op match {
case intersect => //...
case complement => //...
case _ => //...
}
you’d get a complaint from the compiler that the last two cases are unreachable code, but the way that sets
is included in the pattern matching makes all alternatives possible, so the compiler thinks it is all good! Once we understand what is going on here, the fix is simple: just use SetOp.intersect
and SetOp.complement
as the patterns:
def doSetOp(op: SetOp, sets: Seq[Set[Int]]) = (op, sets) match {
case (SetOp.intersect, s) if s.size > 1 => s.reduce((s1, s2) => s1.intersect(s2))
case (SetOp.complement, Seq(s1, s2)) => s1.filterNot(s2.contains)
case _ => throw new IllegalArgumentException("bad args")
}
Now everything works as expected.