One of the well known duals is coproduct, which is the dual of product. Prefixing with “co-” is the convention to name duals.
Here’s the definition of products again:
Definition 2.15. In any category C, a product diagram for the objects A and B consists of an object P and arrows p1 and p2
satisfying the following UMP:Given any diagram of the form
there exists a unique u: X => P, making the diagram
commute, that is, such that x1 = p1 u and x2 = p2 u.
Flip the arrows around, and we get a coproduct diagram:
Since coproducts are unique up to isomorphism, we can denote the coproduct as A + B, and [f, g] for the arrow u: A + B => X.
The “coprojections” i1: A => A + B and i2: B => A + B are usually called injections, even though they need not be “injective” in any sense.
Similar to the way products related to product type encoded as scala.Product, coproducts relate to the notion of sum type, or disjoint union type.
First way to encode A + B might be using sealed trait and case classes.
sealed trait XList[A]
object XList {
case class XNil[A]() extends XList[A]
case class XCons[A](head: A, rest: XList[A]) extends XList[A]
}
XList.XCons(1, XList.XNil[Int])
// res0: XList.XCons[Int] = XCons(head = 1, rest = XNil())
If we squint Either can be considered a union type. We can define a type alias called |: for Either as follows:
type |:[+A1, +A2] = Either[A1, A2]
Because Scala allows infix syntax for type constructors, we can write Either[String, Int] as String |: Int.
val x: String |: Int = Right(1)
// x: String |: Int = Right(value = 1)
Thus far I’ve only used normal Scala features only. Cats provides a typeclass called cats.Inject that represents injections i1: A => A + B and i2: B => A + B. You can use it to build up a coproduct without worrying about Left or Right.
import cats._, cats.data._, cats.syntax.all._
val a = Inject[String, String |: Int].inj("a")
// a: String |: Int = Left(value = "a")
val one = Inject[Int, String |: Int].inj(1)
// one: String |: Int = Right(value = 1)
To retrieve the value back you can call prj:
Inject[String, String |: Int].prj(a)
// res1: Option[String] = Some(value = "a")
Inject[String, String |: Int].prj(one)
// res2: Option[String] = None
We can also make it look nice by using apply and unapply:
lazy val StringInj = Inject[String, String |: Int]
lazy val IntInj = Inject[Int, String |: Int]
val b = StringInj("b")
// b: String |: Int = Left(value = "b")
val two = IntInj(2)
// two: String |: Int = Right(value = 2)
two match {
case StringInj(x) => x
case IntInj(x) => x.show + "!"
}
// res3: String = "2!"
The reason I put colon in |: is to make it right-associative. This matters when you expand to three types:
val three = Inject[Int, String |: Int |: Boolean].inj(3)
// three: String |: Int |: Boolean = Right(value = Left(value = 3))
The return type is String |: (Int |: Boolean).
An interesting read on this topic is Miles Sabin (@milessabin)’s Unboxed union types in Scala via the Curry-Howard isomorphism.
See also Coproducts and discriminated unions in Shapeless.
There’s a datatype in Cats called EitherK[F[_], G[_], A], which is an either on type constructor.
In Data types à la carte Wouter Swierstra (@wouterswierstra) describes how this could be used to solve the so-called Expression Problem.
That’s it for today.