Previously on Scalera…
Let’s remember that, on previous post, we defined our favourite weekday subset as a co-product:
case object Monday case object Tuesday case object Wednesday case object GroundhogDay import iota._ import iota.syntax.all._ import TList.:: type Weekday = Cop[Monday.type :: Tuesday.type :: Wednesday.type :: TNil]
Now imagine we wanted to define a serializer for our weekdays…
Writer[T]
Assuming that our serializer is something like this:
trait Writer[T] { def write(t: T): String } object Writer { def apply[T](implicit w: Writer[T]): Writer[T] = w }
And that it has several weekday defined instances:
implicit val mondayW: Writer[Monday.type] = _ => "monday" implicit val tuesdayW: Writer[Tuesday.type] = _ => "TUESDAY" implicit val wednesdayW: Writer[Wednesday.type] = _ => "wed" implicit val madeUpdayW: Writer[GroundhogDay.type] = _ => "nonsense!"
…is not a big deal: a type class with its instances.
The question now is, how can we define a generic serializer for Weekday
?
The first part of the party of the first part…
We know that
Weekday = Monday U Tuesday U Wednesday
So if we have defined Writer[Monday.type]
, Writer[Tuesday.type]
and Writer[Wednesday.type]
, we could say that weekday serializer will be:
WeekdayWriter = Writer[Monday.type] U Writer[Tuesday.type] U Writer[Wednesday.type]
The question is, how can we map each weekday with its serializer? Well, the answer is with the following complex (and simple) snippet:
import TList.Op.Map type WeekdayWriter = Cop[Map[Writer, Weekday#L]]
We’ve defined the weekdays serializer as the coproduct result of mapping(Map
) each T type – contained inside the Weekday TList
that composes the co-product(Weekday#L
) – to the Writer[T]
type.
In other words, internally the compiler has done something remotely similar (mind the gap between types and instances – compile time vs runtime) to:
val weekdays = List(Monday, Tuesday, Wednesday) val writers = weekdays.map{ case Monday => Writer[Monday.type] case Tuesday => Writer[Tuesday.type] case Wednesday => Writer[Wednesday.type] }
From now on, WeekdayWriter
can’t be any other type appart from Writer[T]
where T is defined inside Weekday
.
Yes, but now you have two different unrelated co-products
Yep, true. Let’s establish the relationship.
…shall be known in this contract as the first part of the party of the first part…
We know that we want something similar to this:
def weekdayWriter[D](weekday: D)(implicit stuff): WeekdayWriter = ???
Which implicit evidences do we need?
- D can be anything, but we need it to have a
Writer[D]
(if it can’t be serialized, it’s worthless) - something that tell us that
Writer[D]
is one of the included writers inWeekdayWriter
. As we explained on the previous post, this evidence isCop.Inject
.
So the method would like this:
def weekdayWriter[D]( weekday: D)( implicit w: Writer[D], ev: Cop.Inject[Writer[D], WeekdayWriter]): WeekdayWriter = { //So if we know that w is injectable into WeekdayWriter... w.inject[WeekdayWriter] //Let's inject it }
we could define a helper for making it even prettier though:
implicit class AnyWeekdayWriter[D]( day: D)( implicit w: Writer[D], ev: Cop.Inject[Writer[D], WeekdayWriter]){ def write: String = weekdayWriter(day).value match { case w: Writer[D@unchecked] => w.write(day) } }
If we try it, we’ll notice that:
Monday.write //"monday" Tuesday.write //"TUESDAY" GroundhogDay.write //NOPE! It doesn't compile
…even though there’s an implicit Writer[GroundhogDay.type]
, as there’s no evidence that proves that it’s injectable into the WeekdayWriter
co-product, it’s not serializable as a Weekday.
So what does this provide?
Let’s suppose the initial model:
sealed trait Weekday case object Monday extends Weekday case object Tuesday extends Weekday case object Wednesday extends Weekday implicit lazy val mondayW: Writer[Monday.type] = _ => "monday" implicit lazy val tuesdayW: Writer[Tuesday.type] = _ => "TUES" implicit lazy val wednesdayW: Writer[Wednesday.type] = _ => "wed"
How would you define a serializer for Weekday
? Something logical would be:
implicit def weekdayWriter: Writer[Weekday] = { case Monday => mondayW.write(Monday) case Tuesday => tuesdayW.write(Tuesday) case Wednesday => wednesdayW.write(Wednesday) }
If we decided to add a new day to the week we’d have to:
- add the
case object
- make it extend from
Weekday
- add the concrete serializer
- modify the generic serializer
Whereas the focus we’ve used so far would involve:
- adding the
case object
- modifying
Weekday
definition - adding the concrete serializer
…okay, that’s not a big deal re-factoring but, what would it happen if we wanted to group those days in another way, for example, by odd days?
With the co-product focus would be as simple as defining the new co-product that stands for the type union of the odd days, as well as the function for getting the implicit Writer[T]
:
type OddDay = Cop[Monday.type :: Wednesday.type :: TNil] type OddDayWriter = Cop[Map[Writer, OddDay#L]] def odddayWriter[D]( oddday: D)( implicit w: Writer[D], ev: Cop.Inject[Writer[D], OddDayWriter]): OddDayWriter = { w.inject[OddDayWriter] }
And in the inheritance world, where there’s suffering and death…
sealed trait Weekday sealed trait OddDay case object Monday extends Weekday with OddDay case object Tuesday extends Weekday case object Wednesday extends Weekday with OddDay implicit def weekdayWriter: Writer[OddDay] = { case Monday => mondayW.write(Monday) case Wednesday => wednesdayW.write(Wednesday) }
Although this is heavy metal, we hope we’ve helped you having a better understanding about the fascinating coproduct world.
See you next post.
Peace out!