En anteriores episodios de Scalera…
Recordemos que, en el anterior post, definimos nuestro subconjunto de días de la semana favorito como un co-producto:
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]
Ahora imaginemos que queremos definir un serializador para nuestros días de la semana…
Writer[T]
Pongamos que nuestro serializador es algo sencillo del estilo:
trait Writer[T] { def write(t: T): String } object Writer { def apply[T](implicit w: Writer[T]): Writer[T] = w }
Que tiene definidas instancias para nuestros días:
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!"
Nada fuera de lo normal hasta ahora: una type class con sus instancias.
La cuestión ahora es, ¿cómo podemos definir un serializador genérico para Weekday
?
La parte contratante de la primera parte…
Sabemos que
Weekday = Monday U Tuesday U Wednesday
Por lo que si tenemos definidos Writer[Monday.type]
, Writer[Tuesday.type]
y Writer[Wednesday.type]
, podríamos decir que el serializador de días de la semana será:
WeekdayWriter = Writer[Monday.type] U Writer[Tuesday.type] U Writer[Wednesday.type]
La cuestión es, ¿cómo mapeamos cada día de la semana con su serializador?
Con el complejo a la vez que simple snippet:
import TList.Op.Map type WeekdayWriter = Cop[Map[Writer, Weekday#L]]
Hemos definido el serializador de días de semana como el coproducto resultante de mapear(Map
) cada tipo T contenido en la TList
de días de la semana que compone el co-producto(Weekday#L
) al tipo Writer[T]
.
Es decir, que el compilador internamente ha hecho algo parecido (salvando obviamente las distancias cuando comparamos tipos con instancias) a:
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] }
A partir de ahora WeekdayWriter
no puede ser de ningún otro tipo que no sea Writer[T]
donde T está definido en Weekday
Sí, pero ahora tienes dos coproductos sin forma de establecer una relación entre ellos
…ok, cierto.
…será considerada como la parte contratante de la primera parte
Sabemos que queremos algo del estilo:
def weekdayWriter[D](weekday: D)(implicit stuff): WeekdayWriter = ???
¿Qué evidencias implícitas necesitamos?
- D puede ser cualquier cosa, pero necesitamos que tenga un
Writer[D]
(si no se puede serializar, no nos vale) - algo que nos diga que ese
Writer[D]
es uno de los incluídos enWeekdayWriter
. Cómo vimos en el post anterior de esta serie, esa evidencia esCop.Inject
.
Por lo tanto el método nos quedaría como:
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 }
Incluso podríamos definir un helper para hacerlo más bonito:
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) } }
Si lo probamos notaremos que:
Monday.write //"monday" Tuesday.write //"TUESDAY" GroundhogDay.write //NOPE! It doesn't compile
…incluso aunque hay un Writer[GroundhogDay.type]
implícito, al no haber una evidencia que permita inyectarlo en el co-producto de WeekdayWriter
, no permite serializarlo.
¿Y qué me aporta?
Supongamos el modelo inicial:
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"
¿Cómo definirías un serializador para Weekday
? Lo lógico sería hacer algo del estilo:
implicit def weekdayWriter: Writer[Weekday] = { case Monday => mondayW.write(Monday) case Tuesday => tuesdayW.write(Tuesday) case Wednesday => wednesdayW.write(Wednesday) }
Si resulta que decidiésemos añadir un nuevo día a la semana tendríamos que:
- añadir el
case object
- hacer que extienda de
Weekday
- añadir el serializador concreto
- modificar el serializador genérico
Mientras que con el enfoque que hemos utilizado durante estos dos posts:
- añadir el
case object
- modificar la definición de
Weekday
- añadir el serializador concreto
…venga va, tampoco es que nos ahorremos muchos sitios donde tocar pero, ¿qué ocurriría si quisieramos realizar otra agrupación, por ejemplo, por días impares?
Con el enfoque del co-producto sería tan sencillo como añadir el nuevo co-producto que representa la unión de los tipos de los días impares y la función para obtener el 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] }
Mientras que en el mundo de la herencia, donde hay muerte y sufrimiento…
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) }
Aunque es material denso, esperamos haberos ayudado a entender el fascinante mundo de los co-productos
Nos vemos en el próximo post.
¡Agur de limón!