You may (or may not) remember that we occasionally mentioned co-products when we spoke about algebraic data types. We defined them as the natural way of expressing type union(Wine = White U Red U Espumoso
). In this post we’ll try to define new enumerations expressed as co-products, for example, weekdays….
Intro: product vs. co-product
Just to sum up and to better understand the example, imagine you have a triplet of certain type elements:
(Int, String, Double)
If the data that stands for a student may be expressed as a triplet of her age AND
name AND
average score, we could say that a student may be expressed as the product of these three types:
case class Student( age: Int, name: String, avg: Double)
(Surprisingly case class
es extend from something called scala.Product
, ehem …)
On the other hand, if we’re told that the result of some web request may return the number of error rows OR
the value we wanted to retrieve OR
the amount of seconds to wait until next request try, we would be talking about a co-product.
Intro: helpless co-products in Scala
In Scala, defining an enumeration as a co-product can be as easy as:
sealed trait Color case object Blue extends Color case object Red extends Color //...
But, what happens when the co-product former types are not related between them (Int
, Animal
, …)?
Well, we could use Either
type:
type Or[A, B] = Either[A, B] val v1: Int Or String = Left(2) val v2: Int Or String = Right("hi")
Easy, right? But, what happens now if we want to point that the value could be an Int
or String
or Boolean
? Things become more complicated:
type Or[A, B] = Either[A, B] val v1: Or[String, Or[Int, Boolean]] = Left("hi") val v2: Or[String, Or[Int, Boolean]] = Right(Left(2)) val v2: Or[String, Or[Int, Boolean]] = Right(Right(true))
iota co-products
One of the alternatives for avoiding this headache is iota, a library developed by 47deg for defining co-products in an awesome way.
It both supports scalaz and cats. In this post, we’ll see how it’s applicable to cats (because reasons…)
library-dependant
In order to start compiling this, we’ll start by adding some cats-core
and iota-core
dependencies to our project:
scalacOptions += "-Ypartial-unification" // Needed if you're using Scala 2.11+ libraryDependencies ++= Seq( "org.typelevel" %% "cats-core" % "1.0.1", "io.frees" %% "iota-core" % "0.3.4" )
Start point
Let’s suppose we have to define an enumeration.
One of the most common ways to achieve it in Scala, as we mentioned somewhere before, would be with:
sealed trait Weekday case object Monday extends Weekday case object Tuesday extends Weekday case object Wednesday extends Weekday case object Thursday extends Weekday case object Friday extends Weekday case object Saturday extends Weekday case object Sunday extends Weekday
Nevertheless, let’s skip some extra days (for making it simpler) and let’s remove the inheritance relationship (for making it more interesting):
case object Monday case object Tuesday case object Wednesday case object GroundhogDay //Just for messing things up...
For expressing now the type union ( Weekday = Monday U Tuesday U Wednesday
) we define with iota the co-product this way:
// Let's import some iota core stuff (including syntax) import iota._ import iota.syntax.all._ // ...and also import TList (which it's something really similar to a Shapeless HList) // it keeps info about the contained element types. import TList.:: type Weekday = Cop[Monday.type :: Tuesday.type :: Wednesday.type :: TNil]
And after these wonderful mayan glyphs, how do we use the Weekday
type?
Because the following definitively doesn’t work:
val day: Weekday = Monday //NOPE! It doesn't fit
For being able to use it, we’ll have to check how to inject Monday.type
into our co-product.
If your very first reaction to the previous sentence was ‘eing?’, take a look at the following section about injection and projection concepts. Otherwise, jump onto page 30 for killing the troll skip next section.
Inject & Project
Without giving a full-detail view about injective and projective modules, let’s see an easy example for better understanding these concepts.
Let’s imagine I have defined the weekdays co-product as a case class
with all possible values as Option
s so only one of these can be fulfilled:
// Crazy stuff BTW, // defining a co-product as a product of Option's case class Weekday( monday: Option[Monday.type] = None, tuesday: Option[Tuesday.type] = None, wednesday: Option[Wednesday.type] = None){ require({ val defined = List(monday, tuesday, wednesday) .filter(_.isDefined).size defined <= 1 }, "A Weekday can only be one of the options") } object Weekday { def inject(day: Monday.type): Weekday = Weekday(monday=Some(day)) def inject(day: Tuesday.type): Weekday = Weekday(tuesday=Some(day)) def inject(day: Wednesday.type): Weekday = Weekday(wednesday=Some(day)) }
With this definition we try to illustrate that Monday.type
is not actually a Weekday
sub-type, but there’s a function (inject
) that makes us able to create a Weekday from a Monday.type: this function injects the value into the co-product.
The projection, on the other hand, stands for the reverse process.
Within the same example previously showed we can say that, from a Weekday
we can project its ‘tuesday’ part. As this field might be empty, we return it as an Option
value:
object Weekday { // ... all previously defined stuff goes here ... def projectM(wd: Weekday): Option[Monday.type] = wd.monday def projectT(wd: Weekday): Option[Tuesday.type] = wd.tuesday def projectW(wd: Weekday): Option[Wednesday.type] = wd.wednesday }
And how can you do this with iota?
Easy peasy! Just by using the Cop.Inject
type class, which is used for establishing the relationship between one of the types that composes the co-product and the co-product itself.
In our case, for injecting a Monday into a Weekday, we need the implicit evidence that it’s injectable:
type Weekday = Cop[Monday.type :: Tuesday.type :: Wednesday.type :: TNil] implicitly[Cop.Inject[Monday.type, Weekday]]
Thanks to this evidence, we’ll be able of doing:
val wd: Weekday = Monday.inject[Weekday]
and also of retrieving the Monday projection from any Weekday:
val wd: Weekday = Tuesday.inject[Weekday] val monday: Option[Monday.type] = Cop.Inject[Monday.type, Weekday].prj(wd) assert(monday.isEmpty, "It's actually a wednesday!")
At next post, we’ll see how to generate serializers for these co-products: pure gold…
Peace out!