Coproductos en iota (o cómo hacer las semanas más cortas) – parte 1

Quizás recordaréis (o puede que no) que nombramos hace tiempo de pasada los co-productos cuando hablábamos de tipos algebraicos de datos. Los definíamos entonces como la manera natural de expresar unión de tipos (Wine = White U Red U Espumoso). En este post trataremos de definir enumerados expresados como co-productos, como por ejemplo, los días de la semana…

Intro: producto vs. co-producto

Por hacer algo de memoria y entender mejor el ejemplo, imaginad que tenéis una terna de elementos de ciertos tipos:

(Int, String, Double)

Si los datos que representan a un estudiante se pueden expresar como una terna de edad Y nombre Y nota media, podemos decir que un estudiante se expresa como el producto de estos tres tipos:

case class Student(
  age: Int,
  name: String,
  avg: Double)

(Curioso además que las case classes extiendan de algo llamado scala.Product, ehem …)

Si por el contrario nos dicen que el resultado de una llamada web nos puede devolver el número de filas erróneas O el valor que queríamos recuperar O el número de segundos a esperar hasta nueva petición, estamos hablando de un co-producto.

Intro: Co-productos en Scala sin ruedines

En Scala, definir un enumerado como un co-producto puede ser tan sencillo como definir:

sealed trait Color
case object Blue extends Color
case object Red extends Color
//...

Pero, ¿qué ocurre cuando los tipos que forman el co-producto no tienen relación entre sí (Int, Animal, …)?

Bueno, podríamos usar el tipo Either:

type Or[A, B] = Either[A, B]
val v1: Int Or String = Left(2)
val v2: Int Or String = Right("hi")

Fácil, ¿no? Pero, ¿qué ocurre ahora si queremos indicar que el valor puede ser Int o String o Boolean? La cosa se complica un poco más:

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

Una de las alternativas para evitar este dolor de cabeza es iota, una librería desarrollada por 47deg para definir co-productos de una manera ‘awesómica’.
Tiene soporte tanto para scalaz como para cats. En este post, veremos como aplica a cats (because reasons…)

libro-dependiente

Para que esto empiece a compilar, empezaremos añadiendo a nuestro proyecto las dependencias de cats-core e iota-core:

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"
)

Punto de partida

Supongamos que tenemos que definir un enumerado.
Una de las formas más comunes de hacerlo en Scala, cómo decíamos algún párrafo más arriba, sería con:

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

No obstante, quitemos algunos días de más (para hacerlo más sencillo) y eliminemos la relación de herencia que los relaciona (para hacerlo más interesante):

case object Monday
case object Tuesday
case object Wednesday
case object GroundhogDay //Just for messing things up...

Para expresar ahora la unión de tipos ( Weekday = Monday U Tuesday U Wednesday) con iota definimos el co-producto como sigue:

// 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]

Y después de estos bonitos glifos mayas, ¿cómo usamos el tipo Weekday?
Porque esto definitivamente no funciona:

val day: Weekday = Monday //NOPE! It doesn't fit

Para poder usarlo, tendremos que ver cómo inyectar el tipo Monday.type en nuestro co-producto.
Si tu primera impresión a la frase anterior ha sido ‘eing?’, dale una ojeada a la siguiente sección sobre los conceptos de inyección y proyección. En caso contrario, salta a la página 30 para matar al troll puedes pasar a la siguiente.

Inject & Project

Sin entrar en mucho detalle sobre módulos inyectivos y proyectivos, veamos un sencillo ejemplo para entender los conceptos.
Imaginemos que yo hubiera definido el co-producto de los días de la semana como una case class con todos los posibles valores representados por un Option, de manera que solo uno de ellos puede estar relleno:

// 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))
}

Con esta definición pretendemos ilustrar que Monday.type realmente no es un subtipo de Weekday, pero existe una función (inject) que nos permite crear un Weekday a partir de un Monday.type: esta función inyecta el valor en el co-producto.

La proyección, por otra parte, representa el proceso inverso.
En el mismo ejemplo de antes, podemos decir que, a partir de un Weekday, podemos proyectar su parte ‘tuesday’. Como podría estar vacío para ese campo, devolvemos el Option (que ya es por definición):

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

}

Y esto en iota, ¿cómo se hace?

Pues easy peasy, utilizando la type class Cop.Inject, que se utiliza para establecer la relación entre el tipo que compone el co-producto y el co-producto en sí.
En nuestro caso, para poder inyectar un Monday en un Weekday, necesitamos la evidencia implícita de que es inyectable :

type Weekday = Cop[Monday.type :: Tuesday.type :: Wednesday.type :: TNil]

implicitly[Cop.Inject[Monday.type, Weekday]]

Mediante dicha evidencia, seremos capaces de hacer:

val wd: Weekday = Monday.inject[Weekday]

Y también de obtener la proyección de Monday sobre cualquier 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!")

En la próxima entrega de la serie, veremos cómo generar serializadores para estos co-productos: canela fina…

¡Agur de limón!

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s