Can I use pattern matching with shapeless coproducts?
import shapeless.{CNil, :+:}
type ListOrString = List[Int] :+: String :+: CNil
def f(a: ListOrString): Int = a match {
case 0 :: second :: Nil => second
case first :: Nil => first
case Nil => -1
case string: String => string.toInt
}
That of course doesn't work, since a
is boxed as a Coproduct
.
Is there an alternative way to use coproducts and maintain the ability to pattern match?
You can use the Inl
and Inr
constructors in the pattern match:
import shapeless.{ CNil, Inl, Inr, :+: }
type ListOrString = List[Int] :+: String :+: CNil
def f(a: ListOrString): Int = a match {
case Inl(0 :: second :: Nil) => second
case Inl(first :: Nil) => first
case Inl(Nil) => -1
case Inr(Inl(string)) => string.toInt
}
This approach isn't ideal because you have to handle the CNil
case if you want the compiler to be able to tell that the match is exhaustive—we know that it's not possible for that case to match, but the compiler doesn't, so we have to do something like this:
def f(a: ListOrString): Int = a match {
case Inl(0 :: second :: Nil) => second
case Inl(first :: Nil) => first
case Inl(Nil) => -1
case Inl(other) => other.sum
case Inr(Inl(string)) => string.toInt
case Inr(Inr(_)) => sys.error("Impossible")
}
I also personally just find navigating to the appropriate positions in the coproduct with Inr
and Inl
a little counterintuitive.
In general it's better to fold over the coproduct with a polymorphic function value:
object losToInt extends shapeless.Poly1 {
implicit val atList: Case.Aux[List[Int], Int] = at {
case 0 :: second :: Nil => second
case first :: Nil => first
case Nil => -1
case other => other.sum
}
implicit val atString: Case.Aux[String, Int] = at(_.toInt)
}
def f(a: ListOrString): Int = a.fold(losToInt)
Now the compiler will verify exhaustivity without you having to handle impossible cases.
I just submitted Shapeless a pull request here that may work well for your needs. (Note that it is just a pull request and may undergo revisions or be rejected...but feel free to take the machinery and use it in your own code if you find it useful.)
From the commit message:
[...] a Coproduct c of type Int :+: String :+: Boolean :+: CNil could
be folded into a Double as follows:
val result = c.foldCases[Double]
.atCase(i => math.sqrt(i))
.atCase(s => s.length.toDouble)
.atCase(b => if (b) 100.0 else -1.0)
This provides some benefits over existing methods for folding over
Coproducts. Unlike the Folder type class, this one does not require a
polymorphic function with a stable identifier, so the syntax is
somewhat lightweight and better suited to situations where the folding
function is not reused (e.g., parser combinator libraries).
Additionally, unlike directly folding over a Coproduct with pattern
matching over Inl and Inr injectors, this type class guarantees that
the resulting fold is exhaustive. It is also possible to partially
fold a Coproduct (as long as cases are handled in the order specified
by the Coproduct type signature), which makes it possible to
incrementally fold a Coproduct.
For your example, you could do this:
def f(a: ListOrString): Int = a.foldCases[Int]
.atCase(list => list match {
case 0 :: second :: Nil => second
case first :: Nil => first
case Nil => -1
case other => other.sum
})
.atCase(s => s.toInt)