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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With