No, no es el título de la nueva película de Coixet. Cuando hablamos de categorías como los monoides, tendemos a pensar que se quedan en el ámbito de lo experimental (aunque funcionalmente puro) y que no existe aplicación directa sobre el mundo real. Algo parecido a lo que ocurría cuando te enseñaban a hacer raíces cuadradas en el colegio y no veías el momento para usarlo en la vida real…
El caso de uso
Esta misma mañana, ingenuo de mi, intentaba hacer que funcionara lo siguiente:
type Passengers = Int type MaxCapacity = Passengers type Plane = (Passengers, MaxCapacity) val planes: List[Plane] = List(1 -> 1, 2 -> 3, 3 -> 3) val (totalPassengers, totalCapacity) = planes.sum // ERROR: Could not find implicit value // for parameter num: Numeric[(Int, Int)]
Vale, comprensible, Scala necesita algo, una evidencia, para poder sumar tuplas de enteros.
Antes de pegarnos con la “evidencia”, intentemos hacerlo funcionar de una manera mucho más mecánica:
val sum = ((0,0) /: planes){ case ((totalPas,totalCap), (passengers, capacity)) => (totalPas + passengers, totalCap + capacity) }
Okay, funciona. Pero debería ser algo más simple, por lo que retomemos la idea de la evidencia Numeric[N]
.
Implementando Numeric[N]
Necesitamos un Numeric[(A,B)]
pero antes de implementarlo (tiene bastantes métodos abstractos) escondamos debajo de la alfombra aquellos métodos en los cuales no queremos centrarnos en este ejemplo. Para ello creamos una capa por encima que deje los métodos sin implementar (que no abstractos):
trait LeanNumeric[T] extends Numeric[T] { override def fromInt(x: Int): T = ??? override def toInt(x: T): Int = ??? override def minus(x: T, y: T): T = ??? override def times(x: T, y: T): T = ??? override def negate(x: T): T = ??? override def toLong(x: T): Long = ??? override def toFloat(x: T): Float = ??? override def toDouble(x: T): Double = ??? override def compare(x: T, y: T): Int = ??? }
A esta aberración la llamaremos LeanNumeric (solo contiene lo esencial para desarrollar nuestro ejemplo). Y ahora definimos el método que genera la evidencia para cualquier Tuple2
:
implicit def numeric[A, B]( implicit nA: Numeric[A], nB: Numeric[B]): Numeric[(A, B)] = { new LeanNumeric[(A, B)]{ override def zero = (nA.zero, nB.zero) override def plus(x: (A, B), y: (A, B)): (A, B) = { val (a1, b1) = x val (a2, b2) = y (nA.plus(a1, a2), nB.plus(b1, b2)) } } }
Si ponemos el implícito en scope y ejecutamos de nuevo el planes.sum
…¡boom! Magia.
Num…oide
No hace falta saber mucho de teoría de categorías para centrarse en que Numeric[N]
, podrá ser mil cosas más, pero cumple dos propiedades:
- La operación append : La operación suma – dados n1 y n2 de tipo N, devuelve otro elemento N. Solo por disponer de esta operación (y el clossure, la asociatividad, la conmutativa, …) ya podemos considerarlo un Semigroup
- Y adicionalmente el elemento zero : genera el elemento neutro de la suma
¿en serio?¿No es evidente?…amigos, ¡el monoide ha vuelto a la ciudad!
Implementación con scalaz.Monoid
Viendo que el tipo Numeric tiene al menos esas dos propiedades, re-implementamos el implícito haciendo uso de Monoid. Primero definimos el monoide para enteros y el monoide de las tuplas,
el cual requiere de un monoide para cada tipo que compone la tupla (easy peasy)
import scalaz._ implicit object IntMonoid extends Monoid[Int]{ override def zero: Int = 0 override def append(f1: Int, f2: => Int): Int = f1 + f2 } implicit def tupleMonoid[A,B]( implicit mA: Monoid[A], mB: Monoid[B]): Monoid[(A,B)] = { new Monoid[(A, B)] { override def zero: (A, B) = (mA.zero, mB.zero) override def append(f1: (A, B), f2: => (A, B)): (A, B) = { val (a1, b1) = f1 lazy val (a2, b2) = f2 (mA.append(a1,a2), mB.append(b1, b2)) } } }
Hasta aquí bien, ¿correcto?
Implementemos ahora el implícito que nos proporcionará un Numeric siempre que exista un Monoid para el tipo
implicit def numeric[T]( implicit m: Monoid[T]): Numeric[T] = { new LeanNumeric[T]{ override def zero = m.zero override def plus(x: T, y: T): T = m.append(x, y) } } planes.sum //(6,7)
Y es increiblemente sencillo abstraerse de lo que demonios sea T (¿una tupla?, ¿un perro?, …).
Mientras sea un monoide, se puede definir un LeanNumeric.
Podéis encontrar aquí el gist resumen
Hasta la próxima locura funcional.
¡Agur de limón!