Programación Funcional para Mortales con Scalaz
Programación Funcional para Mortales con Scalaz
Sam Halliday y Oscar Vargas Torres
Buy on Leanpub

Tabla de contenidos

“El amor es sabio; el odio es tonto. En este mundo, que está interconectado cada vez más, tenemos que aprender a tolerarnos unos a otros, tenemos que aprender a soportar el hecho de que algunas personas dirán cosas que no nos agraden. Es la única manera en la que podemos vivir juntos. Pero si hemos de vivir juntos, y no morir juntos, debemos aprender una clase de caridad y clase de tolerancia, que es absolutamente vital para la continuidad de la vida humana en este planeta.”

― Bertrand Russell

Acerca de este libro

Este libro es para el desarrollador de Scala típico, probablemente con conocimientos previos de Java, que tiene escepticismo y curiosidad sobre el paradigma de Programación Funcional (PF). Este libro justifica cada concepto con ejemplos prácticos, incluyendo la escritura de una aplicación web.

Este libro usa Scalaz 7.2, la librería para Programación Funcional en Scala más exhaustiva, popular, estable y basada en principios.

Este libro está diseñado para leerse de principio a fin, en el orden presentado, con un descanso entre capítulos. Los primeros capítulos incentivan estilos de programación que más tarde serán desacreditados: de manera similar a cómo aprendimos la teoría la gravedad de Newton cuando eramos niños, y progresamos a Riemann/Einstein/Maxwell si nos convertimos en estudiantes de física.

No es necesaria una computadora mientras se lee el libro, pero se recomienda el estudio del código fuente de Scalaz. Algunos de los ejemplos de código más complejos se encuentran con el código fuente del libro y se anima a aquellos que deseen ejercicios prácticos a reimplementar Scalaz (y la aplicación de ejemplo) usando las descripciones parciales presentadas en este libro.

También recomendamos El libro rojo como lectura adicional. Éste libro enseña cómo escribir una librería de Programación Funcional en Scala usando principios fundamentales.

Aviso de Copyleft

Este libro es Libre y sigue la filosofía de Free Software: usted puede usar este libro como desee, el código fuente está disponible y puede redistribuir este libro y puede distribuir su propia versión. Esto significa que puede imprimirlo, fotocopiarlos, enviarlo por correo electrónico, subirlo a sitios web, cambiarlo, traducirlo, cobrar por él, mezclarlo, borrar partes de él, y dibujar encima de él.

Este libro es Copyleft: si usted cambia el libro y distribuye su propia version, también debe pasar estas libertades a quienes lo reciban.

Este libro usa la licencia Atribución/Reconocimiento-CompartirIgual 4.0 Internacional (CC BY-SA 4.0).

Todos los fragmentos de código en este libro están licenciadas de manera separada de acuerdo con CC0, y usted puede usarlos sin restricción. Los fragmentos de Scalaz y librerías relacionadas mantienen su propia licencia, que se reproduce de manera completa en el apéndice.

La aplicación de ejemplo drone-dynamic-agents se distribuye bajo los términos de la licencia GPLv3: sólo los fragmentos de código en este libro están disponibles sin restricción alguna.

Agradecimientos

Diego Esteban Alonso Blas, Raúl Raja Martínez y Peter Neyens de 47 degrees, Rúnar Bjarnason, Tony Morris, John de Goes y Edward Kmett por su ayuda explicando los principios de la Programación Funcional. Kenji Yoshida y Jason Zaugg por ser los principales autores de Scalaz, y Paul Chiusano / Miles Sabin por arreglar un error crítico en el compilador de Scala (SI-2712).

Muchas gracias a los lectores que dieron retroalimentación de los primeros bosquejos de este texto.

Cierto material fue particularmente valioso para mi propio entendimiento de los conceptos que están en este libro. Gracias a Juan Manuel Serrano por All Roads Lead to Lambda, Pere Villega por On Free Monads, Dick Wall y Josh Suereth por For: What is it Good For?, Erik Bakker por Options in Futures, how to unsuck them, Noel Markham por ADTs for the Win!, Sukant Hajra por Classy Monad Transformers, Luka Jacobowitz por Optimizing Tagless Final, Vincent Marquez por Index your State, Gabriel Gonzalez por The Continuation Monad, y Yi Lin Wei / Zainab Ali por sus tutoriales en los meetups Hack The Tower.

A las almas serviciales que pacientemente me explicaron cosas a mi: Merlin Göttlinger, Edmund Noble, Fabio Labella, Adelbert Chang, Michael Pilquist, Paul Snively, Daniel Spiewak, Stephen Compall, Brian McKenna, Ryan Delucchi, Pedro Rodriguez, Emily Pillmore, Aaron Vargo, Tomas Mikula, Jean-Baptiste Giraudeau, Itamar Ravid, Ross A. Baker, Alexander Konovalov, Harrison Houghton, Alexandre Archambault, Christopher Davenport, Jose Cardona, Isaac Elliott.

Aspectos prácticos

Para configurar un proyecto que use las librerías presentadas en este libro, use una versión reciente de Scala con características específicas de Programación Funcional habilitadas (por ejemplo, en build.sbt):

  scalaVersion in ThisBuild := "2.12.6"
  scalacOptions in ThisBuild ++= Seq(
    "-language:_",
    "-Ypartial-unification",
    "-Xfatal-warnings"
  )

  libraryDependencies ++= Seq(
    "com.github.mpilquist" %% "simulacrum"     % "0.13.0",
    "org.scalaz"           %% "scalaz-core"    % "7.2.26"
  )

  addCompilerPlugin("org.spire-math" %% "kind-projector" % "0.9.7")
  addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full)

Con el objetivo de mantener la brevedad en los fragmentos de código, omitiremos la sección de import. A menos que se diga lo contrario, asumimos que todos los fragmentos tienen las siguientes sentencias de import:

  import scalaz._, Scalaz._
  import simulacrum._

1. Introducción

Es parte de la naturaleza humana permanecer escéptico a un nuevo paradigma. Para poner en perspectiva qué tan lejos hemos llegado, y los cambios que ya hemos aceptado en la JVM (de las siglas en inglés para Máquina Virtual de Java), empecemos recapitulando brevemente los últimos 20 años.

Java 1.2 introdujo la API de Colecciones, permitiéndonos escribir métodos que abstraen sobre colecciones mutables. Era útil para escribir algoritmos de propósito general y era el fundamento de nuestras bases de código.

Pero había un problema, teníamos que hacer casting en tiempo de ejecución:

  public String first(Collection collection) {
    return (String)(collection.get(0));
  }

En respuesta, los desarrolladores definieron objectos en su lógica de negocios que eran efectivamente ColeccionesDeCosas, y la API de Colecciones se convirtió en un detalle de implementación.

En 2005, Java 5 introdujo los genéricos, permitiéndonos definir una Coleccion<Cosa>, abstrayendo no sólo el contenedor sino sus elementos. Los genéricos cambiaron la forma en que escribimos Java.

El autor del compilador de genéricos para Java, Martin Odersky, creó Scala después, con un systema de tipo más fuerte, estructuras inmutables y herencia múltiple. Esto trajo consigo una fusión de la Programación Orientada a Objectos (POO) y la Programación Funcional (PF).

Para la mayoría de los desarrolladores, PF significa usar datos inmutables tanto como sea posible, pero consideran que el estado mutable todavía es un mal necesario que debe aislarse y controlarse, por ejemplo con actores de Akka o clases sincronizadas. Este estilo de PF resulta en programas simples que son fáciles de paralelizar y distribuir: definitivamente es una mejora con respecto a Java. Pero este enfoque solo toca la amplia superficie de beneficios de PF, como descubriremos en este libro.

Scala también incorpora Future, haciendo simple escribir aplicaciones asíncronas. Pero cuando un Future se usa en un tipo de retorno, todo necesita reescribirse para usarlos, incluyendo las pruebas, que ahora están sujetas a timeouts arbitrarios.

Nos encontramos con un problema similar al que se tuvo con Java 1.0: no hay forma de abstraer la ejecución, así como no se tenía una manera de abstraer sobre las colecciones.

1.1 Abstrayendo sobre la ejecución

Suponga que deseamos interactuar con el usuario a través de la línea de comandos. Podemos leer (read) lo que el usuario teclea y entonces podemos escribirle (write) un mensaje.

  trait TerminalSync {
    def read(): String
    def write(t: String): Unit
  }

  trait TerminalAsync {
    def read(): Future[String]
    def write(t: String): Future[Unit]
  }

¿Cómo podemos escribir código genérico que haga algo tan simple como un eco de la entrada del usuario, ya sea de manera síncrona o asíncrona, dependiendo de nuestra implementación para tiempo de ejecución?

Podríamos escribir una versión síncrona y envolverla en un Future pero ahora tendríamos que preocuparnos por cuál thread pool debemos usar para ejecutar el trabajo, o podríamos esperar el Future con Await.result y bloquear el thread. En ambos casos, es mucho código tedioso y fundamentalmente estamos lidiando con dos APIs que no están unificadas.

Podríamos resolver el problema, como con Java 1.2, con un padre común usando el soporte de Scala para higher-kinded types (HKT).

Deseamos definir Terminal para un constructor de tipo C[_]. Si definimos Now (en español, ahora) de modo que sea equivalente a su parámetro de tipo (como Id), es posible implementar una interfaz común para terminales síncronas y asíncronas:

  trait Terminal[C[_]] {
    def read: C[String]
    def write(t: String): C[Unit]
  }

  type Now[X] = X

  object TerminalSync extends Terminal[Now] {
    def read: String = ???
    def write(t: String): Unit = ???
  }

  object TerminalAsync extends Terminal[Future] {
    def read: Future[String] = ???
    def write(t: String): Future[Unit] = ???
  }

Podemos pensar en C como un Contexto porque decimos “en el contexto de ejecución Now (ahora)” o “en el Futuro”.

Pero no sabemos nada sobre C y tampoco podemos hacer algo con un C[String]. Lo que necesitamos es un contexto/ambiente de ejecución que nos permita invocar un método que devuelva C[T] y después sea capaz de hacer algo con la T, incluyendo la invocación de otro método sobre Terminal. También necesitamos una forma de envolver un valor como un C[_]. La siguiente signatura funciona bien:

  trait Execution[C[_]] {
    def chain[A, B](c: C[A])(f: A => C[B]): C[B]
    def create[B](b: B): C[B]
  }

que nos permite escribir:

  def echo[C[_]](t: Terminal[C], e: Execution[C]): C[String] =
    e.chain(t.read) { in: String =>
      e.chain(t.write(in)) { _: Unit =>
        e.create(in)
      }
    }

Ahora podemos compartir la implementación de echo en código síncrono y asíncrono. Podemos escribir una implementación simulada de Terminal[Now] y usarla en nuestras pruebas sin ningún tiempo límite (timeout).

Las implementaciones de Execution[Now] y Execution[Future] son reusables por métodos genéricos como echo.

Pero el código para echo es horrible!

La característica de clases implícitas del lenguaje Scala puede darle a C algunos métodos. Llamaremos a estos métodos flatMap y map por razones que serán más claras en un momento. Cada método acepta un implicit Execution[C]. Estos no son más que los métodos flatMap y map a los que estamos acostumbrados a usar con Seq, Option y Future.

  object Execution {
    implicit class Ops[A, C[_]](c: C[A]) {
      def flatMap[B](f: A => C[B])(implicit e: Execution[C]): C[B] =
            e.chain(c)(f)
      def map[B](f: A => B)(implicit e: Execution[C]): C[B] =
            e.chain(c)(f andThen e.create)
    }
  }

  def echo[C[_]](implicit t: Terminal[C], e: Execution[C]): C[String] =
    t.read.flatMap { in: String =>
      t.write(in).map { _: Unit =>
        in
      }
    }

Ahora podemos revelar porqué usamos flatMap como el nombre del método: nos permite usar una for comprehension, que es una conveniencia sintáctica (syntax sugar) para flatMap y map anidados.

  def echo[C[_]](implicit t: Terminal[C], e: Execution[C]): C[String] =
    for {
      in <- t.read
       _ <- t.write(in)
    } yield in

Nuestro contexto Execution tiene las mismas signaturas que una trait en Scalaz llamada Monad, excepto que chain es bind y create es pure. Decimos que C es monádica cuando hay una Monad[C] implícita disponible en el ámbito. Además, Scalaz tiene el alias de tipo Id.

En resumen, si escribimos métodos que operen en tipos monádicos, entonces podemos escribir código sequencial que puede abstraer sobre su contexto de ejecución. Aquí, hemos mostrado una abstracción sobre la ejecución síncrona y asíncrona pero también puede ser con el propósito de conseguir un manejo más riguroso de los errores (donde C[_] es Either[Error, ?]), administrar o controlar el acceso a estado volátil, realizar I/O, o la revisión de la sesión.

1.2 Programación Funcional Pura

La programación funcional es el acto de escribir programas con funciones puras. Las funciones puras tienen tres propiedades:

  • Totales: devuelven un valor para cada entrada posible.
  • Deterministas: devuelven el mismo valor para la misma entrada.
  • Inculpable: no interactúan (directamente) con el mundo o el estado del programa.

Juntas, estas propiedades nos dan una habilidad sin precedentes para razonar sobre nuestro código. Por ejemplo, la validación de entradas es más fácil de aislar gracias a la totalidad, el almacenamiento en caché es posible cuando las funciones son deterministas, y la interacción con el mundo es más fácil de controlar, y probar, cuando las funciones son inculpables.

Los tipos de cosas que violan estas propiedades son efectos laterales: el acceso o cambio directo de estado mutable (por ejemplo, mantener una var en una clase o el uso de una API antigua e impura), la comunicación con recursos externos (por ejemplo, archivos o una búsqueda en la red), o lanzar y atrapar excepciones.

Escribimos funciones puras mediante evitar las excepciones, e interactuando con el mundo únicamente a través de un contexto de ejecución F[_] seguro.

En la sección previa, se hizo una abstracción sobre la ejecución y se definieron echo[Id] y echo[Future]. Tal vez esperaríamos, razonablemente, que la invocación de cualquier echo no realizara ningún efecto lateral, porque es puro. Sin embargo, si usamos Future o Id como nuestro contexto de ejecución, nuestra aplicación empezará a escuchar a stdin:

  val futureEcho: Future[String] = echo[Future]

Estaríamos violando la pureza y no estaríamos escribiendo código puramente funcional: futureEchoes el resultado de invocar echo una vez. Future combina la definición de un programa con su interpretación (su ejecución). Como resultado, es más difícil razonar sobre las aplicaciones construidas con Future.

Podemos definir un contexto de ejecución simple F[_]:

  final class IO[A](val interpret: () => A) {
    def map[B](f: A => B): IO[B] = IO(f(interpret()))
    def flatMap[B](f: A => IO[B]): IO[B] = IO(f(interpret()).interpret())
  }
  object IO {
    def apply[A](a: =>A): IO[A] = new IO(() => a)
  }

que evalúa un thunk de manera perezosa (o por necesidad). IO es simplemente una estructura de datos que referencia (posiblemente) código impuro, y no está, en realidad, ejecutando algo. Podemos implementar Terminal[IO]:

  object TerminalIO extends Terminal[IO] {
    def read: IO[String]           = IO { io.StdIn.readLine }
    def write(t: String): IO[Unit] = IO { println(t) }
  }

e invocar echo[IO] para obtener de vuelta el valor

  val delayed: IO[String] = echo[IO]

Este val delayed puede ser reutilizado, es simplemente la definición del trabajo que debe hacerse. Podemos mapear la cadena y componer (en el sentido funcional) programas, tal como podríamos mapear un Futuro. IO nos mantiene honestos al hacer explícito que dependemos de una interacción con el mundo, pero no nos detiene de acceder a la salida de tal interacción.

El código impuro dentro de IO solo es evaluado cuando se invoca .interpret() sobre el valor, y se trata de una acción impura

  delayed.interpret()

Una aplicación compuesta de programas IO solamente se interpreta una vez, en el método main, que tambié se llama el fin del mundo.

En este libro, expandiremos los conceptos introducidos en este capítulo y mostraremos cómo escribir funciones mantenibles y puras, que logren nuestros objetivos de negocio.

2. Comprensión for

La comprensión for de Scala es la abstracción ideal de la programación funcional para los programas secuenciales que interactúan con el mundo. Dado que la usaremos mucho, vamos a reaprender los principios de for y cómo Scalaz puede ayudarnos a escribir código más claro.

Este capítulo no intenta enseñarnos a escribir programas puros y las técnicas son aplicables a bases de código que no sean puramente funcionales.

2.1 Conveniencia sintáctica

for en Scala es simplemente una regla de reescritura, llamada también una conveniencia sintáctica (azúcar sintáctico) y no tiene ninguna información contextual.

Para ver qué es lo que hace una for comprehension, usamos la característica show y reify de la REPL (Read-Eval-Print-Loop) para mostrar cómo se ve el código después de la inferencia de tipos.

  scala> import scala.reflect.runtime.universe._
  scala> val a, b, c = Option(1)
  scala> show { reify {
           for { i <- a ; j <- b ; k <- c } yield (i + j + k)
         } }

  res:
  $read.a.flatMap(
    ((i) => $read.b.flatMap(
      ((j) => $read.c.map(
        ((k) => i.$plus(j).$plus(k)))))))

Existe mucho “ruido” debido a las conveniencias sintácticas adicionales (por ejemplo, + se reescribe $plus, etc.). No mostraremos el show y el reify por brevedad cuando la línea del REPL se muestre como reify, y manualmente limpiaremos el código generado de modo que no se vuelva una distracción.

  reify> for { i <- a ; j <- b ; k <- c } yield (i + j + k)

  a.flatMap {
    i => b.flatMap {
      j => c.map {
        k => i + j + k }}}

La regla de oro es que cada <- (llamada un generador) es una invocación anidada de flatMap, siendo el último generador un map que contiene el cuerpo del yield.

2.1.1 Asignación

Podemos asignar valores al vuelo (inline) como ij = i + j (no es necesaria la palabra reservada val).

  reify> for {
           i <- a
           j <- b
           ij = i + j
           k <- c
         } yield (ij + k)

  a.flatMap {
    i => b.map { j => (j, i + j) }.flatMap {
      case (j, ij) => c.map {
        k => ij + k }}}

Un map sobre la b introduce la ij, sobre la cual se llama un flatMap junto con la j, y entonces se invoca a un map final sobre el código en el yield.

Desgraciadamente no podemos realizar ninguna asignación antes de algún generador. Esta característica ya ha sido solicitada para que sea soportada por el lenguaje, pero la implementación no se ha llevado a cabo: https://github.com/scala/bug/issues/907

  scala> for {
           initial = getDefault
           i <- a
         } yield initial + i
  <console>:1: error: '<-' expected but '=' found.

Podemos proporcionar una solución alternativa al definir un val fuera del for

  scala> val initial = getDefault
  scala> for { i <- a } yield initial + i

o crear un Option a partir de la asignación inicial

  scala> for {
           initial <- Option(getDefault)
           i <- a
         } yield initial + i

2.1.2 Filter

Es posible poner sentencias if después de un generador para filtrar los valores usando un predicado

  reify> for {
           i  <- a
           j  <- b
           if i > j
           k  <- c
         } yield (i + j + k)

  a.flatMap {
    i => b.withFilter {
      j => i > j }.flatMap {
        j => c.map {
          k => i + j + k }}}

Versiones anteriores de Scala usaban filter, pero Traversable.filter crea nuevas colecciones para cada predicado, de modo que withFilter se introdujo como una alternativa de mayor rendimiento. Podemos ocasionar accidentalmente la invocación de withFilter al proporcionar información de tipo, interpretada como un empate de patrones.

  reify> for { i: Int <- a } yield i

  a.withFilter {
    case i: Int => true
    case _      => false
  }.map { case i: Int => i }

Como la asignación, un generador puede usar un empate de patrones en el lado izquierdo. Pero a diferencia de una asignación (que lanza un MatchError al fallar) los generadores son filtrados y no fallarán en tiempo de ejecución. Sin embargo, existe una aplicación ineficiente, doble, del patrón.

2.1.3 For Each

Finalmente, si no hay un yield, el compilador usará foreach en lugar de flatMap, que es útil únicamente por los efectos colaterales.

  reify> for { i <- a ; j <- b } println(s"$i $j")

  a.foreach { i => b.foreach { j => println(s"$i $j") } }

2.1.4 Resumen

El conjunto completo de métodos soportados por las for comprehensions no comparten un super tipo común; cada fragmento generado es compilado de manera independiente. Si hubiera un trait, se vería aproximadamente así:

  trait ForComprehensible[C[_]] {
    def map[A, B](f: A => B): C[B]
    def flatMap[A, B](f: A => C[B]): C[B]
    def withFilter[A](p: A => Boolean): C[A]
    def foreach[A](f: A => Unit): Unit
  }

Si el contexto (C[_]) de una for comprehension no proporciona su propio map y flatMap, no todo está perdido. Si existe un valor implícito de scalaz.Bind[T] para la T en cuestión, este proporcionará el map y el flatMap.

2.2 El camino dificultoso

Hasta el momento sólo hemos observado las reglas de reescritura, no lo que está sucediendo en el map y en el flatMap. Considere lo que sucede cuando el contexto del for decide que no se puede proceder más.

En el ejemplo del Option, el yield se invoca únicamente cuando todos i, j, k están definidos.

  for {
    i <- a
    j <- b
    k <- c
  } yield (i + j + k)

Si cualquiera de a, b, c es None, la comprehension se corto-circuita con None pero no nos indica qué fue lo que salió mal.

Si usamos Either, entonces un Left ocasionará que la for comprehension se corto circuite con información extra, mucho mejor que Option para reporte de errores:

  scala> val a = Right(1)
  scala> val b = Right(2)
  scala> val c: Either[String, Int] = Left("sorry, no c")
  scala> for { i <- a ; j <- b ; k <- c } yield (i + j + k)

  Left(sorry, no c)

Y por último, veamos que sucede con un Future que falla:

  scala> import scala.concurrent._
  scala> import ExecutionContext.Implicits.global
  scala> for {
           i <- Future.failed[Int](new Throwable)
           j <- Future { println("hello") ; 1 }
         } yield (i + j)
  scala> Await.result(f, duration.Duration.Inf)
  caught java.lang.Throwable

El Future que imprime a la terminal nunca se invoca porque, como Option y Either, la for comprehension se corto circuita.

El corto circuito para el camino difícil es un tema común e importante. Las for comprehensions no pueden expresar la limpieza de recursos: no hay forma de realizar un try/finally. Esto es bueno, en PF asigna claramente la responsabilidad por la recuperación inesperada de errores y la limpieza de recursos en el contexto (que normalmente es una Monad como veremos más tarde), no la lógica del negocio.

2.3 Gimnasia

Aunque es sencillo reescribir código secuencial simple como una for comprehension, algunas veces desearemos hacer algo que parezca requerir hacer volteretas mentales. Esta sección recolecta algunos ejemplos prácticos de cómo lidiar con ellos.

2.3.1 Lógica de respaldo

Digamos que estamos llamando a un método que regresa un Option. Si no es exitoso deseamos ejecutar otro método como respaldo (y así consecutivamente), como cuando estamos unando un cache:

  def getFromRedis(s: String): Option[String]
  def getFromSql(s: String): Option[String]

  getFromRedis(key) orElse getFromSql(key)

Si tenemos que hacer esto para una versión asíncrona de la misma API

  def getFromRedis(s: String): Future[Option[String]]
  def getFromSql(s: String): Future[Option[String]]

entonces tenemos que ser cuidadosos de no hacer trabajo extra porque

  for {
    cache <- getFromRedis(key)
    sql   <- getFromSql(key)
  } yield cache orElse sql

ejecutará ambas consultas. Podemos hacer empate de patrones en el primer resultado pero el tipo es incorrecto

  for {
    cache <- getFromRedis(key)
    res   <- cache match {
               case Some(_) => cache !!! wrong type !!!
               case None    => getFromSql(key)
             }
  } yield res

Necesitamos crear un Future a partir del cache

  for {
    cache <- getFromRedis(key)
    res   <- cache match {
               case Some(_) => Future.successful(cache)
               case None    => getFromSql(key)
             }
  } yield res

Future.successful crea un nuevo Future, de manera muy similar a cómo un Option o un constructor de List.

2.3.2 Salida temprana

Digamos que tenemos alguna condición que implique la terminación temprana con un valor exitoso.

Si deseamos terminar de manera temprana con un error, es práctica estándar en programación orientada a objectos lanzar una excepción

  def getA: Int = ...

  val a = getA
  require(a > 0, s"$a must be positive")
  a * 10

que puede reescribirse de manera asíncrona

  def getA: Future[Int] = ...
  def error(msg: String): Future[Nothing] =
    Future.failed(new RuntimeException(msg))

  for {
    a <- getA
    b <- if (a <= 0) error(s"$a must be positive")
         else Future.successful(a)
  } yield b * 10

Pero si deseamos terminar temprano con un valor de retorno exitoso, el código síncrono simple:

  def getB: Int = ...

  val a = getA
  if (a <= 0) 0
  else a * getB

se traduce a for comprehension anidados cuando nuestras dependencias son asíncronas:

  def getB: Future[Int] = ...

  for {
    a <- getA
    c <- if (a <= 0) Future.successful(0)
         else for { b <- getB } yield a * b
  } yield c

2.4 Sin posibilidad de usar for comprehension

El contexto que sobre el que estamos haciendo la for comprehension debe ser el mismo: no es posible mezclar contextos.

  scala> def option: Option[Int] = ...
  scala> def future: Future[Int] = ...
  scala> for {
           a <- option
           b <- future
         } yield a * b
  <console>:23: error: type mismatch;
   found   : Future[Int]
   required: Option[?]
           b <- future
                ^

Nada puede ayudarnos a mezclar contextos arbitrarios en una for comprehension porque el significado no está bien definido.

Cuando tenemos contextos anidados la intención es normalmente obvia, pero sin embargo, el compilador no aceptará nuestro código.

  scala> def getA: Future[Option[Int]] = ...
  scala> def getB: Future[Option[Int]] = ...
  scala> for {
           a <- getA
           b <- getB
         } yield a * b
                   ^
  <console>:30: error: value * is not a member of Option[Int]

Aquí deseamos que for se ocupe del contexto externo y nos deje escribir nuestro código en el Option interno. Esconder el contexto externo es exactamente lo que hace un transformador de mónadas, y Scalaz proporciona implementaciones para Option y Either llamadas OptionT y EitherT respectivamente.

El contexto externo puede ser cualquier cosa que normalmente funcione en una for comprehension, pero necesita ser el mismo a través de toda la comprehension.

Aquí creamos un OptionT por cada invocación de método. Esto cambia el contexto del for de un Future[Option[_]] a OptionT[Future, _].

  scala> val result = for {
           a <- OptionT(getA)
           b <- OptionT(getB)
         } yield a * b
  result: OptionT[Future, Int] = OptionT(Future(<not completed>))

.run nos devuelve el contexto original.

  scala> result.run
  res: Future[Option[Int]] = Future(<not completed>)

El transformador de mónadas también nos permite mezclar invocaciones a Future[Option[_]] con métodos que simplemente devuelven Future mediante el uso de .liftM[OptionT] (proporcionado por Scalaz):

  scala> def getC: Future[Int] = ...
  scala> val result = for {
           a <- OptionT(getA)
           b <- OptionT(getB)
           c <- getC.liftM[OptionT]
         } yield a * b / c
  result: OptionT[Future, Int] = OptionT(Future(<not completed>))

y así podemos mezclar métodos que devuelven un Option simple al envolverlos en un Future.successful (.pure[Future]) seguido de un OptionT

  scala> def getD: Option[Int] = ...
  scala> val result = for {
           a <- OptionT(getA)
           b <- OptionT(getB)
           c <- getC.liftM[OptionT]
           d <- OptionT(getD.pure[Future])
         } yield (a * b) / (c * d)
  result: OptionT[Future, Int] = OptionT(Future(<not completed>))

De nuevo, el código es algo enmarañado, pero es mejor que escribir flatMap y map anidados manualmente. Podríamos limpiarlo un poco con un Domain Specific Language (DSL) que se encargue de todas las conversiones a OptionT[Future, _] que sean necesarias

  def liftFutureOption[A](f: Future[Option[A]]) = OptionT(f)
  def liftFuture[A](f: Future[A]) = f.liftM[OptionT]
  def liftOption[A](o: Option[A]) = OptionT(o.pure[Future])
  def lift[A](a: A)               = liftOption(Option(a))

combinados con el operador |>, que aplica la función de la derecha al valor a la izquierda, para separar visualmente la lógica de los transformadores

  scala> val result = for {
           a <- getA       |> liftFutureOption
           b <- getB       |> liftFutureOption
           c <- getC       |> liftFuture
           d <- getD       |> liftOption
           e <- 10         |> lift
         } yield e * (a * b) / (c * d)
  result: OptionT[Future, Int] = OptionT(Future(<not completed>))

Este enfoque también funciona para EitherT (y otros) como el contexto interno, pero sus métodos para hacer lifting som más complejos y requieren parámetros. Scalaz proporciona transformadores de mónadas para muchos de sus propios tipos, de modo que vale la pena verificar si hay alguno disponible.

3. Diseño de aplicaciones

En este capítulo escribiremos la lógica de negocio y las pruebas para una aplicación de servidor puramente funcional. El código fuente para esta aplicación se incluye bajo el directorio example junto con la fuente del libro, aunque se recomienda no leer el código fuente hasta el final del capítulo porque habrá refactorizaciones significativas a medida que aprendamos más sobre la programación funcional.

3.1 Especificación

Nuestra aplicación administrará una granja de compilación just-in-time (justo a tiempo) con un presupuesto limitado. Escuchará al servidor de integración continua Drone, y creará agentes trabajadores usando Google Container Engine (GKE) para lograr las demandas de la cola de trabajo.

El drone recibe trabajo cuando un contribuidor manda un pull request de github a uno de los proyectos administrados. El dron asigna el trabajo a sus agentes, cada uno procesando una actividad a la vez.

La meta de nuestra aplicación es asegurar que hay suficientes agentes para completar el trabajo, con un límite de capacidad para el número de agentes, a la vez que se minimiza el costo total. Nuestra aplicación necesita conocer el número de artículos en el backlog y el número disponible de agentes.

Google puede crear nodos, y cada uno puede hospedar múltiples agentes de dron. Cuando un agente inicia, se registra a sí mismo con un dron y el dron se encarga del ciclo de vida (incluyendo las invocaciones de supervivencia para detectar los agentes removidos).

GKE cobra una cuota por minuto de tiempo de actividad, redondeado (hacia arriba) al la hora más cercana para cada nodo. No se trata de simplemente crear un nuevo nodo por cada trabajo en la cola de actividades, debemos reusar nodos y retenerlos haste su minuto # 58 para obtener el mayor valor por el dinero.

Nuestra aplicación necesita ser capaz de empezar y detener nodos, así como verificar su estatus (por ejemplo, los tiempos de actividad, y una lista de los nodos inactivos) y conocer qué tiempos GKE piensa que debe haber.

Además, no hay una API para hablar directamente con un agente de modo que no sabemos si alguno de los agentes individuales está realizando algún trabajo para el servidor de drones. Si accidentalmente detenemos un agente mientras está realizando trabajo, es inconveniente y requiere que un humano reinicie el trabajo.

Los contribuidores pueden añadir agentes manualmente a la granja, de modo que contar agentes y nodos no es equivalente. No es necesario proporcionar algún nodo si hay agentes disponibles.

El modo de falla siempre debería tomar al menos la opción menos costosa.

Tanto Drone como GKE tienen una interfaz JSON sobre una API REST con autenticación OAuth 2.0.

3.2 Interfaces / Algebras

Ahora codificaremos el diagrama de arquitectura de la sección previa. Primeramente, necesitamos definir un tipo de datos simple para almacenar un momento (tiempo) en milisegundos porque este concepto simple no existe ni en la librería estándar de Java ni en la de Scala:

  import scala.concurrent.duration._

  final case class Epoch(millis: Long) extends AnyVal {
    def +(d: FiniteDuration): Epoch = Epoch(millis + d.toMillis)
    def -(e: Epoch): FiniteDuration = (millis - e.millis).millis
  }

En PF, una álgebra toma el lugar de una interface en Java, o el conjunto de mensajes válidos para un Actor de Akka. Esta es la capa donde definimos todas las interacciones colaterales de nuestro sistema.

Existe una interacción estrecha entre la escritura de la lógica de negocio y su álgebra: es un buen nivel de abstracción para diseñar un sistema.

  trait Drone[F[_]] {
    def getBacklog: F[Int]
    def getAgents: F[Int]
  }

  final case class MachineNode(id: String)
  trait Machines[F[_]] {
    def getTime: F[Epoch]
    def getManaged: F[NonEmptyList[MachineNode]]
    def getAlive: F[Map[MachineNode, Epoch]]
    def start(node: MachineNode): F[MachineNode]
    def stop(node: MachineNode): F[MachineNode]
  }

Ya hemos usado NonEmptyList, creado fácilmente mediante la invocación de .toNel sobre un objecto List de la librería estándar (que devuelve un Option[NonEmptyList]), y el resto debería resultar familiar.

3.3 Lógica de negocios

Ahora escribiremos la lógica de negocios que define el comportamiento de la aplicación, considerando únicamente la situación más positiva.

Necesitamos una clase WorldView para mantener una instantánea de nuestro conocimiento del mundo. Si estuvieramos diseñando esta aplicación en Akka, WorldView probablemente sería un var en un Actor con estado.

WorldView acumula los valores de retorno de todos los métodos en las álgebras, y agrega un campo pendiente (pending) para darle seguimiento a peticiones que no han sido satisfechas.

  final case class WorldView(
    backlog: Int,
    agents: Int,
    managed: NonEmptyList[MachineNode],
    alive: Map[MachineNode, Epoch],
    pending: Map[MachineNode, Epoch],
    time: Epoch
  )

Ahora estamos listos para escribir nuestra lógica de negocio, pero necesitamos indicar que dependemos de Droney de Machines.

Podemos escribir la interfaz para nuestra lógica de negocio

  trait DynAgents[F[_]] {
    def initial: F[WorldView]
    def update(old: WorldView): F[WorldView]
    def act(world: WorldView): F[WorldView]
  }

e implementarla con un módulo. Un módulo depende únicamente de otros módulos, álgebras y funciones puras, y puede ser abstraída sobre F. Si una implementación de una interfaz algebraica está acoplada a cierto tipo específico, por ejemplo, IO, se llama un intérprete.

  final class DynAgentsModule[F[_]: Monad](D: Drone[F], M: Machines[F])
    extends DynAgents[F] {

El límite de contexto Monad significa que F es monádico, permitiéndonos usar map, pure y, por supuesto, flatMap por medio de for comprehensions.

Requerimos acceso al álgebra de Drone y Machines como D y M, respectivamente. El uso de una sola letra mayúscula para el nombre es una convención de nombre común para las implementaciones de mónadas y álgebras.

Nuestra lógica de negocio se ejecutará en un ciclo infinito (pseudocódigo)

  state = initial()
  while True:
    state = update(state)
    state = act(state)

3.3.1 initial

En initial llamamos a todos los servicios externos y acumulamos sus resultados en un WorldView. Por default se asigna el campo pending a un Map vacío.

  def initial: F[WorldView] = for {
    db <- D.getBacklog
    da <- D.getAgents
    mm <- M.getManaged
    ma <- M.getAlive
    mt <- M.getTime
  } yield WorldView(db, da, mm, ma, Map.empty, mt)

Recuerde del Capítulo 1 que flatMap (es decir, cuando usamos el generador <-) nos permite operar sobre un valor que se calcula en tiempo de ejecución. Cuando devolvemos un F[_] devolvemos otro programa que será interpretado en tiempo de ejecución, sobre el cual podemos a continuación invocar flatMap. Es de esta manera como encadenamos secuencialmente código con efectos colaterales, al mismo tiempo que somos capaces de proporcionar una implementación pura para las pruebas. PF podría ser descrita como Extreme Mocking.

3.3.2 update

update debería llamar a initial para refrescar nuestra visión del mundo, preservando acciones pending conocidas.

Si un nodo ha cambiado su estado, la quitamos de pending y si una acción pendiente está tomando más de 10 minutos para lograr algo, asumimos que ha fallado y olvidamos que se solicitó trabajo al mismo.

  def update(old: WorldView): F[WorldView] = for {
    snap <- initial
    changed = symdiff(old.alive.keySet, snap.alive.keySet)
    pending = (old.pending -- changed).filterNot {
      case (_, started) => (snap.time - started) >= 10.minutes
    }
    update = snap.copy(pending = pending)
  } yield update

  private def symdiff[T](a: Set[T], b: Set[T]): Set[T] =
    (a union b) -- (a intersect b)

Funciones concretas como .symdiff no requieren intérpretes de prueba, tienen entradas y salidas explícitas, de modo que podríamos mover todo el código puro a métodos autónomos en un object sin estado, que se puede probar en aislamiento. Estamos conformes con probar únicamente los métodos públicos, prefiriendo que nuestra lógica de negocios sea fácil de leer.

3.3.3 act

El método act es ligeramente más complejo, de modo que lo dividiremos en dos partes por claridad: la detección de cúando es necesario tomar una acción, seguida de la ejecución de la acción. Esta simplificación significa que únicamente podemos realizar una acción por invocación, pero esto es razonable porque podemos controlar las invocaciones y podemos escoger ejecutar nuevamente act hasta que no se tome acción alguna.

Escribiremos los detectores de los diferentes escenarios como extractores para WorldView, los cuáles no son más que formas expresivas de escribir condiciones if / else.

Necesitamos agregar agentes a la granja si existe una lista de trabajo pendiente (backlog), no tenemos agentes, no tenemos nodos vivos, y no hay acciones pendientes. Regresamos un nodo candidato que nos gustaría iniciar:

  private object NeedsAgent {
    def unapply(world: WorldView): Option[MachineNode] = world match {
      case WorldView(backlog, 0, managed, alive, pending, _)
           if backlog > 0 && alive.isEmpty && pending.isEmpty
             => Option(managed.head)
      case _ => None
    }
  }

Si no hay backlog, deberíamos detener todos los nodos que están detenidos (no están haciendo ningún trabajo). Sin embargo, dado que Google cobra por hora nosotros únicamente apagamos las máquinas en su minuto 58, para obtener el máximo de nuestro dinero. Devolvemos una lista no vacía de los nodos que hay que detener.

Como una red de seguridad financiera, todos los nodos deben tener un tiempo de vida máximo de 5 horas.

  private object Stale {
    def unapply(world: WorldView): Option[NonEmptyList[MachineNode]] = world match {
      case WorldView(backlog, _, _, alive, pending, time) if alive.nonEmpty =>
        (alive -- pending.keys).collect {
          case (n, started) if backlog == 0 && (time - started).toMinutes % 60 >= 58 => n
          case (n, started) if (time - started) >= 5.hours => n
        }.toList.toNel

      case _ => None
    }
  }

Ahora que hemos detectado los escenario que pueden ocurrir, podemos escribir el método act. Cuando se planea que un nodo se inicie o se detenga, lo agregamos a pending tomando nota del tiempo en el que se programó la acción.

  def act(world: WorldView): F[WorldView] = world match {
    case NeedsAgent(node) =>
      for {
        _ <- M.start(node)
        update = world.copy(pending = Map(node -> world.time))
      } yield update

    case Stale(nodes) =>
      nodes.foldLeftM(world) { (world, n) =>
        for {
          _ <- M.stop(n)
          update = world.copy(pending = world.pending + (n -> world.time))
        } yield update
      }

    case _ => world.pure[F]
  }

Dado que NeedsAgent y Stale no cubren todas las situaciones posibles, requerimos de un case _ que atrape todas las situaciones posibles restantes, y que no haga nada. Recuerde del Capítulo 2 que .pure crea el contexto monádico del for a partir de un valor.

foldLeftM es como foldLeft, pero cada iteración de un fold puede devolver un valor monádico. En nuestro caso, cada iteración del fold devuelve F[WorldView]. El M es por Monádico. Nos encontraremos con más de estos métodos lifted (alzados) que se comportan como uno esperaría, tomando valores monádicos en lugar de valores.

3.4 Unit Tests

El enfoque de FP de escribir aplicaciones es el sueño de un diseñador: delegar la escritura de las implementaciones algebraicas a otros miembros del equipo mientras que se enfoca en lograr que la lógica de negocios cumpla con los requerimientos.

Nuestra aplicación es altamente dependiente de la temporización y de los servicios web de terceros. Si esta fuera una aplicación POO tradicional, crearíamos mocks para todas las invocaciones de métodos, o probaríamos los buzones de salida de los actores. El mocking en PF es equivalente a proporcionar implementaciones alternativas de las álgebras dependientes. Las álgebras ya aislan las partes del sistema que necesitan tener un mock, por ejemplo, interpretándolas de manera distinta en las pruebas unitarias.

Empezaremos con algunos datos de prueba

  object Data {
    val node1   = MachineNode("1243d1af-828f-4ba3-9fc0-a19d86852b5a")
    val node2   = MachineNode("550c4943-229e-47b0-b6be-3d686c5f013f")
    val managed = NonEmptyList(node1, node2)

    val time1: Epoch = epoch"2017-03-03T18:07:00Z"
    val time2: Epoch = epoch"2017-03-03T18:59:00Z" // +52 mins
    val time3: Epoch = epoch"2017-03-03T19:06:00Z" // +59 mins
    val time4: Epoch = epoch"2017-03-03T23:07:00Z" // +5 hours

    val needsAgents = WorldView(5, 0, managed, Map.empty, Map.empty, time1)
  }
  import Data._

Implementamos algebras al extender Drone y Machines con un contexto monádico específico, siendo Id el más simple.

Nuestras implementaciones mock simplemente repiten un WorldView fijo. Ya hemos aislado el estado de nuestro sistema, de modo que podemos usar var para almacenar el estado:

  class Mutable(state: WorldView) {
    var started, stopped: Int = 0

    private val D: Drone[Id] = new Drone[Id] {
      def getBacklog: Int = state.backlog
      def getAgents: Int = state.agents
    }

    private val M: Machines[Id] = new Machines[Id] {
      def getAlive: Map[MachineNode, Epoch] = state.alive
      def getManaged: NonEmptyList[MachineNode] = state.managed
      def getTime: Epoch = state.time
      def start(node: MachineNode): MachineNode = { started += 1 ; node }
      def stop(node: MachineNode): MachineNode = { stopped += 1 ; node }
    }

    val program = new DynAgentsModule[Id](D, M)
  }

Cuando escribimos una prueba unitaria (aquí usando FlatSpec desde ScalaTest), creamos una instancia de Mutable y entonces importamos todos sus miembros.

Tanto nuestro drone y machine implícitos usan el contexto de ejecución Id y por lo tanto interpretar este programa con ellos devuelve un Id[WorldView] sobre el cual podemos hacer aserciones.

En este caso trivial simplemente verificamos que el método initial devuelva el mismo valor que usamos en nuestras implementaciones estáticas:

  "Business Logic" should "generate an initial world view" in {
    val mutable = new Mutable(needsAgents)
    import mutable._

    program.initial shouldBe needsAgents
  }

Entonces podemos crear pruebas más avanzadas de los métodos update y act, ayudándonos a eliminar bugs y refinar los requerimientos:

  it should "remove changed nodes from pending" in {
    val world = WorldView(0, 0, managed, Map(node1 -> time3), Map.empty, time3)
    val mutable = new Mutable(world)
    import mutable._

    val old = world.copy(alive = Map.empty,
                         pending = Map(node1 -> time2),
                         time = time2)
    program.update(old) shouldBe world
  }

  it should "request agents when needed" in {
    val mutable = new Mutable(needsAgents)
    import mutable._

    val expected = needsAgents.copy(
      pending = Map(node1 -> time1)
    )

    program.act(needsAgents) shouldBe expected

    mutable.stopped shouldBe 0
    mutable.started shouldBe 1
  }

Sería aburrido ejecutar el conjunto de pruebas completo. Las siguientes pruebas serían fáciles de implementar usando el mismo enfoque:

  • No solicitar agentes cuando haya pendientes
  • No apagar los agentes si los nodos son muy jóvenes
  • Apagar los agentes cuando no hay backlog y los nodos ocasionarán costos pronto
  • No apague a los agentes si hay acciones pendientes
  • Apague a los agentes cuando no hay backlog si son muy viejos
  • Apague a los agentes, incluso si potencialmente están haciendo trabajo, si son muy viejos
  • Ignore las acciones pendientes que no responden durante las actualizaciones

Todas estas pruebas son síncronas y aisladas al hilo de ejecutor de prueba (que podría estar ejecutando pruebas en paralelo). Si hubieramos diseñado nuestro conjunto de pruebas en Akka, nuestras pruebas estarían sujetas a tiempos de espera arbitrarias y las fallas estarían ocultas en los archivos de registro.

El disparo en la productividad de las pruebas simples para la lógica de negocios no puede ser exagerada. Considere que el 90% del tiempo ocupado por el desarrollador de aplicaciones usado en la interacción con el cliente está en la refinación, actualización y fijación de estas reglas de negocios. Todo lo demás es un detalle de implementación.

3.5 Paralelismo

La aplicación que hemos diseñado ejecuta cada uno de sus métodos algebraicos secuencialmente. Pero hay algunos lugares obvios donde el trabajo puede ejecutarse en paralelo.

3.5.1 initial

En nuestra definición de initial podríamos solicitar toda la información que requerimos a la vez en lugar de hacer una consulta a la vez.

En contraste con flatMap para operaciones secuenciales, Scalaz usa la sintaxis Apply para operaciones paralelas:

  ^^^^(D.getBacklog, D.getAgents, M.getManaged, M.getAlive, M.getTime)

y también puede usar notación infija:

  (D.getBacklog |@| D.getAgents |@| M.getManaged |@| M.getAlive |@| M.getTime)

Si cada una de las operaciones paralelas regresa un valor en el mismo contexto monádico, podemos aplicar una función a los resultados cuando todos ellos sean devueltos. Reescribiendo initial para tomar ventaja de esto:

  def initial: F[WorldView] =
    ^^^^(D.getBacklog, D.getAgents, M.getManaged, M.getAlive, M.getTime) {
      case (db, da, mm, ma, mt) => WorldView(db, da, mm, ma, Map.empty, mt)
    }

3.5.2 act

En la lógica actual para act, estamos deteniéndonos en cada nodo secuencialmente, esperando por el resultado, y entonces procediendo. Pero podríamos detener todos los nodos en paralelo y entonces actualizar nuestra vista del mundo.

Una desventaja de hacerlo de esta manera es que cualquier falla ocasionará que se paren los cómputos antes de actualizar el campo pending. Pero se trata de una concesión razonable dado que nuestra función update manejará el caso cuando un node se apague inesperadamente.

Necesitamos un método que funcione en NonEmptyList que nos permita hacer un map sobre cada elemento en un F[MachineNode], devolviendo un F[NonEmptyList[MachineNode]]. El método se llama traverse, y cuando invoquemos un flatMap sobre este tendremos un NonEmptyList[MachineNode] con el cuál lidiaremos de una manera sencilla:

  for {
    stopped <- nodes.traverse(M.stop)
    updates = stopped.map(_ -> world.time).toList.toMap
    update = world.copy(pending = world.pending ++ updates)
  } yield update

Podría argumentarse, que este código es más fácil de entender que la versión secuencial.

3.6 Summary

  1. Las algebras definen las interfaces entre sistemas.
  2. Los módulos son implementaciones de un álgebra en términos de otras álgebras.
  3. Los intérpretes son implementaciones concretas de un álgebra para una F[_] fija.
  4. Los intérpretes de prueba pueden reemplazar las partes con efectos colaterales de un sistema, proporcionando un grado elevado de cobertura de las pruebas.

4. Datos y funcionalidad

De la POO estamos acostumbrados a pensar en datos y funcionalidad a la vez: las jerarquías de clases llevan métodos, y mediante el uso de traits podemos demandar que existan campos de datos. El polimorfismo en tiempo de ejecución se da en términos de relaciones “is a” (“es un”), requiriendo que las clases hereden de interfaces comunes. Esto puede llegar a complicarse a medida que la base de código crece. Los tipos de datos simples se vuelven obscuros al complicarse con cientos de líneas de métodos, los mixins con traits sufren a causa del orden de inicialización, y las pruebas y mocking de componentes altamente acoplados se convierte en una carga.

La PF toma un enfoque distinto, definiendo los datos y la funcionalidad de manera separada. En este capítulo, se estudiará lo básico de los tipos de datos y las ventajas de restringirnos a un subconjunto del lenguaje de programación Scala. También descubriremos las typeclasses como una forma de lograr polimorfismo en tiempo de compilación: pensando en la funcionalidad de una estructura de datos en términos de “has a” (“tiene un”), más bien que relaciones del tipo “es un”.

4.1 Datos

Los bloques de construcción fundamentales de los tipos son

  • final case class también conocidos como productos
  • sealed abstract class también conocidos como coproductos
  • case object e Int, Double, String, (etc.), valores.

sin métodos o campos más que los parámetros del constructor. Preferimos usar abstract class a trait para lograr mejor compatibilidad binaria y para evitar la mezcla de traits.

El nombre colectivo para productos, coproductos y valores es Tipo de Datos Algebraico (del inglés Algebraic Data Type o ADT).

Formamos la composición de tipos de datos a partir de AND y XOR (OR exclusivos) del álgebra booleana: un producto contiene cada uno de los tipos de los que está compuesto, pero un coproducto puede ser únicamente uno de ellos. Por ejemplo

  • producto: ABC = a AND b AND c
  • coproducto: XYZ = x XOR y XOR z

escrito en Scala

  // values
  case object A
  type B = String
  type C = Int

  // product
  final case class ABC(a: A.type, b: B, c: C)

  // coproduct
  sealed abstract class XYZ
  case object X extends XYZ
  case object Y extends XYZ
  final case class Z(b: B) extends XYZ

4.1.1 Estructuras de datos recursivas

Cuando un ADT se refiere a sí misma, la llamamos Tipo de Datos Algebraico Recursivo.

scalaz.IList, una alternativa segura a List de la librería estándar, es recursiva porque ICons contiene una referencia a IList.:

  sealed abstract class IList[A]
  final case class INil[A]() extends IList[A]
  final case class ICons[A](head: A, tail: IList[A]) extends IList[A]

4.1.2 Funciones sobre ADTs

Las ADTs pueden tener funciones puras

  final case class UserConfiguration(accepts: Int => Boolean)

Pero las ADTs que contienen funciones tienen consigo algunas limitaciones dado que no se pueden traducir de manera perfecta a la JVM. Por ejemplo, los antiguos métodos Serializable, hashCode, equals y toString no se comportan como uno podría razonablemente esperar.

Desgraciadamente, Serializable es usado por frameworks populares, a pesar de que existen alternativas superiores. Una trampa común es olvidar que Serializable podría intentar serializar la cerradura completa de una función, lo que podría ocasionar el fallo total de servidores de producción. Una trampa similar aplica a clases de Java antiguas tales como Throwable, que pueden llevar consigo referencias a objetos arbitrarios.

Exploraremos alternativas a los métodos antiguos cuando discutamos Scalaz en el próximo capítulo, a costa de perder interoperabilidad con algo de código antiguo de Java y Scala.

4.1.3 Exhaustividad

Es importante que usemos sealed abstract class, no simplemente abstract class, cuando definimos un tipo de datos. Sellar una class significa que todos los subtipos deben estar definidos en el mismo archivo, permitiendo que el compilador pueda verificar de manera exhaustiva durante el proceso de empate de patrones y en macros que eliminen el código repetitivo. Por ejemplo,

  scala> sealed abstract class Foo
         final case class Bar(flag: Boolean) extends Foo
         final case object Baz extends Foo

  scala> def thing(foo: Foo) = foo match {
           case Bar(_) => true
         }
  <console>:14: error: match may not be exhaustive.
  It would fail on the following input: Baz
         def thing(foo: Foo) = foo match {
                               ^

Esto muestra al desarrollador qué ha fallado cuando añaden un nuevo producto a la base de código. Aquí se está usando -Xfatal-warnings, porque de otra manera esto es solamente un warning o advertencia.

Sin embargo, el compilador no realizará un chequeo exhaustivo si la clase no está sellada o si existen guardas, por ejemplo

  scala> def thing(foo: Foo) = foo match {
           case Bar(flag) if flag => true
         }

  scala> thing(Baz)
  scala.MatchError: Baz (of class Baz$)
    at .thing(<console>:15)

para conseguir seguridad, es mejor evitar las guardas en los tipos sellados.

La bandera -Xstrict-patmat-analysis se ha propuesto como una mejora al lenguaje para realizar chequeos adicionales durante el proceso de empate de patrones.

4.1.4 Productos alternativos y coproductos

Otra forma de producto es la tupla, que es como una final case class sin etiqueta o nombre.

(A.type, B, C) es equivalente a ABC en el ejemplo de arriba, pero es mejor usar final case class cuando se trate de una parte de alguna ADT porque es algo difícil lidiar con la falta de nombres, y case class tiene mucho mejor performance para valores primitivos.

Otra forma de coproducto es aquella que se logra al anidar tipos Either, por ejemplo

  Either[X.type, Either[Y.type, Z]]

y es equivalente a la clase abstracta sellada XYZ. Se puede lograr una sintáxis más limpia al definir tipos Either anidados al crear un alias de tipo que termine en dos puntos, permitiendo así el uso de notación infija con asociación hacia la derecha:

  type |:[L,R] = Either[L, R]

  X.type |: Y.type |: Z

Esto es útil al crear coproductos anónimos cuando no podemos poner todas las implementaciones en el mismo archivo de código fuente.

  type Accepted = String |: Long |: Boolean

Otra forma de coproducto alternativa es creat un sealed abstract class especial con definiciones final case class que simplemente envuelvan a los tipos deseados:

  sealed abstract class Accepted
  final case class AcceptedString(value: String) extends Accepted
  final case class AcceptedLong(value: Long) extends Accepted
  final case class AcceptedBoolean(value: Boolean) extends Accepted

El empate de patrones en estas formas de coproducto puede ser tediosa, razón por la cual los tipos Unión están explorandose en la siguiente generación del compilador de Scala, Dotty. Macros tales como totalitarian y iotaz existen como formas alternativas de codificar coproductos anónimos.

4.1.5 Transmisión de información

Además de ser contenedores para información de negocios necesaria, los tipos de datos pueden usarse para codificar restricciones. Por ejemplo,

  final case class NonEmptyList[A](head: A, tail: IList[A])

nunca puede estar vacía. Esto hace que scalaz.NonEmptyList sea un tipo de datos útil a pesar de que contiene la misma información que IList.

Los tipos producto con frecuencia contienen tipos que son mucho más generales de lo que en realidad se requiere. En programación orientada a objetos tradicional, la situación se manejaría con validación de datos de entrada a través de aserciones:

  final case class Person(name: String, age: Int) {
    require(name.nonEmpty && age > 0) // breaks Totality, don't do this!
  }

En lugar de hacer esto, podríamos usar el tipo de datos Either para proporcionar Right[Person] para instancias válidas y protegernos de que instancias inválidas se propaguen. Note que el constructor es privado (private):

  final case class Person private(name: String, age: Int)
  object Person {
    def apply(name: String, age: Int): Either[String, Person] = {
      if (name.nonEmpty && age > 0) Right(new Person(name, age))
      else Left(s"bad input: $name, $age")
    }
  }

  def welcome(person: Person): String =
    s"${person.name} you look wonderful at ${person.age}!"

  for {
    person <- Person("", -1)
  } yield welcome(person)
4.1.5.1 Tipos de datos refinados

Una forma limpia de restringir los valores de un tipo general es con la librería refined, que provee una conjunto de restricciones al contenido de los datos. Para instalar refined, agregue la siguiente línea a su archivo build.sbt

  libraryDependencies += "eu.timepit" %% "refined-scalaz" % "0.9.2"

y los siguientes imports

  import eu.timepit.refined
  import refined.api.Refined

Refined permite definir Person usando tipos ad hoc refinados para capturar los requerimientos de manera exacta, escrito A Refined B.

  import refined.numeric.Positive
  import refined.collection.NonEmpty

  final case class Person(
    name: String Refined NonEmpty,
    age: Int Refined Positive
  )

El valor subyacente puede obtenerse con .value. Podemos construir un valor en tiempo de ejecución usando .refineV, que devuelve un Either

  scala> import refined.refineV
  scala> refineV[NonEmpty]("")
  Left(Predicate isEmpty() did not fail.)

  scala> refineV[NonEmpty]("Sam")
  Right(Sam)

Si agregamos el siguiente import

  import refined.auto._

podemos construir valores válidos en tiempo de compilación y obtener errores si el valor provisto no cumple con los requerimientos

  scala> val sam: String Refined NonEmpty = "Sam"
  Sam

  scala> val empty: String Refined NonEmpty = ""
  <console>:21: error: Predicate isEmpty() did not fail.

Es posible codificar requerimientos más completos, por ejemplo podríamos usar la regla MaxSize que con los siguientes imports

  import refined.W
  import refined.boolean.And
  import refined.collection.MaxSize

que captura los requerimientos de que String debe no ser vacía y además tener un máximo de 10 caracteres.

  type Name = NonEmpty And MaxSize[W.`10`.T]

  final case class Person(
    name: String Refined Name,
    age: Int Refined Positive
  )

Es fácil definir requerimientos a la medida que no estén cubiertos por la librería refined. Por ejemplo en drone-dynamic-agents necesitaremos una manera de asegurarnos de que String tenga contenido application/x-www-form-urlencoded. Podríamos crear una regla de Refined usando la librería de expresiones regulares de Java:

  sealed abstract class UrlEncoded
  object UrlEncoded {
    private[this] val valid: Pattern =
      Pattern.compile("\\A(\\p{Alnum}++|[-.*_+=&]++|%\\p{XDigit}{2})*\\z")

    implicit def urlValidate: Validate.Plain[String, UrlEncoded] =
      Validate.fromPredicate(
        s => valid.matcher(s).find(),
        identity,
        new UrlEncoded {}
      )
  }

4.1.6 Simple de compartir

Al no proporcionar ninguna funcionalidad, las ADTs pueden tener un conjunto mínimo de dependencias. Esto hace que sean fáciles de publicar y de compartir con otros desarrolladores. Al usar un lenguaje de modelado de datos simple, se hace posible la interacción con equipos interdisciplinarios, tales como DBAs, desarrolladores de interfaces de usuario y analistas de negocios, usando el código fuente actual en lugar de un documento escrito manualmente como la fuente de la verdad.

Más aún, las herramientas pueden ser escritas más fácilmente para producir o consumir esquemas de otros lenguajes de programación y protocolos físicos.

4.1.7 Contando la complejidad

La complejidad de un tipo de datos es la cuenta de los valores que puede tener. Un buen tipo de datos tiene la cantidad mínima de complejidad requerida para contener la información que transmite, y nada más.

Los valores tienen una complejidad inherente:

  • Unit tien un solo valor (razón por la cual se llama “unit”)
  • Boolean tiene dos valores
  • Int tiene 4,294,967,295 valores
  • String tiene valores infinitos, de manera práctica

Para encontrar la complejidad de un producto, multiplicamos la complejidad de cada parte

  • (Boolean, Boolean) tiene 4 valores, (2 * 2)
  • (Boolean, Boolean, Boolean) tiene 8 valores (2 * 2 * 2)

Para encontrar la complejidad de un coproducto, sumamos la complejidad de cada parte

  • (Boolean |: Boolean) tiene 4 valores (2 + 2)
  • (Boolean |: Boolean |: Boolean) tiene 6 valores (2 + 2 + 2)

Para encontrar la complejidad de un ADT con un parámetro de tipo, multiplique cada parte por la complejidad del parámetro de tipo:

  • Option[Boolean] tiene 3 valores, Some[Boolean] y None. (2 + 1).

En la PF, las funciones son totales y deben devolver el mismo valor para cada entrada, no una Exception. Minimizar la complejidad de las entradas y las salidas es la mejor forma de lograr totalidad. Como regla de oro, es un signo de una función con mal dise;o cuando la complejidad del valor de retorno es más grande que el producto de sus entradas: es una fuente de entropía.

La complejidad de una función total es el número de funciones posibles que pueden satisfacer la signatura del tipo: la salida a la potencia de la entrada.

  • Unit => Boolean tiene complejidad 2
  • Boolean => Boolean tiene complejidad 4
  • Option[Boolean] => Option[Boolean] tiene complejidad 27
  • Boolean => Int es como un quintillón, aproximándose a un sextillón.
  • Int => Boolean es tan grande que si a todas las implementaciones se les asignara un número único, cada una requeriría 4 gigabytes para representarla.

En la práctica, Int => Boolean será algo tan simple como isOdd, isEven o un BitSet disperso. Esta función, cuando se usa en una ADT, será mejor reemplazarla con un coproducto que asigne un nombre al conjunto de funciones que son relevantes.

Cuando la complejidad es “infinito a la entrada, infinito a la salida” deberíamos introducir tipos de datos que restrictivos y validaciones más cercanos al punto de entrada con Refined de la sección anterior.

La habilidad de contar la complejidad de una signatura de tipo tiene otra aplicación práctica: podemos encontrar signaturas de tipo más simples usando Álgebra de la secundaria! Para ir de una signatura de tipos al álgebra de las complejidades, simplemente reemplace

  • Either[A, B] con a + b
  • (A, B) con a * b
  • A => B con b ^ a

hacer algunos rearreglos, y convertir de vuelta. Por ejemplo, digamos que hemos diseñado un framework basado en callbacks y que hemos logrado llegar a la situación donde tenemos la siguiente signatura de tipos:

  (A => C) => ((B => C) => C)

Podríamos convertir y reordenar

  (c ^ (c ^ b)) ^ (c ^ a)
  = c ^ ((c ^ b) * (c ^ a))
  = c ^ (c ^ (a + b))

y convertir de vuelta a tipos y obtener

  (Either[A, B] => C) => C

que es mucho más simple: sólo es necesario pedir a los usuarios de nuestro framework que proporcionen un Either[A, B] => C.

La misma línea de razonamiento puede ser usada para probar que

  A => B => C

es equivalente a

  (A, B) => C

que también se conoce como Currying.

4.1.8 Prefiera coproducto en vez de producto

Un problema de modelado típico que surge con frecuencia es el que tenemos cuando hay parámetros de configuración mutuamente exclusivos a, b, y c. El producto (a: Boolean, b: Boolean, c: Boolean) tiene complejidad 8, mientras que el coproducto

  sealed abstract class Config
  object Config {
    case object A extends Config
    case object B extends Config
    case object C extends Config
  }

tiene una complejidad de 3. Es mejor modelar estos parámetros de configuración como un coproducto más bien que permitir la existencia de 5 estados inválidos.

La complejidad de un tipo de datos también tiene implicaciones a la hora de hacer pruebas. Es prácticamente imposible probar cada entrada a una función, pero es fácil probar una muestra de los valores con el framework para hacer pruebas de propiedades con Scalacheck.

Si una muestra aleatoria de un tipo de datos tiene una probabilidad baja de ser válida, tenemos una señal de que los datos están modelados incorrectamente.

4.1.9 Optimizaciones

Una gran ventaja de usar un conjunto simplificado del lenguaje Scala para representar tipos de datos es que el tooling puede optimizar la representación en bytecodes de la JVM.

Por ejemplo, podríamos empacar campos Boolean y Option en un Array[Byte], hacer un caché con los valores, memoizar hashCode, optimizar equals, usar sentencias @switch al momento de realizar empate de patrones, y mucho más.

Estas optimizaciones no son aplicables a jerarquías de clases en POO que tal vez manipulen el estado, lancen excepciones, o proporcionen implementaciones ad hoc de los métodos.

4.2 Funcionalidad

Las funciones puras están definidas típicamente como métodos en un object.

  package object math {
    def sin(x: Double): Double = java.lang.Math.sin(x)
    ...
  }

  math.sin(1.0)

Sin embargo, el uso de métodos en objects puede ser un poco torpe, dado que se lee de dentro hacia afuera, y no de izquierda a derecha. Además, una función en un object se roba el namespace. Si tuvieramos que definir sin(t: T) en algún otro lugar, tendríamos errores por referencias ambiguas. Este es el mismo problema de los métodos estáticos de Java vs los métodos de clase.

Con ayuda de la característica del lenguaje implicit class, (también conocida como metodología de extensión o sintaxis), y un poco de código tedioso, es posible lograr un estilo más familiar:

  scala> implicit class DoubleOps(x: Double) {
           def sin: Double = math.sin(x)
         }

  scala> (1.0).sin
  res: Double = 0.8414709848078965

Con frecuencia es mejor saltarnos la definición de un object e ir directo con una implicit class, manteniendo el código repetitivo al mínimo:

  implicit class DoubleOps(x: Double) {
    def sin: Double = java.lang.Math.sin(x)
  }

4.2.1 Funciones polimórficas

La clase de funciones más comunes es la función polimórfica, que vive en una typeclass. Una typeclass es un trait que:

  • no tiene estado
  • tiene un parámetro de tipo
  • tiene al menos un método abstracto (combinador primitivo)
  • puede contener métodos generalizadores (combinadores derivados)
  • puede extender a otros typeclases

Sólo puede existir una implementación de una typeclass para un parámetro de tipo específico correspondiente, una propiedad conocida también como coherencia de typeclass. Las typeclasses se ven superficialmente similares a las interfaces algebraicas del capítulo anterior, pero las álgebras no tienen que ser coherentes.

Las typeclasses son usadas en la librería estándar de Scala. Exploraremos una versión simplificada de scala.math.Numeric para demostrar el principio:

  trait Ordering[T] {
    def compare(x: T, y: T): Int

    def lt(x: T, y: T): Boolean = compare(x, y) < 0
    def gt(x: T, y: T): Boolean = compare(x, y) > 0
  }

  trait Numeric[T] extends Ordering[T] {
    def plus(x: T, y: T): T
    def times(x: T, y: T): T
    def negate(x: T): T
    def zero: T

    def abs(x: T): T = if (lt(x, zero)) negate(x) else x
  }

Podemos ver las características clave de una typeclass en acción:

  • no hay estado
  • Ordering y Numeric tienen un parámetro de tipo T
  • Ordering tiene un compare abstracto, y Numeric tiene un plus, times, negate y zero abstractos.
  • Ordering define un lt generalizado, y gt basados en compare, Numeric define abs en términos de lt, negate y zero.
  • Numeric extiende Ordering.

Ahora podemos escribir funciones para tipos que “tengan un(a)” typeclass Numeric:

  def signOfTheTimes[T](t: T)(implicit N: Numeric[T]): T = {
    import N._
    times(negate(abs(t)), t)
  }

Ya no dependemos de la jerarquía POO de nuestros tipos de entrada, es decir, no demandamos que nuestra entrada sea (“is a”) Numeric, lo cual es vitalmente importante si deseamos soportar clases de terceros que no podemos redefinir.

Otra ventaja de las typeclasses es que la asociación de funcionalidad a los datos se da en tiempo de compilación, en oposición del despacho dinámico en tiempo de ejecución de la POO.

Por ejemplo, mientras que la clase List solo puede tener una implementación de un método, una typeclasss nos permite tener diferentes implementaciones dependiendo del contenido de List y por lo tanto nos permite descargar el trabajo al tiempo de compilación en lugar de dejarlo al tiempo de ejecución.

4.2.2 Sintaxis

La sintaxis para escribir signOfTheTimes es un poco torpe, y hay algunas cosas que podemos hacer para limpiarla.

Los usuarios finales de nuestro código, preferirán que nuestro métod use context bounds, dado que la signatura se lee limpiamente como “toma una T que tiene un Numeric

  def signOfTheTimes[T: Numeric](t: T): T = ...

pero ahora tenemos que usar implicitly[Numeric[T]] en todos lados. Al definir el código repetitivo en el companion object de la typeclass

  object Numeric {
    def apply[T](implicit numeric: Numeric[T]): Numeric[T] = numeric
  }

podemos obtener el implícito con menos ruido

  def signOfTheTimes[T: Numeric](t: T): T = {
    val N = Numeric[T]
    import N._
    times(negate(abs(t)), t)
  }

Pero es todavía peor para nosotros como los implementadores. Tenemos el problema sintáctico de métodos estáticos afuera vs métodos de la clase. Una forma de lidiar con esta situación es mediante introducir ops en el companion de la typeclass:

  object Numeric {
    def apply[T](implicit numeric: Numeric[T]): Numeric[T] = numeric

    object ops {
      implicit class NumericOps[T](t: T)(implicit N: Numeric[T]) {
        def +(o: T): T = N.plus(t, o)
        def *(o: T): T = N.times(t, o)
        def unary_-: T = N.negate(t)
        def abs: T = N.abs(t)

        // duplicated from Ordering.ops
        def <(o: T): T = N.lt(t, o)
        def >(o: T): T = N.gt(t, o)
      }
    }
  }

Note que -x se expande a x.unary_- mediante las conveniencias sintácticas del compilador, razón por la cual definimos unary_- como un método de extensión. Podemos ahora escribir la forma mucho más clara:

  import Numeric.ops._
  def signOfTheTimes[T: Numeric](t: T): T = -(t.abs) * t

La buena noticia es que nunca necesitamos escribir este tipo de código repetitivo porque Simulacrum proporciona una anotación @typeclass que genera automáticamente los métodos apply y ops. Incluso nos permite definir nombres alternativos (normalmente simbólicos) para métodos comunes. Mostrando el código completo:

  import simulacrum._

  @typeclass trait Ordering[T] {
    def compare(x: T, y: T): Int
    @op("<") def lt(x: T, y: T): Boolean = compare(x, y) < 0
    @op(">") def gt(x: T, y: T): Boolean = compare(x, y) > 0
  }

  @typeclass trait Numeric[T] extends Ordering[T] {
    @op("+") def plus(x: T, y: T): T
    @op("*") def times(x: T, y: T): T
    @op("unary_-") def negate(x: T): T
    def zero: T
    def abs(x: T): T = if (lt(x, zero)) negate(x) else x
  }

  import Numeric.ops._
  def signOfTheTimes[T: Numeric](t: T): T = -(t.abs) * t

Cuando existe un operador simbólico personalizado (@op), se puede pronunciar como el nombre del método, por ejemplo, < se pronuncia “menor que”, y no “paréntesis angular izquierdo”.

4.2.3 Instancias

Las instancias de Numeric (que también son instancias de Ordering) se definen como un implicit val que extiende a la typeclass, y que proporciona implementaciones optimizadas pra los métodos generalizados:

  implicit val NumericDouble: Numeric[Double] = new Numeric[Double] {
    def plus(x: Double, y: Double): Double = x + y
    def times(x: Double, y: Double): Double = x * y
    def negate(x: Double): Double = -x
    def zero: Double = 0.0
    def compare(x: Double, y: Double): Int = java.lang.Double.compare(x, y)

    // optimised
    override def lt(x: Double, y: Double): Boolean = x < y
    override def gt(x: Double, y: Double): Boolean = x > y
    override def abs(x: Double): Double = java.lang.Math.abs(x)
  }

Aunque estamos usando +, *, unary_- , < y > aquí, que están en el ops (y podría ser un loop infinito!) estos métodos ya existen en Double. Los métodos de una clase siempre se usan en preferencia a los métodos de extensión. En realidad, el compilador de Scala realiza un manejo especial de los tipos primitivos y convierte estas llamadas en instrucciones directas dadd, dmul, dcmpl, y dcmpg, respectivamente.

También podemos implementar Numeric para la clase de Java BigDecimal (evite `scala.BigDecimal, it is fundamentally broken)

  import java.math.{ BigDecimal => BD }

  implicit val NumericBD: Numeric[BD] = new Numeric[BD] {
    def plus(x: BD, y: BD): BD = x.add(y)
    def times(x: BD, y: BD): BD = x.multiply(y)
    def negate(x: BD): BD = x.negate
    def zero: BD = BD.ZERO
    def compare(x: BD, y: BD): Int = x.compareTo(y)
  }

Podríamos crear nuestra propia estructura de datos para números complejos:

  final case class Complex[T](r: T, i: T)

y construir un Numeric[Complex[T]] si Numeric[T] existe. Dado que estas instancias dependen del parámetro de tipo, es un def y no un val.

  implicit def numericComplex[T: Numeric]: Numeric[Complex[T]] =
    new Numeric[Complex[T]] {
      type CT = Complex[T]
      def plus(x: CT, y: CT): CT = Complex(x.r + y.r, x.i + y.i)
      def times(x: CT, y: CT): CT =
        Complex(x.r * y.r + (-x.i * y.i), x.r * y.i + x.i * y.r)
      def negate(x: CT): CT = Complex(-x.r, -x.i)
      def zero: CT = Complex(Numeric[T].zero, Numeric[T].zero)
      def compare(x: CT, y: CT): Int = {
        val real = (Numeric[T].compare(x.r, y.r))
        if (real != 0) real
        else Numeric[T].compare(x.i, y.i)
      }
    }

El lector observador podrá notar que abs no es lo que un matemático esperaría. El valor de retorno correcto para abs debería ser de tipo T, y no Complex[T].

scala.math.Numeric intenta hacer muchas cosas y no generaliza adecuadamente más allá de los números reales. esta es una buena lección que muestra que typeclasses pequeñas, bien definidas, son con frecuencia mejores que una colección monolítica de características demasiado específicas.

4.2.4 Resolución implícita

Ya hemos discutido los implícitos bastante: esta sección es para clarificar qué son los implícitos y de cómo funcionan.

Los parámetros implícitos se usan cuando un método solicita que una instancia única de un tipo particular exista en el ámbito/alcance implícito (implicit scope) del que realiza la llamada al método, con una sintáxis especial para las instancias de las typeclasses. Los parámetros implícitos son una manera limpia de pasar configuración a una aplicación.

En este ejemplo, foo requiere que las instancias de typeclass de Numeric y Typeable estén disponibles para A, así como un objecto implícito Handler que toma dos parámetros de tipo

  def foo[A: Numeric: Typeable](implicit A: Handler[String, A]) = ...

Las conversiones implícitas se usan cuando existe un implicit def. Un uso posible de dichas conversiones implícitas es para habilitar la metodología de extensión. Cuando el compilador está calculando cómo invocar un método, primero verifica si el método existe en el tipo, luego en sus ancestros (reglas similares a las de Java). Si no encuentra un emparejamiento, entonces buscará en conversiones implícitas en el alcance implícito, y entonces buscará métodos sobre esos tipos.

Otro uso de las conversiones implícitas es en la derivación de typeclasses. En la sección anterior escribimos una implicit def que construía un Numeric[Complex[T]] si existía un Numeric[T] en el alcance implícito. Es posible encadenar juntas muchas implicit def (incluyendo las que pudieran invocarse de manera recursiva) y esto forma la bese de la programación con tipos, permitiendo que cálculos se realicen en tiempo de compilación más bien que en tiempo de ejecución.

El pegamento que combina parámetros implícitos (receptores) con conversiones implícitas (proveedores) es la resolución implícita.

Primero, se buscan implícitos en el ámbito/alcance de variables normales, en orden:

  • alcance local, incluyendo imports dentro del alcance (por ejemplo, en el bloque o el método)
  • alcance externo, incluyendo imports detro del alcance (por ejemplo miembros de la clase)
  • ancestros (por ejemplo miembros en la super clase)
  • el objeto paquete actual
  • el objeto paquete de los ancestros (cuando se usan paquetes anidados)
  • los imports del archivo

Si se falla en encontrar un emparejamiento, se busca en el alcance especial, la cual se realiza para encontrar instancias implícitas dentro del companion del tipo, su paquete de objetos, objetos externos (si están anidados), y entonces se repite para sus ancestros. Esto se realiza, en orden, para:

  • el parámetro de tipo dado
  • el parámetro de tipo esperado
  • el parámetro de tipo (si hay alguno)

Si se encuentran dos implícitos que emparejen en la misma fase de resolución implícita, se lanza un error de implícito ambiguo (ambiguous implicit).

Los implícitos con frecuencia se definen un un `trait, el cual se extiende con un objecto. Esto es para controlar la prioridad de un implícito con referencia a otro más específico, para evitar implícitos ambiguos.

La Especificación del lenguaje de Scala es un tanto vaga para los casos poco comunes, y en realidad la implementación del compilador es el estándar real. Hay algunas reglas de oro que usaremos en este libro, por ejemplo preferir implicit val en lugar de implicit object a pesar de la tentación de teclear menos. Es una capricho de la resolución implícita que los implicit object en los objetos companion no sean tratados de la misma manera que un implicit val.

La resolución implícita se queda corta cuando existe una jerarquía de typeclasses, como Ordering y Numeric. Si escribimos una función que tome un implícito de Ordering, y la llamamos para un tipo primitivo que tiene una instancia de Numeric definido en el companion Numeric, el compilador fallará en encontrarlo.

La resolución implícita es particularmente mala si se usan aliases de tipo donde la forma de los parámetros implícitos son cambiados. Por ejemplo un parámetro implícito usando un alias tal como type Values[A] = List[Option[A]] probablemente fallará al encontrar implícitos definidos como un List[Option[A]] porque la forma se cambió de una cosa de cosas de A a una cosa de As.

4.3 Modelling OAuth2

Terminaremos este capítulo con un ejemplo práctico de modelado de datos y derivación de typeclasses, combinado con el diseño del álgebra/módulo del capítulo anterior.

En nuestra aplicación drone-dynamic-agents, debemos comunicarnos con Drone y Google Cloud usando JSON sobre REST. Ambos servicios usan OAuth2 para la autenticación.

Hay muchas maneras de interpretar OAuth2, pero nos enfocaremos en la versión que funciona para Google Cloud (la versión para Drone es aún más sencilla).

4.3.1 Descripción

Cada aplicación de Google Cloud necesita tener una OAuth 2.0 Client key configurada en

  https://console.developers.google.com/apis/credentials?project={PROJECT_ID}

y así se obtiene un Client ID y un Client secret.

La aplicación puede entonces un código para usarse una sola vez, al hacer que el usuario realice una Authorization Request (petición de autorización) en su navegador (sí, de verdad, en su navegador). Necesitamos hacer que esta página se abra en el navegador:

  https://accounts.google.com/o/oauth2/v2/auth?\
    redirect_uri={CALLBACK_URI}&\
    prompt=consent&\
    response_type=code&\
    scope={SCOPE}&\
    access_type=offline&\
    client_id={CLIENT_ID}

El código se entrega al {CALLBACK_URI} en una petición GET. Para capturarla en nuestra aplicación, necesitamos un servidor web escuchando en localhost.

Una vez que tenemos el código, podemos realizar una petición Acess Token Request:

  POST /oauth2/v4/token HTTP/1.1
  Host: www.googleapis.com
  Content-length: {CONTENT_LENGTH}
  content-type: application/x-www-form-urlencoded
  user-agent: google-oauth-playground
  code={CODE}&\
    redirect_uri={CALLBACK_URI}&\
    client_id={CLIENT_ID}&\
    client_secret={CLIENT_SECRET}&\
    scope={SCOPE}&\
    grant_type=authorization_code

y entonces se tiene una respuesta JSON en el payload

  {
    "access_token": "BEARER_TOKEN",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "REFRESH_TOKEN"
  }

Todas las peticiones al servidor, provenientes del usuario, deben incluir el encabezado

  Authorization: Bearer BEARER_TOKEN

después de sustituir el BEARER_TOKEN real.

Google hace que expiren todas excepto las 50 más recientes bearer tokens, de modo que los tiempos de expiración son únicamente una guía. Los refresh tokens persisten entre sesiones y pueden expirar manualmente por el usuario. Por lo tanto podemos tener una aplicación de configuración que se use una sola vez para obtener el refresh token y entonces incluirlo como una configuración para la instalación del usuario del servidor headless.

Drone no implementa el endpoint /auth, o el refresco, y simplemente proporciona un BEARER_TOKEN a través de su interfaz de usuario.

4.3.2 Datos

El primer paso es modelar los datos necesarios para OAuth2. Creamos un ADT con los campos teniendo el mismo nombre como es requerido por el servidor OAuth2. Usaremos String y Long por brevedad, pero podríamos usar tipos refinados

  import refined.api.Refined
  import refined.string.Url

  final case class AuthRequest(
    redirect_uri: String Refined Url,
    scope: String,
    client_id: String,
    prompt: String = "consent",
    response_type: String = "code",
    access_type: String = "offline"
  )
  final case class AccessRequest(
    code: String,
    redirect_uri: String Refined Url,
    client_id: String,
    client_secret: String,
    scope: String = "",
    grant_type: String = "authorization_code"
  )
  final case class AccessResponse(
    access_token: String,
    token_type: String,
    expires_in: Long,
    refresh_token: String
  )
  final case class RefreshRequest(
    client_secret: String,
    refresh_token: String,
    client_id: String,
    grant_type: String = "refresh_token"
  )
  final case class RefreshResponse(
    access_token: String,
    token_type: String,
    expires_in: Long
  )

4.3.3 Funcionalidad

Es necesario serializar las clases de datos que definimos en la sección previa en JSON, URL, y las formas codificadas POST. Dado que esto requiere de polimorfismo, necesitaremos typeclasses.

jsonformat es una librería JSON simple que estudiaremos en más detalle en un capítulo posterior, dado que ha sido escrita con principios de PF y facilidad de lectura como sus objetivos de diseño primario. Consiste de una AST JSON y typeclasses de codificadores/decodificadores:

  package jsonformat

  sealed abstract class JsValue
  final case object JsNull                                    extends JsValue
  final case class JsObject(fields: IList[(String, JsValue)]) extends JsValue
  final case class JsArray(elements: IList[JsValue])          extends JsValue
  final case class JsBoolean(value: Boolean)                  extends JsValue
  final case class JsString(value: String)                    extends JsValue
  final case class JsDouble(value: Double)                    extends JsValue
  final case class JsInteger(value: Long)                     extends JsValue

  @typeclass trait JsEncoder[A] {
    def toJson(obj: A): JsValue
  }

  @typeclass trait JsDecoder[A] {
    def fromJson(json: JsValue): String \/ A
  }

Necesitamos instancias de JsDecoder[AccessResponse] y JsDecoder[RefreshResponse]. Podemos hacer esto mediante el uso de una función auxiliar:

  implicit class JsValueOps(j: JsValue) {
    def getAs[A: JsDecoder](key: String): String \/ A = ...
  }

Ponemos las instancias de los companions en nuestros tipos de datos, de modo que siempre estén en el alcance/ámbito implícito:

  import jsonformat._, JsDecoder.ops._

  object AccessResponse {
    implicit val json: JsDecoder[AccessResponse] = j =>
      for {
        acc <- j.getAs[String]("access_token")
        tpe <- j.getAs[String]("token_type")
        exp <- j.getAs[Long]("expires_in")
        ref <- j.getAs[String]("refresh_token")
      } yield AccessResponse(acc, tpe, exp, ref)
  }

  object RefreshResponse {
    implicit val json: JsDecoder[RefreshResponse] = j =>
      for {
        acc <- j.getAs[String]("access_token")
        tpe <- j.getAs[String]("token_type")
        exp <- j.getAs[Long]("expires_in")
      } yield RefreshResponse(acc, tpe, exp)
  }

Podemos entonces parsear una cadena en un AccessResponse o una RefreshResponse

  scala> import jsonformat._, JsDecoder.ops._
  scala> val json = JsParser("""
                       {
                         "access_token": "BEARER_TOKEN",
                         "token_type": "Bearer",
                         "expires_in": 3600,
                         "refresh_token": "REFRESH_TOKEN"
                       }
                       """)

  scala> json.map(_.as[AccessResponse])
  AccessResponse(BEARER_TOKEN,Bearer,3600,REFRESH_TOKEN)

Es necesario escribir nuestra propia typeclass para codificación de URL y POST. El siguiente fragmento de código es un diseño razonable:

  // URL query key=value pairs, in un-encoded form.
  final case class UrlQuery(params: List[(String, String)])

  @typeclass trait UrlQueryWriter[A] {
    def toUrlQuery(a: A): UrlQuery
  }

  @typeclass trait UrlEncodedWriter[A] {
    def toUrlEncoded(a: A): String Refined UrlEncoded
  }

Es necesario proporcionar instancias de una typeclass para los tipos básicos:

  import java.net.URLEncoder

  object UrlEncodedWriter {
    implicit val encoded: UrlEncodedWriter[String Refined UrlEncoded] = identity

    implicit val string: UrlEncodedWriter[String] =
      (s => Refined.unsafeApply(URLEncoder.encode(s, "UTF-8")))

    implicit val long: UrlEncodedWriter[Long] =
      (s => Refined.unsafeApply(s.toString))

    implicit def ilist[K: UrlEncodedWriter, V: UrlEncodedWriter]
      : UrlEncodedWriter[IList[(K, V)]] = { m =>
      val raw = m.map {
        case (k, v) => k.toUrlEncoded.value + "=" + v.toUrlEncoded.value
      }.intercalate("&")
      Refined.unsafeApply(raw) // by deduction
    }

  }

Usamos Refined.unsafeApply cuando podemos deducir lógicamente que el contenido de una cadena ya está codificado como una url, dejando de hacer verificaciones adicionales.

ilist es un ejemplo de derivación de typeclass simple, así como derivamos Numeric[Complex] de la representación numérica subyacente. El método .intercalate es como .mkString pero más general.

En un capítulo dedicado a la derivación de typeclasses calcularemos instancias de UrlQueryWriter automáticamente, y también limpiaremos lo que ya hemos escrito, pero por ahora escribiremos el código repetitivo para los tipos que deseemos convertir:

  import UrlEncodedWriter.ops._
  object AuthRequest {
    implicit val query: UrlQueryWriter[AuthRequest] = { a =>
      UriQuery(List(
        ("redirect_uri"  -> a.redirect_uri.value),
        ("scope"         -> a.scope),
        ("client_id"     -> a.client_id),
        ("prompt"        -> a.prompt),
        ("response_type" -> a.response_type),
        ("access_type"   -> a.access_type))
    }
  }
  object AccessRequest {
    implicit val encoded: UrlEncodedWriter[AccessRequest] = { a =>
      List(
        "code"          -> a.code.toUrlEncoded,
        "redirect_uri"  -> a.redirect_uri.toUrlEncoded,
        "client_id"     -> a.client_id.toUrlEncoded,
        "client_secret" -> a.client_secret.toUrlEncoded,
        "scope"         -> a.scope.toUrlEncoded,
        "grant_type"    -> a.grant_type.toUrlEncoded
      ).toUrlEncoded
    }
  }
  object RefreshRequest {
    implicit val encoded: UrlEncodedWriter[RefreshRequest] = { r =>
      List(
        "client_secret" -> r.client_secret.toUrlEncoded,
        "refresh_token" -> r.refresh_token.toUrlEncoded,
        "client_id"     -> r.client_id.toUrlEncoded,
        "grant_type"    -> r.grant_type.toUrlEncoded
      ).toUrlEncoded
    }
  }

4.3.4 Módulo

Esto concluye con el modelado de los datos y funcionalidad requeridos para implementar OAuth2. Recuerde del capítulo anterior que definimos componentes que necesitan interactuar con el mundo como álgebras, y debemos definir la lógica de negocio en un módulo, de modo que pueda ser probada por completo.

Definimos nuestra dependencia de las álgebras, y usamos los límites de contexto para mostrar que nuestras respuestas deben tener un JsDecoder y nuestro payload POST debe tener un UrlEncodedWriter:

  trait JsonClient[F[_]] {
    def get[A: JsDecoder](
      uri: String Refined Url,
      headers: IList[(String, String)]
    ): F[A]

    def post[P: UrlEncodedWriter, A: JsDecoder](
      uri: String Refined Url,
      payload: P,
      headers: IList[(String, String]
    ): F[A]
  }

Note que nosotros únicamente definimos el camino fácil en la API JsonClient. Veremos como lidiar con los errores en un capítulo posterior.

Obtener un CodeToken de un servidor OAuth2 de Google envuelve

  1. iniciar un servidor HTTP en la máquina local, y obtener su número de puerto.
  2. hacer que el usuario abra una página web en su navegador, lo que les permite identificarse con sus credenciales de Google y autorizar la aplicación, con una redirección de vuelta a la máquina local.
  3. capturar el código, informando al usuario de los siguientes pasos, y cerrar el servidor HTTP.

Podemos modelar esto con tres métodos en una álgebra UserInteraction

  final case class CodeToken(token: String, redirect_uri: String Refined Url)

  trait UserInteraction[F[_]] {
    def start: F[String Refined Url]
    def open(uri: String Refined Url): F[Unit]
    def stop: F[CodeToken]
  }

Casi suena fácil cuando lo escribimos de esta manera.

También requerimos de un álgebra para abstraer el sistema local de tiempo

  trait LocalClock[F[_]] {
    def now: F[Epoch]
  }

E introducimos los tipos de datos que usaremos en la lógica de refresco

  final case class ServerConfig(
    auth: String Refined Url,
    access: String Refined Url,
    refresh: String Refined Url,
    scope: String,
    clientId: String,
    clientSecret: String
  )
  final case class RefreshToken(token: String)
  final case class BearerToken(token: String, expires: Epoch)

y ahora podemos escribir un módulo cliente para OAuth2:

  import http.encoding.UrlQueryWriter.ops._

  class OAuth2Client[F[_]: Monad](
    config: ServerConfig
  )(
    user: UserInteraction[F],
    client: JsonClient[F],
    clock: LocalClock[F]
  ) {
    def authenticate: F[CodeToken] =
      for {
        callback <- user.start
        params   = AuthRequest(callback, config.scope, config.clientId)
        _        <- user.open(params.toUrlQuery.forUrl(config.auth))
        code     <- user.stop
      } yield code

    def access(code: CodeToken): F[(RefreshToken, BearerToken)] =
      for {
        request <- AccessRequest(code.token,
                                 code.redirect_uri,
                                 config.clientId,
                                 config.clientSecret).pure[F]
        msg     <- client.post[AccessRequest, AccessResponse](
                     config.access, request)
        time    <- clock.now
        expires = time + msg.expires_in.seconds
        refresh = RefreshToken(msg.refresh_token)
        bearer  = BearerToken(msg.access_token, expires)
      } yield (refresh, bearer)

    def bearer(refresh: RefreshToken): F[BearerToken] =
      for {
        request <- RefreshRequest(config.clientSecret,
                                  refresh.token,
                                  config.clientId).pure[F]
        msg     <- client.post[RefreshRequest, RefreshResponse](
                     config.refresh, request)
        time    <- clock.now
        expires = time + msg.expires_in.seconds
        bearer  = BearerToken(msg.access_token, expires)
      } yield bearer
  }

4.4 Resumen

  • Los ADTs (tipos de datos algebraicos) están definidos como productos (final case class) y coproductos (sealed abstract class).
  • Los tipos refined hacen cumplir las invariantes/restricciones sobre los valores.
  • Las funciones concretas pueden definirse en una clase implícita, para mantener el flujo de izquierda a derecha.
  • Las funciones polimórficas están definidas en typeclasses. La funcionalidad se proporciona por medio de límites de contexto (“has a”), más bién que por medio de jerarquías de clases.
  • Las instancias de typeclasses son implementaciones de una typeclass.
  • @simulacrum.typeclass genera .ops en el companion, proporcionando sintaxis conveniente para las funciones de la typeclass.
  • La derivación de typeclasses es una composición en tiempo de compilación de instancias de typeclass.

5. Scalaz Typeclasses

En este capítulo tendremos un tour de la mayoría de las typeclasses en scalaz-core. No usamos todas en drone-dynamic-agents de modo que daremos ejemplos standalone cuando sea apropiado.

Ha habido críticas con respecto a los nombres usados en Scalaz, y en la programación funcional en general. La mayoría de los nombres siguen las convenciones introducidas en el lenguaje de programación funcional Haskell, basándose en la Teoría de las Categorías. Siéntase libre de crear type aliases si los verbos basados en la funcionalidad primaria son más fáciles de recordar cuando esté aprendiendo (por ejemplo, Mappable, Pureable, FlatMappable).

Antes de introducir la jerarquía de typeclasses, echaremos un vistazo a los cuatro métodos más importantes desde una perspectiva de control de flujo: los métodos que usaremos más en las aplicaciones típicas de PF:

Typeclass Method Desde Dado Hacia
Functor map F[A] A => B F[B]
Applicative pure A   F[A]
Monad flatMap F[A] A => F[B] F[B]
Traverse sequence F[G[A]]   G[F[A]]

Sabemos que las operaciones que regresan un F[_] pueden ejecutarse secuencialmente en una comprensión for mediante .flatMap definida en su Monad[F]. El contexto F[_] puede considerarse como un contenedor para un efecto intencional que tiene una A como la salida: flatMap nos permite generar nuevos efectos F[B] en tiempo de ejecución basándose en los resultados de evaluar efectos previos.

Por supuesto, no todos los constructores de tipo F[_] tienen efectos, incluso si tienen una Monad[_]. Con frecuencia son estructuras de datos. Mediante el uso de la abstracción menos específica, podemos reusar código para List, Either, Future y más.

Si únicamente necesitamos transformar la salida de un F[_], esto simplemente es un map, introducido por Functor. En el capítulo 3, ejecutamos efectos en paralelo mediante la creación de un producto y realizando un mapeo sobre ellos. En programación funcional, los cómputos paralelizables son considerados menos poderosos que los secuenciales.

Entre Monad y Functor se encuentra Applicative, que define pure que nos permite alzar un valor en un efecto, o la creación de una estructura de datos a partir de un único valor.

.sequence es útil para rearreglar constructores de tipo. Si tenemos un F[G[_]] pero requerimos un G[F[_]], por ejemplo, List[Future[Int]] pero requerimos un Future[List[_]], entonces ocupamos .sequence.

5.1 Agenda

Este capítulo es más largo de lo usual y está repleto de información: es perfectamente razonable abordarlo en varias sesiones de estudio. Recordar todo requeriría poderes sobrehumanos, de modo que trate este capítulo como una manera de buscar más información.

Es notable la ausencia de typeclasses que extienden Monad. Estas tendrán su propio capítulo más adelante.

Scalaz usa generación de código, no simulacrum. Sin embargo, por brevedad, presentaremos los fragmentos de código con @typeclass. La sintaxis equivalente estará disponible cuando hagamos un import scalaz._, Scalaz._ y estará disponible en el paquete scalaz.syntax en el código fuente de Scalaz.

5.2 Cosas que pueden agregarse

  @typeclass trait Semigroup[A] {
    @op("|+|") def append(x: A, y: =>A): A

    def multiply1(value: F, n: Int): F = ...
  }

  @typeclass trait Monoid[A] extends Semigroup[A] {
    def zero: A

    def multiply(value: F, n: Int): F =
      if (n <= 0) zero else multiply1(value, n - 1)
  }

  @typeclass trait Band[A] extends Semigroup[A]

Un Semigroup puede definirse para un tipo si dos valores pueden combinarse. El operador debe ser asociativo, es decir, que el orden de las operaciones anidadas no debería importar, es decir

  (a |+| b) |+| c == a |+| (b |+| c)

  (1 |+| 2) |+| 3 == 1 |+| (2 |+| 3)

Un Monoid es un Semigroup con un elemento zero (también llamado empty –vacío– o identity –identidad–). Combinar zero con cualquier otra a debería dar otra a .

  a |+| zero == a

  a |+| 0 == a

Esto probablemente le traiga memorias sobre Numeric del capítulo 4. Existen implementaciones de Monoid para todos los tipos numéricos primitivos, pero el concepto de cosas que se pueden agregar es útil más allá de los números.

  scala> "hello" |+| " " |+| "world!"
  res: String = "hello world!"

  scala> List(1, 2) |+| List(3, 4)
  res: List[Int] = List(1, 2, 3, 4)

Band tiene la ley de que la operación append de dos elementos iguales es idempotente, es decir devuelve el mismo valor. Ejemplos de esto pueden ser cualesquier cosa que sólo pueda tener un valor, tales como Unit, los límites superiores más pequeños, o un Set (conjunto). Band no proporciona métodos adicionales y sin embargo los usuarios pueden aprovechar las garantías que brinda con fines de optimización del rendimiento.

Como un ejemplo realista de Monoid, considere un sistema de comercio que tenga una base de datos grande de plantillas de transacciones comerciales reutilizables. Llenar las plantillas por default para una nueva transacción implica la selección y combinación de múltiples plantillas, con la regla del “último gana” para realizar uniones si dos plantillas proporcionan un valor para el mismo campo. El trabajo de “seleccionar” trabajo ya se realiza por nosotros mediante otro sistema, es nuestro trabajo combinar las plantillas en orden.

Crearemos un esquema simple de plantillas para demostrar el principio, pero tenga en mente que un sistema realista tendría un ADT más complicado.

  sealed abstract class Currency
  case object EUR extends Currency
  case object USD extends Currency

  final case class TradeTemplate(
    payments: List[java.time.LocalDate],
    ccy: Option[Currency],
    otc: Option[Boolean]
  )

Si escribimos un método que tome templates: List[TradeTemplate], entonces necesitaremos llamar únicamente

  val zero = Monoid[TradeTemplate].zero
  templates.foldLeft(zero)(_ |+| _)

¡y nuestro trabajo está hecho!

Pero para poder usar zero o invocar |+| debemos tener una instancia de Monoid[TradeTemplate]. Aunque derivaremos genéricamente este en un capítulo posterior, por ahora crearemos la instancia en el companion:

  object TradeTemplate {
    implicit val monoid: Monoid[TradeTemplate] = Monoid.instance(
      (a, b) => TradeTemplate(a.payments |+| b.payments,
                              a.ccy |+| b.ccy,
                              a.otc |+| b.otc),
      TradeTemplate(Nil, None, None)
    )
  }

Sin embargo, esto no hace lo que queremos porque Monoid[Option[A]] realizará una agregación de su contenido, por ejemplo,

  scala> Option(2) |+| None
  res: Option[Int] = Some(2)
  scala> Option(2) |+| Option(1)
  res: Option[Int] = Some(3)

mientras que deseamos implementar la regla del “último gana”. Podríamos hacer un override del valor default Monoid[Option[A]] con el nuestro propio:

  implicit def lastWins[A]: Monoid[Option[A]] = Monoid.instance(
    {
      case (None, None)   => None
      case (only, None)   => only
      case (None, only)   => only
      case (_   , winner) => winner
    },
    None
  )

Ahora todo compila, de modo que si lo intentamos…

  scala> import java.time.{LocalDate => LD}
  scala> val templates = List(
           TradeTemplate(Nil,                     None,      None),
           TradeTemplate(Nil,                     Some(EUR), None),
           TradeTemplate(List(LD.of(2017, 8, 5)), Some(USD), None),
           TradeTemplate(List(LD.of(2017, 9, 5)), None,      Some(true)),
           TradeTemplate(Nil,                     None,      Some(false))
         )

  scala> templates.foldLeft(zero)(_ |+| _)
  res: TradeTemplate = TradeTemplate(
                         List(2017-08-05,2017-09-05),
                         Some(USD),
                         Some(false))

Todo lo que tuvimos que hacer fue implementar una pieza de lógica de negocios y, !el Monoid se encargó de todo por nosotros!

Note que la lista de payments se concatenó. Esto es debido a que el Monoid[List] por default usa concatenación de elementos y simplemente ocurre que este es el comportamiento deseado aquí. Si el requerimiento de negocios fuera distinto, la solución sería proporcionar un Monoid[List[LocalDate]] personalizado. Recuerde del capítulo 4 que con el polimorfismo de tiempo de compilación tenemos una implementacion distinta de append dependiendo de la E en List[E], no sólo de la clase de tiempo de ejecución List.

5.3 Cosas parecidas a objetos

En el capítulo sobre Datos y Funcionalidad dijimos que la noción de la JVM de igualdad se derrumba para muchas cosas que podemos poner en una ADT. El problema es que la JVM fue diseñada para Java, y equals está definido sobre java.lang.Object aunque esto tenga sentido o no. No existe manera de remover equals y no hay forma de garantizar que esté implementado.

Sin embargo, en PF preferimos el uso de typeclasses para tener funcionalidad polimórfica e incluso el concepto de igualdad es capturado en tiempo de compilación.

  @typeclass trait Equal[F]  {
    @op("===") def equal(a1: F, a2: F): Boolean
    @op("/==") def notEqual(a1: F, a2: F): Boolean = !equal(a1, a2)
  }

En verdad === (triple igual) es más seguro desde la perspectiva de tipos que ==(doble igual) porque únicamente puede compilarse cuando los tipos son los mismos en ambos lados de la comparación. Esto atrapa muchos errores.

equal tiene los mismos requisitos de implementación que Object.equals

  • conmutativo f1 === f2 implica f2 === f1
  • reflexivo f === f
  • transitivo f1 === f2 && f2 === f3 implica que f1 === f3

Al desechar el concepto universal de Object.equals no damos por sentado el concepto de igualdad cuando construimos un ADT, y nos detiene en tiempo de compilación de esperar igualdad cuando en realidad no existe tal.

Continuando con la tendencia de reemplazar conceptos viejos de Java, más bien que considerar que los datos son un java.lang.Comparable, ahora tienen un Order de acuerdo con

  @typeclass trait Order[F] extends Equal[F] {
    @op("?|?") def order(x: F, y: F): Ordering

    override  def equal(x: F, y: F): Boolean = order(x, y) == Ordering.EQ
    @op("<" ) def lt(x: F, y: F): Boolean = ...
    @op("<=") def lte(x: F, y: F): Boolean = ...
    @op(">" ) def gt(x: F, y: F): Boolean = ...
    @op(">=") def gte(x: F, y: F): Boolean = ...

    def max(x: F, y: F): F = ...
    def min(x: F, y: F): F = ...
    def sort(x: F, y: F): (F, F) = ...
  }

  sealed abstract class Ordering
  object Ordering {
    case object LT extends Ordering
    case object EQ extends Ordering
    case object GT extends Ordering
  }

Order implementa .equal en términos de la primitiva nueva .order. Cuando una typeclass implementa el combinador primitivo de su padre con un combinador derivado, se agrega una ley implicación de sustitución para el typeclass. Si una instancia de Order fuera a hacer un override de .equal por razones de desempeño, debería comportase de manera idéntica a la original.

Las cosas que tienen un orden también podrían ser discretas, permitiéndonos avanzar o retroceder hacia un sucesor o predecesor:

  @typeclass trait Enum[F] extends Order[F] {
    def succ(a: F): F
    def pred(a: F): F
    def min: Option[F]
    def max: Option[F]

    @op("-+-") def succn(n: Int, a: F): F = ...
    @op("---") def predn(n: Int, a: F): F = ...

    @op("|->" ) def fromToL(from: F, to: F): List[F] = ...
    @op("|-->") def fromStepToL(from: F, step: Int, to: F): List[F] = ...
    @op("|=>" ) def fromTo(from: F, to: F): EphemeralStream[F] = ...
    @op("|==>") def fromStepTo(from: F, step: Int, to: F): EphemeralStream[F] = ...
  }
  scala> 10 |--> (2, 20)
  res: List[Int] = List(10, 12, 14, 16, 18, 20)

  scala> 'm' |-> 'u'
  res: List[Char] = List(m, n, o, p, q, r, s, t, u)

Discutiremos EphemeralStream en el siguiente capítulo, por ahora sólo necesitamos saber que se trata de una estructura de datos potencialmente infinita que evita los problemas de retención de memoria en el tipo Stream de la librería estándar.

De manera similar a Object.equals, el concepto de .toString en toda clases no tiene sentido en Java. Nos gustaría hacer cumplir el concepto de “poder representar como cadena” en tiempo de compilación y esto es exactamente lo que se consigue con Show:

  trait Show[F] {
    def show(f: F): Cord = ...
    def shows(f: F): String = ...
  }

Exploraremos Cord con más detalle en el capítulo que trate los tipos de datos, por ahora sólo necesitamos saber que es una estructura de datos eficiente para el almacenamiento y manipulación de String.

5.4 Cosas que se pueden mapear o transformar

Ahora nos enfocamos en las cosas que pueden mapearse, o recorrerse, en cierto sentido:

5.4.1 Functor

  @typeclass trait Functor[F[_]] {
    def map[A, B](fa: F[A])(f: A => B): F[B]

    def void[A](fa: F[A]): F[Unit] = map(fa)(_ => ())
    def fproduct[A, B](fa: F[A])(f: A => B): F[(A, B)] = map(fa)(a => (a, f(a)))

    def fpair[A](fa: F[A]): F[(A, A)] = map(fa)(a => (a, a))
    def strengthL[A, B](a: A, f: F[B]): F[(A, B)] = map(f)(b => (a, b))
    def strengthR[A, B](f: F[A], b: B): F[(A, B)] = map(f)(a => (a, b))

    def lift[A, B](f: A => B): F[A] => F[B] = map(_)(f)
    def mapply[A, B](a: A)(f: F[A => B]): F[B] = map(f)((ff: A => B) => ff(a))
  }

El único método abstracto es map, y debe ser posible hacer una composición, es decir, mapear con una f y entonces nuevamente con una g es lo mismo que hacer un mapeo una única vez con la composición de f y g:

  fa.map(f).map(g) == fa.map(f.andThen(g))

El map también debe realizar una operación nula si la función provista es la identidad (es decir, x => x)

  fa.map(identity) == fa

  fa.map(x => x) == fa

Functor define algunos métodos convenientes alrededor de map que pueden optimizarse para algunas instancias específicas. La documentación ha sido intencionalmente omitida en las definiciones arriba para incentivar el análisis de lo que hace un método antes de que vea la implementación. Por favor, deténgase unos momentos estudiando únicamente las signaturas de tipo de los siguientes métodos antes de avanzar más:

  def void[A](fa: F[A]): F[Unit]
  def fproduct[A, B](fa: F[A])(f: A => B): F[(A, B)]

  def fpair[A](fa: F[A]): F[(A, A)]
  def strengthL[A, B](a: A, f: F[B]): F[(A, B)]
  def strengthR[A, B](f: F[A], b: B): F[(A, B)]

  // harder
  def lift[A, B](f: A => B): F[A] => F[B]
  def mapply[A, B](a: A)(f: F[A => B]): F[B]
  1. void toma una instancia de F[A] y siempre devuelve un F[Unit], y se olvida de todos los valores a la vez que preserva la estructura.
  2. fproduct toma la misma entrada que map pero devuelve F[(A, B)], es decir, devuelve el contenido dentro de una tupla, con el resultado obtenido al aplicar la función. Esto es útil cuando deseamos retener la entrada.
  3. fpair repite todos los elementos de A en una tupla F[(A, A)]
  4. strengthL empareja el contenido de una F[B] con una constante A a la izquierda.
  5. strenghtR empareja el contenido de una F[A] con una constante B a la derecha.
  6. lift toma una función A => B y devuelve una F[A] => F[B]. En otras palabras, toma una función del contenido de una F[A] y devuelve una función que opera en el F[A] directamente.
  7. mapply nos obliga a pensar un poco. Digamos que tenemos una F[_] de funciones A => B y el valor A, entonces podemos obtener un F[B]. Tiene una firma/signatura similar a la de pure pero requiere que el que hace la llamada proporcione F[A => B].

fpair, strenghL y strengthR tal vez parezcan inútiles, pero mostrarán su utilidad cuando deseemos retener algo de información que de otra manera se perdería en el ámbito.

Functor tiene una sintaxis especial:

  implicit class FunctorOps[F[_]: Functor, A](self: F[A]) {
    def as[B](b: =>B): F[B] = Functor[F].map(self)(_ => b)
    def >|[B](b: =>B): F[B] = as(b)
  }

.as y >| es una forma de reemplazar la salida con una constante.

En nuestra aplicación de ejemplo, como un hack espantoso (que no admitimos hasta ahora), definimos start y stop de modo que devolvieran su entrada:

  def start(node: MachineNode): F[MachineNode]
  def stop (node: MachineNode): F[MachineNode]

Esto nos permitió escribir lógica breve de negocios como

  for {
    _      <- m.start(node)
    update = world.copy(pending = Map(node -> world.time))
  } yield update

y

  for {
    stopped <- nodes.traverse(m.stop)
    updates = stopped.map(_ -> world.time).toList.toMap
    update  = world.copy(pending = world.pending ++ updates)
  } yield update

pero este hack nos obliga a poner complejidad innecesaria en las implementaciones. Es mejor si dejamos que nuestras álgebras regresen F[Unit] y usar as:

  m.start(node) as world.copy(pending = Map(node -> world.time))

y

  for {
    stopped <- nodes.traverse(a => m.stop(a) as a)
    updates = stopped.map(_ -> world.time).toList.toMap
    update  = world.copy(pending = world.pending ++ updates)
  } yield update

5.4.2 Foldable

Técnicamente, Foldable es para estructuras de datos que pueden recorrerse y producir un valor que las resuma. Sin embargo, esto no dice lo suficiente sobre el hecho de que se trata de un arma poderosa proporcionada por las typeclasses que nos puede proporcionar la mayoría de lo que esperamos ver en una API de colecciones.

Hay tantos métodos que necesitaremos revisarlos en partes, empezando con los métodos abstractos:

  @typeclass trait Foldable[F[_]] {
    def foldMap[A, B: Monoid](fa: F[A])(f: A => B): B
    def foldRight[A, B](fa: F[A], z: =>B)(f: (A, =>B) => B): B
    def foldLeft[A, B](fa: F[A], z: B)(f: (B, A) => B): B = ...

Una instancia de Foldable necesita implementar únicamente foldMap y foldRight para obtener toda la funcionalidad en esta typeclass, aunque los métodos están típicamente optimizqados para estructuras de datos específicas.

.foldMap tiene un nombre usado en mercadotecnia: MapReduce. Dada una F[A], una función de A a B, y una forma de combinar una B (proporcionada por el Monoid, junto con el zero B), podemos producir el valor resumen de tipo B. No existe un orden impuesto en las operaciones, permitiendonos realizar cómputos paralelos.

foldRight no requiere que sus parámetros tengan un Monoid, significando esto que necesita un valor inicial z y una manera de combinar cada elemento de la estructura de datos con el valor resumen. El orden en el que se recorren los elementos es de derecha a izquierda y es por esta razón que no puede paralelizarse.

foldLeft recorre los elementos de izquierda a derecha. foldLeft puede implementarse en términos de foldMap, pero la mayoría de las instancias escogen implementarlas porque se trata de una operación básica. Dado que normalmente se implementa con recursión de cola, no existen parámetros byname.

La única ley para Foldable es que foldLeft y foldRight deberían ser consistentes con foldMap para operaciones monoidales, por ejemplo, agregando un elemento a una lista para foldLeft y anteponiendo un elemento a la lista para foldRight. Sin embargo, foldLeft y foldRight no necesitan ser consistentes la una con la otra: de hecho con frecuencia producen el inverso que produce el otro.

La cosa más sencilla que se puede hacer con foldMap es usar la función identity (identidad), proporcionando fold (la suma natural de los elementos monoidales), con variantes derecha/izquierda para permitirnos escoger basándonos en criterios de rendimiento:

  def fold[A: Monoid](t: F[A]): A = ...
  def sumr[A: Monoid](fa: F[A]): A = ...
  def suml[A: Monoid](fa: F[A]): A = ...

Recuerde que cuando aprendimos sobre Monoid, escribimos lo siguiente:

  scala> templates.foldLeft(Monoid[TradeTemplate].zero)(_ |+| _)

Sabemos que esto es tonto y que pudimos escribir:

  scala> templates.toIList.fold
  res: TradeTemplate = TradeTemplate(
                         List(2017-08-05,2017-09-05),
                         Some(USD),
                         Some(false))

.fold no funciona en la List estándar debido a que ya tiene un método llamado fold que hace su propia cosa a su manera especial.

El método llamado de manera extraña intercalate inserta una A específica entre cada elemento antes de realizar el fold

  def intercalate[A: Monoid](fa: F[A], a: A): A = ...

que es una versión generalizada del método de la librería estándar mkString:

  scala> List("foo", "bar").intercalate(",")
  res: String = "foo,bar"

El foldLeft proporciona el medio para obtener cualquier elemento mediante un índice para recorrer la estructura, incluyendo un grupo grande de métodos relacionados:

  def index[A](fa: F[A], i: Int): Option[A] = ...
  def indexOr[A](fa: F[A], default: =>A, i: Int): A = ...
  def length[A](fa: F[A]): Int = ...
  def count[A](fa: F[A]): Int = length(fa)
  def empty[A](fa: F[A]): Boolean = ...
  def element[A: Equal](fa: F[A], a: A): Boolean = ...

Scalaz es una librería funcional pura que tiene únicamente funciones totales. Mientras que List(0) puede lanzar excepciones, Foldable.index devuelve una Option[A] con el método conveniente .indexOr regreseando una A cuando se proporciona un valor por default. .element es similar al método de la librería estándar .contains pero usa Equal más bien que la mal definida igualdad de la JVM.

Estos métods realmente suenan como una API de colecciones. Y, por supuesto, toda cosa con una instancia de Foldable puede convertirse en una List

  def toList[A](fa: F[A]): List[A] = ...

También existen conversiones a otros tipos de datos de la librería estándar de Scala y de Scalaz, tales como .toSet, .toVector, .toStream, .to[T <: TraversableLike], toIList y la lista continúa.

Existen verificadores de predicados útiles

  def filterLength[A](fa: F[A])(f: A => Boolean): Int = ...
  def all[A](fa: F[A])(p: A => Boolean): Boolean = ...
  def any[A](fa: F[A])(p: A => Boolean): Boolean = ...

filterLenght es una forma de contar cuántos elementos son true para el predicado, all y any\ devuelven true is todos (o algún) elemento cumple con el predicado, y pueden terminar de manera temprana.

Podemos dividir en partes una F[A] que resulten en la misma B con splitBy

  def splitBy[A, B: Equal](fa: F[A])(f: A => B): IList[(B, Nel[A])] = ...
  def splitByRelation[A](fa: F[A])(r: (A, A) => Boolean): IList[Nel[A]] = ...
  def splitWith[A](fa: F[A])(p: A => Boolean): List[Nel[A]] = ...
  def selectSplit[A](fa: F[A])(p: A => Boolean): List[Nel[A]] = ...

  def findLeft[A](fa: F[A])(f: A => Boolean): Option[A] = ...
  def findRight[A](fa: F[A])(f: A => Boolean): Option[A] = ...

por ejemplo

  scala> IList("foo", "bar", "bar", "faz", "gaz", "baz").splitBy(_.charAt(0))
  res = [(f, [foo]), (b, [bar, bar]), (f, [faz]), (g, [gaz]), (b, [baz])]

haciendo la observación de que sólamente existen dos valores indexados por 'b'.

splitByRelation evita la necesidad de tener una Equal pero debemos proporcionar el operador de comparación.

splitWith divide los elementos en grupos que alternativamente satisfacen y no el predicado. selectSplit selecciona grupos de elementos que satisfacen el predicado, descartando los otros. Este es uno de esos casos raros en donde dos métodos comparten la misma firma/signatura, pero tienen significados distintos.

findLeft y findRight sirven para extraer el primer elemento (de la izquierda o de la derecha) que cumpla un predicado.

Haciendo uso adicional de Equal y Order, tenemos métodos distinct que devuelven agrupaciones.

  def distinct[A: Order](fa: F[A]): IList[A] = ...
  def distinctE[A: Equal](fa: F[A]): IList[A] = ...
  def distinctBy[A, B: Equal](fa: F[A])(f: A => B): IList[A] =

distinct se implementa de manera más eficiente que distinctE debido a que puede usar el ordenamiento y por lo tanto usar un algoritmo tipo quicksort que es mucho más rápido que la implementación ingenua de List.distinct. Las estructuras de datos (tales como los conjuntos) pueden implementar distinct y su Foldable sin realizar ningún trabajo.

distinctBy permite la agrupación mediante la aplicación de una función a sus elementos. Por ejemplo, agrupar nombres por su letra inicial.

Podemos hacer uso adicional de Order al extraer el elemento mínimo o máximo (o ambos extremos) incluyendo variaciones usando el patrón Of o By para mapear primero a otro tipo o usar un tipo diferente para hacer la otra comparación.

  def maximum[A: Order](fa: F[A]): Option[A] = ...
  def maximumOf[A, B: Order](fa: F[A])(f: A => B): Option[B] = ...
  def maximumBy[A, B: Order](fa: F[A])(f: A => B): Option[A] = ...

  def minimum[A: Order](fa: F[A]): Option[A] = ...
  def minimumOf[A, B: Order](fa: F[A])(f: A => B): Option[B] = ...
  def minimumBy[A, B: Order](fa: F[A])(f: A => B): Option[A] = ...

  def extrema[A: Order](fa: F[A]): Option[(A, A)] = ...
  def extremaOf[A, B: Order](fa: F[A])(f: A => B): Option[(B, B)] = ...
  def extremaBy[A, B: Order](fa: F[A])(f: A => B): Option[(A, A)] =

Por ejemplo podríamos preguntar cuál String es máxima By (por) longitud, o cuál es la máxima longitud Of (de) los elementos.

  scala> List("foo", "fazz").maximumBy(_.length)
  res: Option[String] = Some(fazz)

  scala> List("foo", "fazz").maximumOf(_.length)
  res: Option[Int] = Some(4)

Esto concluye con las características clave de Foldable. El punto clave a recordar es que cualquier cosa que esperaríamos encontrar en una librearía de colecciones está probablemente en Foldable y si no está ahí, probablemente debería estarlo.

Concluiremos con algunas variantes de los métodos que ya hemos visto. Primero, existen métodos que toman un Semigroup en lugar de un Monoid:

  def fold1Opt[A: Semigroup](fa: F[A]): Option[A] = ...
  def foldMap1Opt[A, B: Semigroup](fa: F[A])(f: A => B): Option[B] = ...
  def sumr1Opt[A: Semigroup](fa: F[A]): Option[A] = ...
  def suml1Opt[A: Semigroup](fa: F[A]): Option[A] = ...
  ...

devolviendo Option para tomar encuenta las estructuras de datos vacías (recuerde que Semigroup no tiene un zero).

La typeclass Foldable1 contiene muchas más variantes usando Semigroup de los métodos que usan Monoid mostrados aquí (todos ellos con el sufijo 1) y tienen sentido para estructuras de datos que nunca están vacías, sin requerrir la existencia de un Monoid para los elementos.

De manera importante, existen variantes que toman valores de retorno monádicos. Ya hemos usado foldLeft cuando escribimos por primera vez la lógica de negocios de nuestra aplicación, ahora sabemos que proviene de Foldable:

  def foldLeftM[G[_]: Monad, A, B](fa: F[A], z: B)(f: (B, A) => G[B]): G[B] = ...
  def foldRightM[G[_]: Monad, A, B](fa: F[A], z: =>B)(f: (A, =>B) => G[B]): G[B] = ...
  def foldMapM[G[_]: Monad, A, B: Monoid](fa: F[A])(f: A => G[B]): G[B] = ...
  def findMapM[M[_]: Monad, A, B](fa: F[A])(f: A => M[Option[B]]): M[Option[B]] = ...
  def allM[G[_]: Monad, A](fa: F[A])(p: A => G[Boolean]): G[Boolean] = ...
  def anyM[G[_]: Monad, A](fa: F[A])(p: A => G[Boolean]): G[Boolean] = ...
  ...

5.4.3 Traverse

Traverse es lo que sucede cuando hacemos el cruce de un Functor con un Foldable

  trait Traverse[F[_]] extends Functor[F] with Foldable[F] {
    def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
    def sequence[G[_]: Applicative, A](fga: F[G[A]]): G[F[A]] = ...

    def reverse[A](fa: F[A]): F[A] = ...

    def zipL[A, B](fa: F[A], fb: F[B]): F[(A, Option[B])] = ...
    def zipR[A, B](fa: F[A], fb: F[B]): F[(Option[A], B)] = ...
    def indexed[A](fa: F[A]): F[(Int, A)] = ...
    def zipWithL[A, B, C](fa: F[A], fb: F[B])(f: (A, Option[B]) => C): F[C] = ...
    def zipWithR[A, B, C](fa: F[A], fb: F[B])(f: (Option[A], B) => C): F[C] = ...

    def mapAccumL[S, A, B](fa: F[A], z: S)(f: (S, A) => (S, B)): (S, F[B]) = ...
    def mapAccumR[S, A, B](fa: F[A], z: S)(f: (S, A) => (S, B)): (S, F[B]) = ...
  }

Al principio del capítulo mostramos la importancia de traverse y sequence para invertir los constructores de tipo para que se ajusten a un requerimiento (por ejemplo, de List[Future[_]] a Future[List[_]]).

En Foldable no fuimos capaces de asumir que reverse fuera un concepto universal, pero ahora podemos invertir algo.

También podemos hacer un zip de dos cosas que tengan un Traverse, obteniendo un None cuando uno de los dos lados se queda sin elementos, usando zipL o zipR para decidir cuál lado truncar cuando las longitudes no empatan. Un caso especial de zip es agregar un índice a cada entrada con indexed.

zipWithL y zipWithR permiten la combinación de dos lados de un zip en un nuevo tipo, y entonces devuelven simplemente un F[C].

mapAccumL y mapAccumR son map regular combinado con un acumulador. Si nos topamos con la situación de que nuestra costumbre vieja proveniente de Java nos quiere hacer usar una var, y deseamos referirnos a ella en un map, deberíamos estar usando mapAccumL.

Por ejemplo, digamos que tenemos una lista de palabras y que deseamos borrar las palabras que ya hemos encontrado. El algoritmo de filtrado no permite procesar las palabras de la lista una segunda vez de modo que pueda escalarse a un stream infinito:

  scala> val freedom =
  """We campaign for these freedoms because everyone deserves them.
     With these freedoms, the users (both individually and collectively)
     control the program and what it does for them."""
     .split("\\s+")
     .toList

  scala> def clean(s: String): String = s.toLowerCase.replaceAll("[,.()]+", "")

  scala> freedom
         .mapAccumL(Set.empty[String]) { (seen, word) =>
           val cleaned = clean(word)
           (seen + cleaned, if (seen(cleaned)) "_" else word)
         }
         ._2
         .intercalate(" ")

  res: String =
  """We campaign for these freedoms because everyone deserves them.
     With _ _ the users (both individually and collectively)
     control _ program _ what it does _ _"""

Finalmente Traversable1, como Foldable1, proporciona variantes de estos métodos para las estructuras de datos que no pueden estar vacías, aceptando Semigroup (más débil) en lugar de un Monoid, y un Apply en lugar de un Applicative. Recuerde que Semigroup no tiene que proporcionar un .empty, y Apply no tiene que proporcionar un .point.

5.4.4 Align

Align es sobre unir y rellenar algo con un Functor. Antes de revisar Align, conozca al tipo de datos \&/ (pronunciado como These, o ¡Viva!).

  sealed abstract class \&/[+A, +B]
  final case class This[A](aa: A) extends (A \&/ Nothing)
  final case class That[B](bb: B) extends (Nothing \&/ B)
  final case class Both[A, B](aa: A, bb: B) extends (A \&/ B)

es decir, se trata de una codificación con datos de un OR lógico inclusivo. A o B o ambos A y B.

  @typeclass trait Align[F[_]] extends Functor[F] {
    def alignWith[A, B, C](f: A \&/ B => C): (F[A], F[B]) => F[C]
    def align[A, B](a: F[A], b: F[B]): F[A \&/ B] = ...

    def merge[A: Semigroup](a1: F[A], a2: F[A]): F[A] = ...

    def pad[A, B]: (F[A], F[B]) => F[(Option[A], Option[B])] = ...
    def padWith[A, B, C](f: (Option[A], Option[B]) => C): (F[A], F[B]) => F[C] = ...

alignWith toma una función de ya sea una A o una B (o ambos) a una C y devuelve una función alzada de una tupla de F[A] y F[B] a una F[C]. align construye \&/ a partir de dos F[_].

merge nos permite combinar dos F[A] cuando A tiene un Semigroup. Por ejemplo, la implementación de Semigroup[Map[K, V]] delega a un Semigroup[V], combinando dos entradas de resultados en la combinación de sus valores, teniendo la consecuencia de que Map[K, List[A]] se comporta como un multimapa:

  scala> Map("foo" -> List(1)) merge Map("foo" -> List(1), "bar" -> List(2))
  res = Map(foo -> List(1, 1), bar -> List(2))

y un Map[K, Int] simplemente totaliza sus contenidos cuando hace la unión

  scala> Map("foo" -> 1) merge Map("foo" -> 1, "bar" -> 2)
  res = Map(foo -> 2, bar -> 2)

.pad y .padWith son para realizar una unión parcial de dos estructuras de datos que puedieran tener valores faltantes en uno de los lados. Por ejemplo si desearamos agregar votos independientes y retener el conocimiento de donde vinieron los votos

  scala> Map("foo" -> 1) pad Map("foo" -> 1, "bar" -> 2)
  res = Map(foo -> (Some(1),Some(1)), bar -> (None,Some(2)))

  scala> Map("foo" -> 1, "bar" -> 2) pad Map("foo" -> 1)
  res = Map(foo -> (Some(1),Some(1)), bar -> (Some(2),None))

Existen variantes convenientes de align que se aprovechan de la estructura de \&/

  ...
    def alignSwap[A, B](a: F[A], b: F[B]): F[B \&/ A] = ...
    def alignA[A, B](a: F[A], b: F[B]): F[Option[A]] = ...
    def alignB[A, B](a: F[A], b: F[B]): F[Option[B]] = ...
    def alignThis[A, B](a: F[A], b: F[B]): F[Option[A]] = ...
    def alignThat[A, B](a: F[A], b: F[B]): F[Option[B]] = ...
    def alignBoth[A, B](a: F[A], b: F[B]): F[Option[(A, B)]] = ...
  }

las cuáles deberían tener sentido a partir de las firmas/signaturas de tipo. Ejemplos:

  scala> List(1,2,3) alignSwap List(4,5)
  res = List(Both(4,1), Both(5,2), That(3))

  scala> List(1,2,3) alignA List(4,5)
  res = List(Some(1), Some(2), Some(3))

  scala> List(1,2,3) alignB List(4,5)
  res = List(Some(4), Some(5), None)

  scala> List(1,2,3) alignThis List(4,5)
  res = List(None, None, Some(3))

  scala> List(1,2,3) alignThat List(4,5)
  res = List(None, None, None)

  scala> List(1,2,3) alignBoth List(4,5)
  res = List(Some((1,4)), Some((2,5)), None)

Note que las variantes A y B usan OR inclusivo, mientras que las variantes This y That son exclusivas, devolviendo None si existe un valor en ambos lados, o ningún valor en algún lado.

5.5 Variancia

Debemos regresar a Functor por un momento y discutir un ancestro que previamente habíamos ignorado:

InvariantFunctor, también conocido como el functor exponencial, tiene un método xmap que dice que dada una función de A a B, y una función de B a A, podemos entonces convertir F[A] a F[B].

Functor es una abreviación de lo que deberíamos llamar functor covariante. Pero dado que Functor es tan popular este conserva la abreviación. De manera similar, Contravariant debería ser llamado functor contravariante.

Functor implementa xmap con map e ignora la función de B a A. Contravariant, por otra parte, implementa xmap con contramap e ignora la función de A a B:

  @typeclass trait InvariantFunctor[F[_]] {
    def xmap[A, B](fa: F[A], f: A => B, g: B => A): F[B]
    ...
  }

  @typeclass trait Functor[F[_]] extends InvariantFunctor[F] {
    def map[A, B](fa: F[A])(f: A => B): F[B]
    def xmap[A, B](fa: F[A], f: A => B, g: B => A): F[B] = map(fa)(f)
    ...
  }

  @typeclass trait Contravariant[F[_]] extends InvariantFunctor[F] {
    def contramap[A, B](fa: F[A])(f: B => A): F[B]
    def xmap[A, B](fa: F[A], f: A => B, g: B => A): F[B] = contramap(fa)(g)
    ...
  }

Es importante notar que, aunque están relacionados a nivel teórico, las palabras covariante, contravariante e invariante no se refieren directamente a la variancia de tipos en Scala (es decir, con los prefijos + y - que pudieran escribirse en las firmas/signaturas de los tipos). Invariancia aquí significa que es posible mapear el contenido de la estructura F[A] en F[B]. Usando la función identidad (identity) podemos ver que A puede convertirse de manera segura en una B dependiendo de la variancia del functor.

.map puede entenderse por medio del contrato “si me das una F de A y una forma de convertir una B en una A, entonces puedo darte una F de B”.

Consideraremos un ejemplo: en nuestra aplicación introducimos tipos específicos del dominio Alpha, Beta, Gamma, etc, para asegurar que no estemos mezclando números en un cálculo financiero:

  final case class Alpha(value: Double)

pero ahora nos encontramos con el problema de que no tenemos ninguna typeclass para estos nuevos tipos. Si usamos los valores en los documentos JSON, entonces tenemos que escribir instancias de JsEncoder y JsDecoder.

Sin embargo, JsEncoder tiene un Contravariant y JsDecoder tiene un Functor, de modo que es posible derivar instancias. Llenando el contrato:

  • “si me das un JsDecoder para un Double, y una manera de ir de un Double a un Alpha, entonces yo puedo darte un JsDecoder para un Alpha”.
  • “si me das un JsEncoder par un Double, y una manera de ir de un Alpha a un Double, entonces yo puedo darte un JsEncoder para un Alpha”.
  object Alpha {
    implicit val decoder: JsDecoder[Alpha] = JsDecoder[Double].map(_.value)
    implicit val encoder: JsEncoder[Alpha] = JsEncoder[Double].contramap(_.value)
  }

Los métodos en una typeclass pueden tener sus parámetros de tipo en posición contravariante (parámetros de método) o en posición covariante (tipo de retorno). Si una typeclass tiene una combinación de posiciones covariantes y contravariantes, tal vez también tenga un functor invariante. Por ejemplo, Semigroup y Monoid tienen un InvariantFunctor, pero no un Functor o un Contravariant.

5.6 Apply y Bind

Considere ésto un calentamiento para Applicative y Monad

5.6.1 Apply

Apply extiende Functor al agregar un método llamado ap que es similar a map en el sentido de que aplica una función a valores. Sin embargo, con ap, la función está en el mismo contexto que los valores.

  @typeclass trait Apply[F[_]] extends Functor[F] {
    @op("<*>") def ap[A, B](fa: =>F[A])(f: =>F[A => B]): F[B]
    ...

Vale la pena tomarse un momento y considerar lo que significa para una estructura de datos simple como Option[A], el que tenga la siguiente implementación de .ap

  implicit def option[A]: Apply[Option[A]] = new Apply[Option[A]] {
    override def ap[A, B](fa: =>Option[A])(f: =>Option[A => B]) = f match {
      case Some(ff) => fa.map(ff)
      case None    => None
    }
    ...
  }

Para implementar .ap primero debemos extraer la función ff: A => B de f: Option[A => B], y entonces podemos mapear sobre fa. La extracción de la función a partir del contexto es el poder importante que Apply tiene, permitiendo que múltiples funciones se combinen dentro del contexto.

Regresando a Apply, encontramos la función auxiliar .applyX que nos permite combinar funciones paralelas y entonces mapear sobre la salida combinada:

  @typeclass trait Apply[F[_]] extends Functor[F] {
    ...
    def apply2[A,B,C](fa: =>F[A], fb: =>F[B])(f: (A, B) => C): F[C] = ...
    def apply3[A,B,C,D](fa: =>F[A],fb: =>F[B],fc: =>F[C])(f: (A,B,C) =>D): F[D] = ...
    ...
    def apply12[...]

Lea .apply2 como un contrato con la promesa siguiente: “si usted me da una F de A y una F de B, con una forma de combinar A y B en una C, entonces puedo devolverle una F de C”. Existen muchos casos de uso para este contrato y los dos más importantes son:

  • construir algunas typeclasses para el tipo producto C a partir de sus constituyentes A y B
  • ejecutar efectos en paralelo, como en las álgebras del drone y de google que creamos en el Capítulo 3, y entonces combinando sus resultados.

En verdad, Apply es tan útil que tiene una sintaxis especial:

  implicit class ApplyOps[F[_]: Apply, A](self: F[A]) {
    def *>[B](fb: F[B]): F[B] = Apply[F].apply2(self,fb)((_,b) => b)
    def <*[B](fb: F[B]): F[A] = Apply[F].apply2(self,fb)((a,_) => a)
    def |@|[B](fb: F[B]): ApplicativeBuilder[F, A, B] = ...
  }

  class ApplicativeBuilder[F[_]: Apply, A, B](a: F[A], b: F[B]) {
    def tupled: F[(A, B)] = Apply[F].apply2(a, b)(Tuple2(_))
    def |@|[C](cc: F[C]): ApplicativeBuilder3[C] = ...

    sealed abstract class ApplicativeBuilder3[C](c: F[C]) {
      ..ApplicativeBuilder4
        ...
          ..ApplicativeBuilder12
  }

que es exactamente lo que se usó en el Capítulo 3:

  (d.getBacklog |@| d.getAgents |@| m.getManaged |@| m.getAlive |@| m.getTime)

La sintaxis <*y *> (el ave hacia la izquierda y hacia la derecha) ofrece una manera conveniente de ignorar la salida de uno de dos efectos paralelos.

Desgraciadamente, aunque la syntaxis con |@| es clara, hay un problema pues se crea un nuevo objeto de tipo ApplicativeBuilder por cada efecto adicional. Si el trabajo es principalmente de naturaleza I/O, el costo de la asignación de memoria es insignificante. Sin embargo, cuando el trabajo es mayormente de naturaleza computacional, es preferible usar la sintaxis alternativa de alzamiento con aridad, que no produce ningún objeto intermedio:

  def ^[F[_]: Apply,A,B,C](fa: =>F[A],fb: =>F[B])(f: (A,B) =>C): F[C] = ...
  def ^^[F[_]: Apply,A,B,C,D](fa: =>F[A],fb: =>F[B],fc: =>F[C])(f: (A,B,C) =>D): F[D] = ...
  ...
  def ^^^^^^[F[_]: Apply, ...]

y se usa así:

  ^^^^(d.getBacklog, d.getAgents, m.getManaged, m.getAlive, m.getTime)

o haga una invocación directa de `applyX

  Apply[F].apply5(d.getBacklog, d.getAgents, m.getManaged, m.getAlive, m.getTime)

A pesar de ser más común su uso con efectos, Applyfunciona también con estructuras de datos. Considere reescribir

  for {
    foo <- data.foo: Option[String]
    bar <- data.bar: Option[Int]
  } yield foo + bar.shows

como

  (data.foo |@| data.bar)(_ + _.shows)

Si nosotros deseamos únicamente la salida combinada como una tupla, existen métodos para hacer sólo eso:

  @op("tuple") def tuple2[A,B](fa: =>F[A],fb: =>F[B]): F[(A,B)] = ...
  def tuple3[A,B,C](fa: =>F[A],fb: =>F[B],fc: =>F[C]): F[(A,B,C)] = ...
  ...
  def tuple12[...]
  (data.foo tuple data.bar) : Option[(String, Int)]

Existen también versiones generalizadas de ap para más de dos parámetros:

  def ap2[A,B,C](fa: =>F[A],fb: =>F[B])(f: F[(A,B) => C]): F[C] = ...
  def ap3[A,B,C,D](fa: =>F[A],fb: =>F[B],fc: =>F[C])(f: F[(A,B,C) => D]): F[D] = ...
  ...
  def ap12[...]

junto con métodos .lift que toman funciones normales y las alzan al contexto F[_], la generalización de Functor.lift

  def lift2[A,B,C](f: (A,B) => C): (F[A],F[B]) => F[C] = ...
  def lift3[A,B,C,D](f: (A,B,C) => D): (F[A],F[B],F[C]) => F[D] = ...
  ...
  def lift12[...]

y .apF, una sintáxis parcialmente aplicada para ap

  def apF[A,B](f: =>F[A => B]): F[A] => F[B] = ...

Finalmente .forever

  def forever[A, B](fa: F[A]): F[B] = ...

que repite el efecto sin detenerse. La instancia de Apply debe tener un uso seguro del stack o tendremos StackOverflowError.

5.6.2 Bind

Bind introduces .bind, que es un sinónimo de .flatMap, que permite funciones sobre el resultado de un efecto regresar un nuevo efecto, o para funciones sobre los valores de una estructura de datos devolver nuevas estructuras de datos que entonces son unidas.

  @typeclass trait Bind[F[_]] extends Apply[F] {

    @op(">>=") def bind[A, B](fa: F[A])(f: A => F[B]): F[B]
    def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] = bind(fa)(f)

    override def ap[A, B](fa: =>F[A])(f: =>F[A => B]): F[B] =
      bind(f)(x => map(fa)(x))
    override def apply2[A, B, C](fa: =>F[A], fb: =>F[B])(f: (A, B) => C): F[C] =
      bind(fa)(a => map(fb)(b => f(a, b)))

    def join[A](ffa: F[F[A]]): F[A] = bind(ffa)(identity)

    def mproduct[A, B](fa: F[A])(f: A => F[B]): F[(A, B)] = ...
    def ifM[B](value: F[Boolean], t: =>F[B], f: =>F[B]): F[B] = ...

  }

El método .join debe ser familiar a los usuarios de .flatten en la librería estándar, y toma un contexto anidado y lo convierte en uno sólo.

Combinadores derivados se introducen para .ap y .apply2 que requieren consistencia con .bind. Veremos más adelante que esta ley tiene consecuencias para las estrategias de paralelización.

mproduct es como Functor.fproduct y empareja la entrada de una función con su salida, dentro de F.

ifM es una forma de construir una estructura de datos o efecto condicional:

  scala> List(true, false, true).ifM(List(0), List(1, 1))
  res: List[Int] = List(0, 1, 1, 0)

ifM y ap están optimizados para crear un cache y reusar las ramas de código, compare a la forma más larga

  scala> List(true, false, true).flatMap { b => if (b) List(0) else List(1, 1) }

que produce una nueva List(0) o List(1, 1) cada vez que se invoca una alternativa.

Bind también tiene sintaxis especial

  implicit class BindOps[F[_]: Bind, A] (self: F[A]) {
    def >>[B](b: =>F[B]): F[B] = Bind[F].bind(self)(_ => b)
    def >>![B](f: A => F[B]): F[A] = Bind[F].bind(self)(a => f(a).map(_ => a))
  }

>> se usa cuando deseamos descartar la entrada a bind y >>! cuando deseamos ejecutar un efecto pero descartar su salida.

5.7 Applicative y Monad

Desde un punto de vista de funcionalidad, Applicative es Apply con un método pure, y Monad extiende Applicative con Bind.

  @typeclass trait Applicative[F[_]] extends Apply[F] {
    def point[A](a: =>A): F[A]
    def pure[A](a: =>A): F[A] = point(a)
  }

  @typeclass trait Monad[F[_]] extends Applicative[F] with Bind[F]

En muchos sentidos, Applicative y Monad son la culminación de todo lo que hemos visto en este capítulo. .pure (o .point como se conoce más comúnmente para las estructuras de datos) nos permite crear efectos o estructuras de datos a partir de valores.

Las instancias de Applicative deben satisfacer algunas leyes, asegurándose así de que todos los métodos sean consistentes:

  • Identidad: fa <*> pure(identity) === fa, (donde fa está en F[A]) es decir, aplicar pure(identity) no realiza ninguna operación.
  • Homomorfismo: pure(a) <*> pure(ab) === pure(ab(a)) (donde ab es una A => B), es decir aplicar una función pure a un valor pure es lo mismo que aplicar la función al valor y entonces usar pure sobre el resultado.
  • Intercambio: pure(a) <*> fab === fab <*> pure (f => f(a)), (donde fab es una F[A => B]), es decir pure es la identidad por la izquierda y por la derecha.
  • Mapeo: map(fa)(f) === fa <*> pure(f)

Monad agrega leyes adicionales:

  • Identidad por la izquierda: pure(a).bind(f) === f(a)
  • Identidad por la derecha: a.bind(pure(_)) === a
  • Asociatividad: fa.bind(f).bind(g) === fa.bind(a => f(a).bind(g)) donde fa es una F[A], f es una A => F[B] y g es una B => F[C].

La asociatividad dice que invocaciones repetidas de bind deben resultar en lo mismo que invocaciones anidadas de bind. Sin embargo, esto no significa que podamos reordenar, lo que sería conmutatividad. Por ejemplo, recordando que flatMap es un alias de bind, no podemos reordenar

  for {
    _ <- machine.start(node1)
    _ <- machine.stop(node1)
  } yield true

como

  for {
    _ <- machine.stop(node1)
    _ <- machine.start(node1)
  } yield true

start y stop no son conmutativas, porque ¡el efecto deseado de iniciar y luego detener un nodo es diferente a detenerlo y luego iniciarlo!

Pero start es conmutativo consigo mismo, y stop es conmutativo consigo mismo, de modo que podemos reescribir

  for {
    _ <- machine.start(node1)
    _ <- machine.start(node2)
  } yield true

como

  for {
    _ <- machine.start(node2)
    _ <- machine.start(node1)
  } yield true

que son equivalentes para nuestra álgebra, pero no en general. Aquí se están haciendo muchas suposiciones sobre la API de Google Container, pero es una elección razonable que podemos hacer.

Una consecuencia práctica es que una Monad debe ser conmutativa si sus métodos applyX pueden ser ejecutados en paralelo. En el capítulo 3 hicimos trampa cuando ejecutamos estos efectos en paralelo

  (d.getBacklog |@| d.getAgents |@| m.getManaged |@| m.getAlive |@| m.getTime)

porque sabemos que son conmutativos entre sí. Cuando tengamos que interpretar nuestra aplicación, más adelante en el libro, tendremos que proporcionar evidencia de que estos efectos son de hecho conmutativos, o una implementación asíncrona podría escoger efectuar las operaciones de manera secuencial por seguridad.

Las sutilezas de cómo lidiar con el reordenamiento de efectos, y cuáles son estos efectos, merece un capítulo dedicado sobre mónadas avanzadas.

5.8 Divide y conquistarás

Divide es el análogo Contravariant de Apply

  @typeclass trait Divide[F[_]] extends Contravariant[F] {
    def divide[A, B, C](fa: F[A], fb: F[B])(f: C => (A, B)): F[C] = divide2(fa, fb)(f)

    def divide1[A1, Z](a1: F[A1])(f: Z => A1): F[Z] = ...
    def divide2[A, B, C](fa: F[A], fb: F[B])(f: C => (A, B)): F[C] = ...
    ...
    def divide22[...] = ...

divide dice que si podemos partir una C en una A y una B, y se nos da una F[A] y una F[B], entonces podemos tener una F[C]. De ahí la frase divide y conquistarás.

Esta es una gran manera de generar instancias contravariantes de una typeclass para tipos producto mediante la separación de los productos en sus partes constituyentes. Scalaz tiene una instancia de Divide[Equal], así que vamos a construir un Equal para un nuevo tipo producto Foo

  scala> case class Foo(s: String, i: Int)
  scala> implicit val fooEqual: Equal[Foo] =
           Divide[Equal].divide2(Equal[String], Equal[Int]) {
             (foo: Foo) => (foo.s, foo.i)
           }
  scala> Foo("foo", 1) === Foo("bar", 1)
  res: Boolean = false

Siguiendo los patrones en Apply, Divide también tiene una sintaxis clara para las tuplas. Es un enfoque más suave que divide de modo que podamos reinar con el objetivo de dominar el mundo:

  ...
    def tuple2[A1, A2](a1: F[A1], a2: F[A2]): F[(A1, A2)] = ...
    ...
    def tuple22[...] = ...
  }

Generalmente, si las typeclasses de un codificador pueden proporcionar una instancia de Divide, más bien que detenerse en Contravariant, entonces se hace posible la derivación de instancias para cualquier case class. De manera similar, las typeclasses de decodificadores pueden proporcionar una instancia de Apply. Exploraremos esto en un capítulo dedicado a la derivación de typeclasses.

Divisible es el análogo contravariante (Contravariant) de Applicative e introduce .conquer, el equivalente a .pure

  @typeclass trait Divisible[F[_]] extends Divide[F] {
    def conquer[A]: F[A]
  }

.conquer también permite la creación trivial de implementaciones donde el parámetro de tipo es ignorado. Tales valores se llaman universalmente cuantificados. Por ejemplo, Divisible[Equal].conquer[INil[String]] devuelve una implementación de Equal para una lista vacía de String que siempre es true.

5.9 Plus

Plus es un Semigroup pero para constructores de tipo, y PlusEmpty es el equivalente de Monoid (incluso tienen las mismas leyes) mientras que IsEmpty es novedoso y nos permite consultar si un F[A] está vacío:

  @typeclass trait Plus[F[_]] {
    @op("<+>") def plus[A](a: F[A], b: =>F[A]): F[A]
  }
  @typeclass trait PlusEmpty[F[_]] extends Plus[F] {
    def empty[A]: F[A]
  }
  @typeclass trait IsEmpty[F[_]] extends PlusEmpty[F] {
    def isEmpty[A](fa: F[A]): Boolean
  }

Aunque superficialmente pueda parecer como si <+> se comportara como |+|

  scala> List(2,3) |+| List(7)
  res = List(2, 3, 7)

  scala> List(2,3) <+> List(7)
  res = List(2, 3, 7)

es mejor pensar que este operador funciona únicamente al nivel de F[_], nunca viendo al contenido. Plus tiene la convención de que debería ignorar las fallas y “escoger al primer ganador”. <+> puede por lo tanto ser usado como un mecanismo de salida temprana (con pérdida de información) y de manejo de fallas mediante alternativas:

  scala> Option(1) |+| Option(2)
  res = Some(3)

  scala> Option(1) <+> Option(2)
  res = Some(1)

  scala> Option.empty[Int] <+> Option(1)
  res = Some(1)

Por ejemplo, si tenemos una NonEmptyList[Option[Int]] y deseamos ignorar los valores None (fallas) y escoger el primer ganador (Some), podemos invocar <+> de Foldable1.foldRight1:

  scala> NonEmptyList(None, None, Some(1), Some(2), None)
         .foldRight1(_ <+> _)
  res: Option[Int] = Some(1)

De hecho, ahora que sabemos de Plus, nos damos cuenta de que no era necesaria violar la coherencia de typeclases (cuando definimos un Monoid[Option[A]] con alcance local) en la sección sobre Cosas que se pueden agregar. Nuestro objectivo era “escoger el último ganador”, que es lo mismo que “escoge al ganador” si los argumentos se intercambian. Note el uso del interceptor TIE para ccy y otc con los argumentos intercambiados.

  implicit val monoid: Monoid[TradeTemplate] = Monoid.instance(
    (a, b) => TradeTemplate(a.payments |+| b.payments,
                            b.ccy <+> a.ccy,
                            b.otc <+> a.otc),
    TradeTemplate(Nil, None, None)
  )

Applicative y Monad tienen versiones especializadas de PlusEmpty

  @typeclass trait ApplicativePlus[F[_]] extends Applicative[F] with PlusEmpty[F]

  @typeclass trait MonadPlus[F[_]] extends Monad[F] with ApplicativePlus[F] {
    def unite[T[_]: Foldable, A](ts: F[T[A]]): F[A] = ...

    def withFilter[A](fa: F[A])(f: A => Boolean): F[A] = ...
  }

.unite nos permite hacer un fold de una estructura de datos usando el contenedor externo PlusEmpy[F].monoid más bien que el contenido interno Monoid. Para List[Either[String, Int]] esto significa que los valores se convierten en .empty, cuando todo se concatena. Una forma conveniente de descartar los errores:

  scala> List(Right(1), Left("boo"), Right(2)).unite
  res: List[Int] = List(1, 2)

  scala> val boo: Either[String, Int] = Left("boo")
         boo.foldMap(a => a.pure[List])
  res: List[String] = List()

  scala> val n: Either[String, Int] = Right(1)
         n.foldMap(a => a.pure[List])
  res: List[Int] = List(1)

withFilternos permite hacer uso del soporte para for comprehension de Scala, como se discutió en el capítulo 2. Es justo decir que el lenguaje Scala tiene soporte incluído para MonadPlus, no sólo Monad!

Regresando a Foldable por un momento, podemos revelar algunos métodos que no discutimos antes:

  @typeclass trait Foldable[F[_]] {
    ...
    def msuml[G[_]: PlusEmpty, A](fa: F[G[A]]): G[A] = ...
    def collapse[X[_]: ApplicativePlus, A](x: F[A]): X[A] = ...
    ...
  }

msuml realiza un fold utilizando el Monoidde PlusEmpty[G] y collapse realiza un foldRight usando PlusEmpty del tipo target:

  scala> IList(Option(1), Option.empty[Int], Option(2)).fold
  res: Option[Int] = Some(3) // uses Monoid[Option[Int]]

  scala> IList(Option(1), Option.empty[Int], Option(2)).msuml
  res: Option[Int] = Some(1) // uses PlusEmpty[Option].monoid

  scala> IList(1, 2).collapse[Option]
  res: Option[Int] = Some(1)

5.10 Lobos solitarios

Algunas typeclasses en Scalaz existen por sí mismas y no son parte de una jerarquía más grande.

5.10.1 Zippy

  @typeclass trait Zip[F[_]]  {
    def zip[A, B](a: =>F[A], b: =>F[B]): F[(A, B)]

    def zipWith[A, B, C](fa: =>F[A], fb: =>F[B])(f: (A, B) => C)
                        (implicit F: Functor[F]): F[C] = ...

    def ap(implicit F: Functor[F]): Apply[F] = ...

    @op("<*|*>") def apzip[A, B](f: =>F[A] => F[B], a: =>F[A]): F[(A, B)] = ...

  }

El método esencial zip que es una versión menos poderosa que Divide.tuple2, y si un Functor[F] se proporciona entonces zipWithpuede comportarse como Apply.apply2. En verdad, un Apply[F] puede crearse a partir de Zip[F] y un Functor[F] mediante invocar ap.

apzip toma un F[A] y una función elevada de F[A] => F[B], produciendo un F[(A, B)] similar a Functor.fproduct.

  @typeclass trait Unzip[F[_]]  {
    @op("unfzip") def unzip[A, B](a: F[(A, B)]): (F[A], F[B])

    def firsts[A, B](a: F[(A, B)]): F[A] = ...
    def seconds[A, B](a: F[(A, B)]): F[B] = ...

    def unzip3[A, B, C](x: F[(A, (B, C))]): (F[A], F[B], F[C]) = ...
    ...
    def unzip7[A ... H](x: F[(A, (B, ... H))]): ...
  }

El método central es unzip con firsts y seconds que permite elegir ya sea el primer o segundo elemento de una tupla en la F. De manera importante, unzip es lo opuesto de zip.

Los métodos unzip3 a unzip7 son aplicaciones repetidas de unzip para evitar escribir código repetitivo. Por ejemplo, si se le proporcionara un conjunto de tuplas anidadas, el Unzip[Id] es una manera sencilla de deshacer la anidación:

  scala> Unzip[Id].unzip7((1, (2, (3, (4, (5, (6, 7)))))))
  res = (1,2,3,4,5,6,7)

En resumen, Zip y Unzip son versiones menos poderosas de Divide y Apply, y proporcionan características poderosas sin requerir que F haga demasiadas promesas.

5.10.2 Optional

Optional es una generalización de estructuras de datos que opcionalmente pueden contener un valor, como Option y Either.

Recuerde que una \/ (disjunción) es la versión mejorada de Scalaz de Scalaz.Either. También veremos Maybe de Scalaz, que es la versión mejorada de scala.Option.

  sealed abstract class Maybe[A]
  final case class Empty[A]()    extends Maybe[A]
  final case class Just[A](a: A) extends Maybe[A]
  @typeclass trait Optional[F[_]] {
    def pextract[B, A](fa: F[A]): F[B] \/ A

    def getOrElse[A](fa: F[A])(default: =>A): A = ...
    def orElse[A](fa: F[A])(alt: =>F[A]): F[A] = ...

    def isDefined[A](fa: F[A]): Boolean = ...
    def nonEmpty[A](fa: F[A]): Boolean = ...
    def isEmpty[A](fa: F[A]): Boolean = ...

    def toOption[A](fa: F[A]): Option[A] = ...
    def toMaybe[A](fa: F[A]): Maybe[A] = ...
  }

Estos métodos le deberían ser familiares, con la excepción, quizá, de pextract, que es una forma de dejar que F[_] regrese una implementación específica F[B] o el valor. Por ejemplo, Optional[Option].pextract devuelve Option[Nothing] \/ A, es decir, None \/ A.

Scalaz proporciona un operador ternario para las cosas que tienen un Optional

  implicit class OptionalOps[F[_]: Optional, A](fa: F[A]) {
    def ?[X](some: =>X): Conditional[X] = new Conditional[X](some)
    final class Conditional[X](some: =>X) {
      def |(none: =>X): X = if (Optional[F].isDefined(fa)) some else none
    }
  }

por ejemplo

  scala> val knock_knock: Option[String] = ...
         knock_knock ? "who's there?" | "<tumbleweed>"

5.11 Co-cosa

Una co-cosa típicamente tiene la firma/signatura de tipo opuesta a lo que sea que una cosa hace, pero no necesariamente a la inversa. Para enfatizar la relación entre una cosa y una co-cosa, incluiremos la firma/signatura de la cosa siempre que sea posible.

5.11.1 Cobind

  @typeclass trait Cobind[F[_]] extends Functor[F] {
    def cobind[A, B](fa: F[A])(f: F[A] => B): F[B]
  //def   bind[A, B](fa: F[A])(f: A => F[B]): F[B]

    def cojoin[A](fa: F[A]): F[F[A]] = ...
  //def   join[A](ffa: F[F[A]]): F[A] = ...
  }

cobind (también conocido como coflatmap) toma una F[A] => B que actúa en una F[A] más bien que sobre sus elementos. Pero no se trata necesariamente de una fa completa, es con frecuencia alguna subestructura como la define un cojoin (también conocida como coflatten) que expande una estructura de datos.

Los casos de uso interesantes para Cobind son raros, aunque cuando son mostrados en la tabla de permutación de la tabla Functor (para F[_], A, y B) es difícil discutir porqué cualquier método debería ser menos importante que los otros:

método parámetro
map A => B
contramap B => A
xmap (A => B, B => A)
ap F[A => B]
bind A => F[B]
cobind F[A] => B

5.11.2 Comonad

  @typeclass trait Comonad[F[_]] extends Cobind[F] {
    def copoint[A](p: F[A]): A
  //def   point[A](a: =>A): F[A]
  }

.copoint (también coonocido como .copure) desenvuelve un elemento de su contexto. Los efectos no tienen típicamente una instancia de Comonad dado que se violaría la transparencia referencial al interpretar una IO[A] en una A. Pero para estructuras de datos similares a colecciones, es una forma de construir una vista de todos los elementos al mismo tiempo que de su vecindad.

Considere la vecindad de una lista que contiene todos los elementos a la izquierda de un elemento (lefts), el elemento en sí mismo (el focus), y todos los elementos a su derecha (rights).

  final case class Hood[A](lefts: IList[A], focus: A, rights: IList[A])

Los lefts y los rights deberían ordenarse con el elemento más cercano al focus en la cabeza, de modo que sea posible recuperar la lista original IList por medio de .toList.

  object Hood {
    implicit class Ops[A](hood: Hood[A]) {
      def toIList: IList[A] = hood.lefts.reverse ::: hood.focus :: hood.rights

Podemos escribir métodos que nos dejen mover el foco una posición a la izquierda (previous), y una posición a la derecha (next)

  ...
      def previous: Maybe[Hood[A]] = hood.lefts match {
        case INil() => Empty()
        case ICons(head, tail) =>
          Just(Hood(tail, head, hood.focus :: hood.rights))
      }
      def next: Maybe[Hood[A]] = hood.rights match {
        case INil() => Empty()
        case ICons(head, tail) =>
          Just(Hood(hood.focus :: hood.lefts, head, tail))
      }

Mediante la introducción de more para aplicar repetidamente una función opcional a Hood podemos calcular todas las positions (posiciones) que Hood puede tomar en la lista

  ...
      def more(f: Hood[A] => Maybe[Hood[A]]): IList[Hood[A]] =
        f(hood) match {
          case Empty() => INil()
          case Just(r) => ICons(r, r.more(f))
        }
      def positions: Hood[Hood[A]] = {
        val left  = hood.more(_.previous)
        val right = hood.more(_.next)
        Hood(left, hood, right)
      }
    }

Ahora podemos implementar una Comonad[Hood]

  ...
    implicit val comonad: Comonad[Hood] = new Comonad[Hood] {
      def map[A, B](fa: Hood[A])(f: A => B): Hood[B] =
        Hood(fa.lefts.map(f), f(fa.focus), fa.rights.map(f))
      def cobind[A, B](fa: Hood[A])(f: Hood[A] => B): Hood[B] =
        fa.positions.map(f)
      def copoint[A](fa: Hood[A]): A = fa.focus
    }
  }

cojoin nos da proporciona una Hood[Hood[IList]] que contiene todas las posibles vecindades en nuestra IList.

  scala> val middle = Hood(IList(4, 3, 2, 1), 5, IList(6, 7, 8, 9))
  scala> middle.cojoin
  res = Hood(
          [Hood([3,2,1],4,[5,6,7,8,9]),
           Hood([2,1],3,[4,5,6,7,8,9]),
           Hood([1],2,[3,4,5,6,7,8,9]),
           Hood([],1,[2,3,4,5,6,7,8,9])],
          Hood([4,3,2,1],5,[6,7,8,9]),
          [Hood([5,4,3,2,1],6,[7,8,9]),
           Hood([6,5,4,3,2,1],7,[8,9]),
           Hood([7,6,5,4,3,2,1],8,[9]),
           Hood([8,7,6,5,4,3,2,1],9,[])])

En verdad, ¡cojoin es simplemente positions! Podríamos hacer un override con una implementación más directa y eficiente

  override def cojoin[A](fa: Hood[A]): Hood[Hood[A]] = fa.positions

Comonad generaliza el concepto de Hood a estructuras de datos arbitrarias. Hood es un ejemplo de un zipper (que no está relacionado a Zip). Scalaz viene con un tipo de datos Zipper para los streams (es decir , estructuras de datos infinitas unidimensionales), que discutiremos en el siguiente capítulo.

Una aplicación de zipper es para automatas celulares, que calculan el valor de cada celda en la siguiente generación mediante realizar un cómputo basándose en la vecindad de dicha celda.

5.11.3 Cozip

  @typeclass trait Cozip[F[_]] {
    def cozip[A, B](x: F[A \/ B]): F[A] \/ F[B]
  //def   zip[A, B](a: =>F[A], b: =>F[B]): F[(A, B)]
  //def unzip[A, B](a: F[(A, B)]): (F[A], F[B])

    def cozip3[A, B, C](x: F[A \/ (B \/ C)]): F[A] \/ (F[B] \/ F[C]) = ...
    ...
    def cozip7[A ... H](x: F[(A \/ (... H))]): F[A] \/ (... F[H]) = ...
  }

Aunque se llame cozip, quizá es más apropiado enfocar nuestra atención en su simetría con unzip. Mientras que unzip divide F[_] de tuplas (productos) en tuplas de F[_], cozip divide F[_] de disjunciones (coproductos) en disjunciones de F[_].

5.12 Bi-cosas

Algunas veces podríamos encontrar una cosa que tiene dos hoyos de tipo y deseemos realizar un map en ambos lados. Por ejemplo, podríamos estar rastreando las fallas a la izquierda de un Either y tal vez querríamos hacer algo con los mensajes de error.

La typeclass Functor/Foldable/Traverse tienen parientes extraños que nos permiten hacer un mapeo de ambas maneras.

  @typeclass trait Bifunctor[F[_, _]] {
    def bimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): F[C, D]

    @op("<-:") def leftMap[A, B, C](fab: F[A, B])(f: A => C): F[C, B] = ...
    @op(":->") def rightMap[A, B, D](fab: F[A, B])(g: B => D): F[A, D] = ...
    @op("<:>") def umap[A, B](faa: F[A, A])(f: A => B): F[B, B] = ...
  }

  @typeclass trait Bifoldable[F[_, _]] {
    def bifoldMap[A, B, M: Monoid](fa: F[A, B])(f: A => M)(g: B => M): M

    def bifoldRight[A,B,C](fa: F[A, B], z: =>C)(f: (A, =>C) => C)(g: (B, =>C) => C): C
    def bifoldLeft[A,B,C](fa: F[A, B], z: C)(f: (C, A) => C)(g: (C, B) => C): C = ...

    def bifoldMap1[A, B, M: Semigroup](fa: F[A,B])(f: A => M)(g: B => M): Option[M] = ...
  }

  @typeclass trait Bitraverse[F[_, _]] extends Bifunctor[F] with Bifoldable[F] {
    def bitraverse[G[_]: Applicative, A, B, C, D](fab: F[A, B])
                                                 (f: A => G[C])
                                                 (g: B => G[D]): G[F[C, D]]

    def bisequence[G[_]: Applicative, A, B](x: F[G[A], G[B]]): G[F[A, B]] = ...
  }

Aunque las signaturas de tipo son verbosas, no son más que los métodos esenciales de Functor, Foldable, y Bitraverse que toman dos funciones en vez de una sola, con frecuencia requiriendo que ambas funciones devuelvan el mismo tipo de modo que sus resultados puedan ser combinados con un Monoid o un Semigroup.

  scala> val a: Either[String, Int] = Left("fail")
         val b: Either[String, Int] = Right(13)

  scala> b.bimap(_.toUpperCase, _ * 2)
  res: Either[String, Int] = Right(26)

  scala> a.bimap(_.toUpperCase, _ * 2)
  res: Either[String, Int] = Left(FAIL)

  scala> b :-> (_ * 2)
  res: Either[String,Int] = Right(26)

  scala> a :-> (_ * 2)
  res: Either[String, Int] = Left(fail)

  scala> { s: String => s.length } <-: a
  res: Either[Int, Int] = Left(4)

  scala> a.bifoldMap(_.length)(identity)
  res: Int = 4

  scala> b.bitraverse(s => Future(s.length), i => Future(i))
  res: Future[Either[Int, Int]] = Future(<not completed>)

Adicionalmente, podemos revisitar MonadPlus (recuerde que se trata de un Monad con la habilidad extra de realizar un filterWith y unite) y ver que puede separar el contenido Bifoldable de un Monad.

  @typeclass trait MonadPlus[F[_]] {
    ...
    def separate[G[_, _]: Bifoldable, A, B](value: F[G[A, B]]): (F[A], F[B]) = ...
    ...
  }

Esto es muy útil se tenemos una colección de bi-cosas y desamos reorganizarlas en una colección de\ A y una colección de B.

  scala> val list: List[Either[Int, String]] =
           List(Right("hello"), Left(1), Left(2), Right("world"))

  scala> list.separate
  res: (List[Int], List[String]) = (List(1, 2), List(hello, world))

5.13 Resumen

!Esto fue bastante material! Hemos apenas explorado la librería estándar de funcionalidad polimórfica. Pero para poner el asunto en perspectiva: hay más traits en la API de collecciones de la librería estándar de Scala que typeclasses en Scalaz.

Es normal que una aplicación de PF usar un porcentaje pequeño de la jerarquía de tipos, con la mayoría de su funcionalidad viniendo de álgebras particulares del dominio y de typeclasses. Inclusive si las typeclasses del dominio específico son simples clones especializados de algo que ya existe en Scalaz, está bien refactorizarlo después.

Para ayudar al lector, hemos incluído un sumario de las typeclasses y sus métodos primarios en el apéndice, tomando inspiración del sumario cuyo autor es Adam Rosien’s: Scalaz Cheatsheet.

Para ayudarle, Valentin Kasas explica cómo combinar Ncosas:

6. Tipos de datos de Scalaz

¿Quién no ama una buena estructura de datos? La respuesta es nadie, porque las estructuras de datos son increíbles.

En este capítulo exploraremos los tipos de datos en Scalaz que son similares a colecciones, así como los tipos de datos que aumentan Scala, como lenguaje, con semántica y seguridad de tipos adicional.

La razón principal por la que nos preocupamos por tener muchas colecciones a nuestra disposición es por rendimiento. Un vector y una lista pueden hacer lo mismo, pero sus características son diferentes: un vector tiene un costo de búsqueda constante mientras que una lista debe ser recorrida.

Todas las colecciones presentadas aquí son persistentes: si agregamos o removemos un elemento todavía podemos usar la versión anterior. Compartir estructuralmente es esencial para el rendimiento de las estructuras de datos persistentes, de otra manera la colección entera se reconstruye con cada operación.

A diferencia de las colecciones de Java y Scala, no hay jerarquía de datos en Scalaz: estas colecciones son mucho más simples de entender. La funcionalidad polimórfica se proporciona por instancias opmitizadas de typeclases que estudiamos en el capítulo anterior. Esto simplifica cambiar implementaciones por razones de rendimiento, y proporcionar la propia.

6.1 Variancia de tipo

Muchos de los tipos de datos de Scalaz son invariantes en sus parámetros de tipo. Por ejemplo, IList[A] no es un subtipo de IList[B] cuando A <: B.

6.1.1 Covariancia

El problema con los tipos de parámetro covariantes, tales como class List[+A], es que List[A] es un subtipo de List[Any] y es fácil perder accidentalmente información de tipo.

  scala> List("hello") ++ List(' ') ++ List("world!")
  res: List[Any] = List(hello,  , world!)

Note que la segunda lista es una List[Char] y que el compilador ha inferido incorrectamente el LUB (Least Upper Bound, o límite mínimo inferior) es Any. Compare con IList, que requiere una aplicación explícita de .widen[Any] para ejecutar el crimen atroz:

  scala> IList("hello") ++ IList(' ') ++ IList("world!")
  <console>:35: error: type mismatch;
   found   : Char(' ')
   required: String

  scala> IList("hello").widen[Any]
           ++ IList(' ').widen[Any]
           ++ IList("world!").widen[Any]
  res: IList[Any] = [hello, ,world!]

De manera similar, cuando el compilador infiere el tipo como with Product with Serializable es un indicador fuerte de que un ensanchamiento accidental ha ocurrido debido a la covariancia.

Desafortunadamente debemos ser cuidadosos cuando construimos tipos de datos invariantes debido a que los cálculos LUB se realizan sobre los parámetros:

  scala> IList("hello", ' ', "world")
  res: IList[Any] = [hello, ,world]

Otro problema similar surge a partir del tipo Nothing, el cuál es un subtipo del resto de tipos, incluyendo ADTs selladas (sealed), clases finales (final), primitivas y null.

No hay valores de tipo Nothing: las funciones que toman un Nothing como un parámetro no pueden ejecutarse y las funciones que devuelven Nothing nunca devuelven un valor (o terminan). Nothing fue introducido como un mecanismo para habilitar los tipos de parámetros covariantes, pero una consecuencia es que podemos escribir código que no puede ejecutarse, por accidente. Scalaz dice que no necesitamos parámetros de tipo covariantes lo que significa que nos estamos limitando a nosotros mismos a escribir código práctico que puede ser ejecutado.

6.1.2 Contrarivarianza

Por otro lado, los tipos de parámetro contravariantes, tales como trait Thing[-A] pueden poner de manifiesto errores devastadores en el compilador. Considere la demostración de Paul Phillips’ (ex miembro del equipo del compilador scalac) de lo que él llama contrarivarianza:

  scala> :paste
         trait Thing[-A]
         def f(x: Thing[ Seq[Int]]): Byte   = 1
         def f(x: Thing[List[Int]]): Short  = 2

  scala> f(new Thing[ Seq[Int]] { })
         f(new Thing[List[Int]] { })

  res = 1
  res = 2

Como era de esperarse, el compilador está encontrando el argumento más específico en cada invocación de f. Sin embargo, la resolución implícita da resultados inesperados:

  scala> :paste
         implicit val t1: Thing[ Seq[Int]] =
           new Thing[ Seq[Int]] { override def toString = "1" }
         implicit val t2: Thing[List[Int]] =
           new Thing[List[Int]] { override def toString = "2" }

  scala> implicitly[Thing[ Seq[Int]]]
         implicitly[Thing[List[Int]]]

  res = 1
  res = 1

La resolución implícita hace un intercambio de su definición de “más específico” para los tipos contravariantes, logrando que sean inútiles para los typeclases o para cualquier cosa que requiera funcionalidad polimórfica. Este comportamiento es fijo en Dotty.

6.1.3 Las limitaciones del mecanismo de subclases

scala.Option tiene un método .flatten que los dejará convertir Option[Option[B]] en un Option[B]. Sin embargo, el sistema de tipos de Scala es incapaz de dejarnos escribir la signatura de tipos requerida. Considere lo siguiente que parece correcto, pero que tiene un error sutil:

  sealed abstract class Option[+A] {
    def flatten[B, A <: Option[B]]: Option[B] = ...
  }

The A introducida en .flatten está ocultando la A introducida por la clase. Es equivalente a escribir

  sealed abstract class Option[+A] {
    def flatten[B, C <: Option[B]]: Option[B] = ...
  }

que no es la restricción deseada.

Para resolver esta limitación, Scala define clases infijas <:< y =:= junto con la evidencia implícita que siempre crea un testigo (witness).

  sealed abstract class <:<[-From, +To] extends (From => To)
  implicit def conforms[A]: A <:< A = new <:<[A, A] { def apply(x: A): A = x }

  sealed abstract class =:=[ From,  To] extends (From => To)
  implicit def tpEquals[A]: A =:= A = new =:=[A, A] { def apply(x: A): A = x }

=:= puede usarse para requerir que dos parámetros de tipo sean exactamente iguales y <:< se usa para describir relaciones de subtipo, dejandonos implementar .flatten como

  sealed abstract class Option[+A] {
    def flatten[B](implicit ev: A <:< Option[B]): Option[B] = this match {
      case None        => None
      case Some(value) => ev(value)
    }
  }
  final case class Some[+A](value: A) extends Option[A]
  case object None                    extends Option[Nothing]

Scalaz mejora sobre <:< y =:= con Liskov (que tiene el alias <~<) y Leibniz (====).

  sealed abstract class Liskov[-A, +B] {
    def apply(a: A): B = ...
    def subst[F[-_]](p: F[B]): F[A]

    def andThen[C](that: Liskov[B, C]): Liskov[A, C] = ...
    def onF[X](fa: X => A): X => B = ...
    ...
  }
  object Liskov {
    type <~<[-A, +B] = Liskov[A, B]
    type >~>[+B, -A] = Liskov[A, B]

    implicit def refl[A]: (A <~< A) = ...
    implicit def isa[A, B >: A]: A <~< B = ...

    implicit def witness[A, B](lt: A <~< B): A => B = ...
    ...
  }

  // type signatures have been simplified
  sealed abstract class Leibniz[A, B] {
    def apply(a: A): B = ...
    def subst[F[_]](p: F[A]): F[B]

    def flip: Leibniz[B, A] = ...
    def andThen[C](that: Leibniz[B, C]): Leibniz[A, C] = ...
    def onF[X](fa: X => A): X => B = ...
    ...
  }
  object Leibniz {
    type ===[A, B] = Leibniz[A, B]

    implicit def refl[A]: Leibniz[A, A] = ...

    implicit def subst[A, B](a: A)(implicit f: A === B): B = ...
    implicit def witness[A, B](f: A === B): A => B = ...
    ...
  }

Además de los métodos generalmente útiles y las conversiones implícitas, la evidencia de Scalaz <~< y === usa mejores principios que en la librería estándar.

6.2 Evaluación

Java es un lenguaje con evaluación estricta: todos los parámetros para un método deben evaluarse a un valor antes de llamar el método. Scala introduce la noción de parámetros por nombre (by-name) en los métodos con la sintaxis a: => A. Estos parámetros se envuelven en una finción de cero argumentos que se invoca cada vez que se referencia la a. Hemos visto por nombre muchas veces en las typeclases.

Scala también tiene por necesidad (by-need), con la palabra lazy: el cómputo se evalúa a lo sumo una vez para producir el valor. Desgraciadamente, Scala no soporta la evaluación por necesidad de los parámetros del método.

Scalaz formaliza los tres estrategias de evaluación con un ADT

  sealed abstract class Name[A] {
    def value: A
  }
  object Name {
    def apply[A](a: =>A) = new Name[A] { def value = a }
    ...
  }

  sealed abstract class Need[A] extends Name[A]
  object Need {
    def apply[A](a: =>A): Need[A] = new Need[A] {
      private lazy val value0: A = a
      def value = value0
    }
    ...
  }

  final case class Value[A](value: A) extends Need[A]

La forma de evaluación más débil es Name, que no proporciona garantías computacionales. A continuación está Need, que garantiza la evaluación como máximo una vez.

Si deseamos ser super escrupulosos podríamos revisar todas las typeclases y hacer sus métodos tomar parámetros por Name, Need o Value. En vez de esto, podemos asumir que los parámetros normales siempre pueden envolverse en un Value, y que los parámetros (by-name) por nombre pueden envolverse con Name.

Cuando escribimos programas puros, somos libres de reemplazar cualquier Name con Need o Value, y viceversa, sin necesidad de cambiar lo correcto del programa. Esta es la esencia de la transparencia referencial: la habilidad de cambiar un cómputo por su valor, o un valor por su respectivo cómputo.

En la programación funcional casi siempre deseamos Value o Need (también conocido como estricto y perezoso): hay poco valor en Name. Debido a que no hay soporte a nivel de lenguaje para parámetros de método perezosos, los métodos típicamente solicitan un parámetro por nombre y entonces lo convierten a Need de manera interna, proporcionando un aumento en el rendimiento.

Name proporciona instancias de las siguientes typeclases

  • Monad
  • Comonad
  • Traverse1
  • Align
  • Zip / Unzip / Cozip

6.3 Memoisation

Scalaz tiene la capacidad de memoizar funciones, formalizada por Memo, que no hace ninguna garantía sobre la evaluación debido a la diversidad de implementaciones:

  sealed abstract class Memo[K, V] {
    def apply(z: K => V): K => V
  }
  object Memo {
    def memo[K, V](f: (K => V) => K => V): Memo[K, V]

    def nilMemo[K, V]: Memo[K, V] = memo[K, V](identity)

    def arrayMemo[V >: Null : ClassTag](n: Int): Memo[Int, V] = ...
    def doubleArrayMemo(n: Int, sentinel: Double = 0.0): Memo[Int, Double] = ...

    def immutableHashMapMemo[K, V]: Memo[K, V] = ...
    def immutableTreeMapMemo[K: scala.Ordering, V]: Memo[K, V] = ...
  }

memo nos permite crear implementaciones de Memo, nilMemo no memoiza, evaluando la función normalmente. Las implementaciones resultantes interceptan llamadas a la función y guardan en cache los resultados obtenidos en una implementación que usa colecciones de la librería estándar.

Para usar Memo simplemente envolvemos una función con una implementación de Memo y entonces invocamos a la función memoizada:

  scala> def foo(n: Int): String = {
           println("running")
           if (n > 10) "wibble" else "wobble"
         }

  scala> val mem = Memo.arrayMemo[String](100)
         val mfoo = mem(foo)

  scala> mfoo(1)
  running // evaluated
  res: String = wobble

  scala> mfoo(1)
  res: String = wobble // memoised

Si la función toma más de un parámetro, entonces debemos invocar .tupled sobre el método, con la versión memoisada tomando una tupla.

  scala> def bar(n: Int, m: Int): String = "hello"
         val mem = Memo.immutableHashMapMemo[(Int, Int), String]
         val mbar = mem((bar _).tupled)

  scala> mbar((1, 2))
  res: String = "hello"

Memo típicamente se trata como un artificio especial y la regla usual sobre la pureza se relaja para las implementaciones. La pureza únicamente requiere que nuestras implementaciones de Memo sean referencialmente transparentes en la evaluación de K => V. Podríamos usar datos mutables y realizar I/O en la implementación de Memo, por ejemplo usando un LRU o un caché distribuido, sin tener que declarar un efecto en las signaturas de tipo. Otros lenguajes de programación funcional tienen memoización manejada por el ambiente de runtime y Memo es nuestra manera de extender la JVM para tener un soporte similar, aunque desgraciadamente únicamente de una manera que requiere opt-in.

6.4 Tagging (etiquetar)

En la sección que se introdujo Monoid construimos un Monoid[TradeTeamplate] y nos dimos cuenta de que Scalaz no hace lo que desearíamos con Monoid[Option[A]]. Este no es una omisión de Scalaz, con frecuencia podemos notar que un tipo de datos puede implementar una tipeclass fundamental en múltiples formas válidas y que la implementación por default no hace lo que deseariamos, o simplemente no está definida.

Los ejemplos básicos son Monoid[Boolean] (la conjunción && vs la disjunción ||) y Monoid[Int] (multiplicación vs adición).

Para implementar Monoid[TradeTemplate] tuvimos que romper la coherencia de typeclases, o usar una typeclass distinta.

scalaz.Tag está diseñada para lidiar con el problema de coherencia que surge con múltiples implementaciones de typeclases, sin romper la coherencia de las typeclases.

La definición es algo compleja, pero la sintáxis al usar scalaz.Tag es bastante limpia. Así es como logramos convencer al compilador para que nos permita definir el tipo infijo A @@ T que es borrado en A en tiempo de ejecución:

  type @@[A, T] = Tag.k.@@[A, T]

  object Tag {
    @inline val k: TagKind = IdTagKind
    @inline def apply[A, T](a: A): A @@ T = k(a)
    ...

    final class TagOf[T] private[Tag]() { ... }
    def of[T]: TagOf[T] = new TagOf[T]
  }
  sealed abstract class TagKind {
    type @@[A, T]
    def apply[A, T](a: A): A @@ T
    ...
  }
  private[scalaz] object IdTagKind extends TagKind {
    type @@[A, T] = A
    @inline override def apply[A, T](a: A): A = a
    ...
  }

Algunas etiquetas/tags útiles se proporcionan en el objeto Tags

  object Tags {
    sealed trait First
    val First = Tag.of[First]

    sealed trait Last
    val Last = Tag.of[Last]

    sealed trait Multiplication
    val Multiplication = Tag.of[Multiplication]

    sealed trait Disjunction
    val Disjunction = Tag.of[Disjunction]

    sealed trait Conjunction
    val Conjunction = Tag.of[Conjunction]

    ...
  }

First/Last se usan para seleccionar instancias d Monoid que escogen el primero o el último (diferente de cero) operando. Multiplication es para multiplicación numérica en vez de adición. Disjunction/Conjuntion son para seleccionar && o ||, respectivamente.

En nuestro TradeTemplate, en lugar de usar Option[Currency] podríamos usar Option[Currency] @@ Tags.Last. En verdad, este uso es tan común que podemos usar el alias previamente definido, LastOption.

  type LastOption[A] = Option[A] @@ Tags.Last

y esto nos permite escribir una implementación mucho más limpia de Monoid[TradeTemplate]

  final case class TradeTemplate(
    payments: List[java.time.LocalDate],
    ccy: LastOption[Currency],
    otc: LastOption[Boolean]
  )
  object TradeTemplate {
    implicit val monoid: Monoid[TradeTemplate] = Monoid.instance(
      (a, b) =>
        TradeTemplate(a.payments |+| b.payments,
                      a.ccy |+| b.ccy,
                      a.otc |+| b.otc),
        TradeTemplate(Nil, Tag(None), Tag(None))
    )
  }

Para crear un valor de tipo LastOption, aplicamos Tag a un Option. Aquí estamos invocando Tag(None).

En el capítulo sobre derivación de typeclases, iremos un paso más allá y derivaremos automáticamente el monoid.

Es tentador usar Tag para marcar tipos de datos para alguna forma de validación (por ejemplo, String @@ PersonName), pero deberíamos evitar esto porque no existe un chequeo del contenido en tiempo de ejecución. Tag únicamente debería ser usado para propósitos de selección de typeclases. Prefiera el uso de la librería Refined, que se introdujo en el Capítulo 4, para restringir valores.

6.5 Transformaciones naturales

Una función de un tipo a otro se escribe en Scala como A => B, y se trata de una conveniencia sintáctica para Function1[A, B]. Scalaz proporciona una conveniencia sintáctica F ~> G para funciones sobre los constructores de tipo F[_] a G[_].

Esta notación, F ~ G, se conoce como transformación natural y son universalmente cuantificables debido a que no nos importa el contenido de F[_].

  type ~>[-F[_], +G[_]] = NaturalTransformation[F, G]
  trait NaturalTransformation[-F[_], +G[_]] {
    def apply[A](fa: F[A]): G[A]

    def compose[E[_]](f: E ~> F): E ~> G = ...
    def andThen[H[_]](f: G ~> H): F ~> H = ...
  }

Un ejemplo de una transformación natural es la función que convierte una IList en una List.

  scala> val convert = new (IList ~> List) {
           def apply[A](fa: IList[A]): List[A] = fa.toList
         }

  scala> convert(IList(1, 2, 3))
  res: List[Int] = List(1, 2, 3)

O, de manera más concisa, usando las conveniencias sintácticas proporcionadas por el plugin kind-projector:

  scala> val convert = λ[IList ~> List](_.toList)

  scala> val convert = Lambda[IList ~> List](_.toList)

Sin embargo, en el desarrollo del día a día, es mucho más probable que usemos una transformación natural para mapear entre álgebras. Por ejemplo, en drone-dynamic-agents tal vez deseemos implementar nuestra álgebra de Machines que usa Google Container Engine con una álgebra ad-hoc, BigMachines. En lugar de cambiar toda nuestra lógica de negocio y probar usando esta nueva interfaz BigMachines, podríamos escribir una transformación natural de Machines ~> BigMachines. Volveremos a esta idea en el capítulo de Mónadas Avanzadas.

6.6 Isomorphism (isomorfismos)

Algunas veces tenemos dos tipos que en realidad son equivalentes (la misma cosa), lo que ocasiona problemas de compatibiliad debido a que el compilador no sabe lo que nosotros sí sabemos. Esto pasa típicamente cuando usamos código de terceros que tiene algo que ya tenemos.

Ahí es cuando Isomorphism nos puede ayudar. Un isomorfismo define una relación formal “es equivalente a” entre dos tipos. Existen tres variantes, para

  object Isomorphism {
    trait Iso[Arr[_, _], A, B] {
      def to: Arr[A, B]
      def from: Arr[B, A]
    }
    type IsoSet[A, B] = Iso[Function1, A, B]
    type <=>[A, B] = IsoSet[A, B]
    object IsoSet {
      def apply[A, B](to: A => B, from: B => A): A <=> B = ...
    }

    trait Iso2[Arr[_[_], _[_]], F[_], G[_]] {
      def to: Arr[F, G]
      def from: Arr[G, F]
    }
    type IsoFunctor[F[_], G[_]] = Iso2[NaturalTransformation, F, G]
    type <~>[F[_], G[_]] = IsoFunctor[F, G]
    object IsoFunctor {
      def apply[F[_], G[_]](to: F ~> G, from: G ~> F): F <~> G = ...
    }

    trait Iso3[Arr[_[_, _], _[_, _]], F[_, _], G[_, _]] {
      def to: Arr[F, G]
      def from: Arr[G, F]
    }
    type IsoBifunctor[F[_, _], G[_, _]] = Iso3[~~>, F, G]
    type <~~>[F[_, _], G[_, _]] = IsoBifunctor[F, G]

    ...
  }

Los aliases de tipos IsoSet, IsoFunctor e IsoBifunctor cubren los casos comunes: una función regular, una transformación natural y binatural. Las funciones de conveniencia nos permiten generar instancias de funciones existentes o transformaciones naturales. Sin embargo, con frecuencia es más fácil usar una de las clases abstractas Template para definir un isomorfismo. Por ejemplo:

  val listIListIso: List <~> IList =
    new IsoFunctorTemplate[List, IList] {
      def to[A](fa: List[A]) = fromList(fa)
      def from[A](fa: IList[A]) = fa.toList
    }

Si introducimos un isomorfismo, con frecuencia podemos generar muchas de las typeclases estándar. Por ejemplo,

  trait IsomorphismSemigroup[F, G] extends Semigroup[F] {
    implicit def G: Semigroup[G]
    def iso: F <=> G
    def append(f1: F, f2: =>F): F = iso.from(G.append(iso.to(f1), iso.to(f2)))
  }

lo que nos permite derivar un Semigroup[F] para un tipo F si ya tenemos un isomorfismo F <=> G y un semigrupo Semigroup[G]. Casi todas las typeclases en la jerarquía proporcionan una invariante isomórfica. Si nos encontramos en la situación en la que copiamos y pegamos una implementación de una typeclass, es útil considerar si Isomorphism es una mejor solución.

6.7 Contenedores

6.7.1 Maybe

Ya nos hemos encontrado con la mejora de Scalaz sobre scala.Option, que se llama Maybe. Es una mejora porque es invariante y no tiene ningún método inseguro como Option.get, el cual puede lanzar una excepción.

Con frecuencia se usa para representar una cosa que puede o no estar presente sin proporcionar ninguna información adicional de porqué falta información.

  sealed abstract class Maybe[A] { ... }
  object Maybe {
    final case class Empty[A]()    extends Maybe[A]
    final case class Just[A](a: A) extends Maybe[A]

    def empty[A]: Maybe[A] = Empty()
    def just[A](a: A): Maybe[A] = Just(a)

    def fromOption[A](oa: Option[A]): Maybe[A] = ...
    def fromNullable[A](a: A): Maybe[A] = if (null == a) empty else just(a)
    ...
  }

Los métodos .empty y .just en el objeto compañero son preferidas en lugar de usar Empty o Just debido a que regresan Maybe, ayudándonos con la inferencia de tipos. Este patrón con frecuencia es preferido a regresar un tipo suma, que es cuando tenemos múltiples implementaciones de un sealed trait pero nunca usamos un subtipo específico en una signatura de método.

Una clase implícita conveniente nos permite invocar.just sobre cualquier valor y recibir un Maybe.

  implicit class MaybeOps[A](self: A) {
    def just: Maybe[A] = Maybe.just(self)
  }

Maybe tiene una instancia de typeclass para todas las cosas

  • Align
  • Traverse
  • MonadPlus / IsEmpty
  • Cobind
  • Cozip / Zip / Unzip
  • Optional

e instancias delegadas que dependen de A

  • Monoid / Band
  • Equal / Order / Show

Además de las instancias arriba mencionadas, Maybe tiene funcionalidad que no es soportada por una typeclass polimórfica.

  sealed abstract class Maybe[A] {
    def cata[B](f: A => B, b: =>B): B = this match {
      case Just(a) => f(a)
      case Empty() => b
    }

    def |(a: =>A): A = cata(identity, a)
    def toLeft[B](b: =>B): A \/ B = cata(\/.left, \/-(b))
    def toRight[B](b: =>B): B \/ A = cata(\/.right, -\/(b))
    def <\/[B](b: =>B): A \/ B = toLeft(b)
    def \/>[B](b: =>B): B \/ A = toRight(b)

    def orZero(implicit A: Monoid[A]): A = getOrElse(A.zero)
    def orEmpty[F[_]: Applicative: PlusEmpty]: F[A] =
      cata(Applicative[F].point(_), PlusEmpty[F].empty)
    ...
  }

.cata proporciona una alternativa más simple para .map(f).gerOrElse(b) y tiene la forma más simple | si el mapeo es identity (es decir, simplemente .getOrElse).

.toLeft y .toRight, y sus aliases simbólicos, crean una disjunción (explicada en la sección siguiente) al tomar un valor por default para el caso Empty.

.orZero toma un Monoid para definir el valor por defecto.

.orEmpty usa un ApplicativePlus para crear un elemento único o un contenedor vacío, sin olvidar que ya tenemos soporte para las colecciones de la librería estándar con el método .to que viene de la instancia Foldable.

  scala> 1.just.orZero
  res: Int = 1

  scala> Maybe.empty[Int].orZero
  res: Int = 0

  scala> Maybe.empty[Int].orEmpty[IList]
  res: IList[Int] = []

  scala> 1.just.orEmpty[IList]
  res: IList[Int] = [1]

  scala> 1.just.to[List] // from Foldable
  res: List[Int] = List(1)

6.7.2 Either

La mejora de Scalaz sobre scalaz.Either es simbólica, pero es común hablar de este caso como either o una Disjunction.

  sealed abstract class \/[+A, +B] { ... }
  final case class -\/[+A](a: A) extends (A \/ Nothing)
  final case class \/-[+B](b: B) extends (Nothing \/ B)

  type Disjunction[+A, +B] = \/[A, B]

  object \/ {
    def left [A, B]: A => A \/ B = -\/(_)
    def right[A, B]: B => A \/ B = \/-(_)

    def fromEither[A, B](e: Either[A, B]): A \/ B = ...
    ...
  }

con la sintaxis correspondiente

  implicit class EitherOps[A](val self: A) {
    final def left [B]: (A \/ B) = -\/(self)
    final def right[B]: (B \/ A) = \/-(self)
  }

y permite una construcción sencilla de los valores. Note que los métodos de extensión toman el tipo del otro lado. De modo que si deseamos crear un valor de tipo String \/ Int y tenemos un Int, debemos pasar String cuando llamamos .right.

  scala> 1.right[String]
  res: String \/ Int = \/-(1)

  scala> "hello".left[Int]
  res: String \/ Int = -\/(hello)

La naturaleza simbólica de \/ hace que sea de fácil lectura cuando se muestra con notación infija. Note que los tipos simbólicos en Scala se asocian desde el lado izquierdo y que \/ deben tener paréntesis, por ejemplo (A \/ (B \/ (C \/ D)).

\/ tiene instancias sesgadas a la derecha (es decir, flatMap aplica a \/-) para:

  • Monad / MonadError
  • Traverse / Bitraverse
  • Plus
  • Optional
  • Cozip

y dependiendo del contenido

  • Equal / Order
  • Semigroup / Monoid / Band

Además, hay métodos especiales

  sealed abstract class \/[+A, +B] { self =>
    def fold[X](l: A => X, r: B => X): X = self match {
      case -\/(a) => l(a)
      case \/-(b) => r(b)
    }

    def swap: (B \/ A) = self match {
      case -\/(a) => \/-(a)
      case \/-(b) => -\/(b)
    }

    def |[BB >: B](x: =>BB): BB = getOrElse(x) // Optional[_]
    def |||[C, BB >: B](x: =>C \/ BB): C \/ BB = orElse(x) // Optional[_]

    def +++[AA >: A: Semigroup, BB >: B: Semigroup](x: =>AA \/ BB): AA \/ BB = ...

    def toEither: Either[A, B] = ...

    final class SwitchingDisjunction[X](right: =>X) {
      def <<?:(left: =>X): X = ...
    }
    def :?>>[X](right: =>X) = new SwitchingDisjunction[X](right)
    ...
  }

.fold es similar a Maybe.cata y requiere que tanto el lado derecho como el lado izquierdo se mapeen al mismo tipo.

.swap intercambia los lados izquierdo y derecho.

El alias | para getOrElse parece similar a Maybe.

The | alias to getOrElse appears similarly to Maybe. We also get ||| as an alias to orElse.

+++ es para combinar disjunciones con los lados izquierdos tomando precedencia sobre los lados derechos:

  • right(v1) +++ right(v2) da right(v1 |+| v2)
  • right(v1) +++ left (v2) da left (v2)
  • left (v1) +++ right(v2) da left (v1)
  • left (v1) +++ left (v2) da left (v1 |+| v2)

.toEither está para proporcionar compatibilidad hacia atrás con la librería estándar de Scala.

La combinación de :?>> y <<? son una sintáxis conveniente para ignorar el contenido de una \/, pero escoger un valor por default basándose en su tipo.

  scala> 1 <<?: foo :?>> 2
  res: Int = 2 // foo is a \/-

  scala> 1 <<?: foo.swap :?>> 2
  res: Int = 1

6.7.3 Validation

A primera vista, Validation (con alias simbólico \?/, Elvis feliz) aparece ser un clon de Disjunction:

  sealed abstract class Validation[+E, +A] { ... }
  final case class Success[A](a: A) extends Validation[Nothing, A]
  final case class Failure[E](e: E) extends Validation[E, Nothing]

  type ValidationNel[E, +X] = Validation[NonEmptyList[E], X]

  object Validation {
    type \?/[+E, +A] = Validation[E, A]

    def success[E, A]: A => Validation[E, A] = Success(_)
    def failure[E, A]: E => Validation[E, A] = Failure(_)
    def failureNel[E, A](e: E): ValidationNel[E, A] = Failure(NonEmptyList(e))

    def lift[E, A](a: A)(f: A => Boolean, fail: E): Validation[E, A] = ...
    def liftNel[E, A](a: A)(f: A => Boolean, fail: E): ValidationNel[E, A] = ...
    def fromEither[E, A](e: Either[E, A]): Validation[E, A] = ...
    ...
  }

Con sintáxis conveniente

  implicit class ValidationOps[A](self: A) {
    def success[X]: Validation[X, A] = Validation.success[X, A](self)
    def successNel[X]: ValidationNel[X, A] = success
    def failure[X]: Validation[A, X] = Validation.failure[A, X](self)
    def failureNel[X]: ValidationNel[A, X] = Validation.failureNel[A, X](self)
  }

Sin embargo, la estructura de datos no es la historia completa. En Scalaz, Validation no tiene una instancia de ninguna Monad, y esto es intencional, restringiéndose a versiones sesgadas a la derecha de:

  • Applicative
  • Traverse / Bitraverse
  • Cozip
  • Plus
  • Optional

y dependiendo del contenido

  • Equal / Order
  • Show
  • Semigroup / Monoid

La gran ventaja de restringirnos a Applicative es que Validation es explícitamente para situaciones donde deseamos reportar todas las fallas, mientras que Disjunction se usa para detenernos en el primer fallo. Para lograr la acumulación de fallas, una forma popular de Validation es ValidationNel, teniendo un NonEmptyList[E] en la posición de fracaso.

Considere realizar validación de datos proporcionados por un usuario con ayuda de Disjunction and flatMap:

  scala> :paste
         final case class Credentials(user: Username, name: Fullname)
         final case class Username(value: String) extends AnyVal
         final case class Fullname(value: String) extends AnyVal

         def username(in: String): String \/ Username =
           if (in.isEmpty) "empty username".left
           else if (in.contains(" ")) "username contains spaces".left
           else Username(in).right

         def realname(in: String): String \/ Fullname =
           if (in.isEmpty) "empty real name".left
           else Fullname(in).right

  scala> for {
           u <- username("sam halliday")
           r <- realname("")
         } yield Credentials(u, r)
  res = -\/(username contains spaces)

Si usamos la sintáxis |@|

  scala> (username("sam halliday") |@| realname("")) (Credentials.apply)
  res = -\/(username contains spaces)

Todavía obtenemos el primer error. Esto es porque Disjuntion es una Monad, y sus métodos .applyX deben ser consistentes con .flatMap y no asumir que ninguna operación pueden realizarse fuera de orden. Compare con:

  scala> :paste
         def username(in: String): ValidationNel[String, Username] =
           if (in.isEmpty) "empty username".failureNel
           else if (in.contains(" ")) "username contains spaces".failureNel
           else Username(in).success

         def realname(in: String): ValidationNel[String, Fullname] =
           if (in.isEmpty) "empty real name".failureNel
           else Fullname(in).success

  scala> (username("sam halliday") |@| realname("")) (Credentials.apply)
  res = Failure(NonEmpty[username contains spaces,empty real name])

Esta vez, tenemos todas las fallas!

Validation tiene muchos de los métodos en Disjunction, tales como .fold, .swap y +++, además de algunas extra:

  sealed abstract class Validation[+E, +A] {
    def append[F >: E: Semigroup, B >: A: Semigroup](x: F \?/ B]): F \?/ B = ...

    def disjunction: (E \/ A) = ...
    ...
  }

.append (con el alias simbólico +|+) tiene la misma signatura de tipo que +++ pero da preferencia al caso de éxito

  • failure(v1) +|+ failure(v2) da failure(v1 |+| v2)
  • failure(v1) +|+ success(v2) da success(v2)
  • success(v1) +|+ failure(v2) da success(v1)
  • success(v1) +|+ success(v2) da success(v1 |+| v2)

.disjunction convierte un valor Validated[A, B] en un A \/ B. Disjunction tiene los métodos .validation y .validationNel para convertir en una Validation, permitiendo la fácil conversión entre acumulación sequencial y paralela de errores.

\/ y Validation son las versiones de PF con mayor rendimiento, equivalentes a una excepción de validación de entrada, evitando tanto un stacktrace y requiriendo que el que realiza la invocación lidie con las fallas resultando en sistemas más robustos.

6.7.4 These

Encontramos These, un codificación en forma de datos de un OR lógicamente inclusivo, cuando aprendimos sobre Align

  sealed abstract class \&/[+A, +B] { ... }
  object \&/ {
    type These[A, B] = A \&/ B

    final case class This[A](aa: A) extends (A \&/ Nothing)
    final case class That[B](bb: B) extends (Nothing \&/ B)
    final case class Both[A, B](aa: A, bb: B) extends (A \&/ B)

    def apply[A, B](a: A, b: B): These[A, B] = Both(a, b)
  }

y con una sintáxis para una construcción conveniente

  implicit class TheseOps[A](self: A) {
    final def wrapThis[B]: A \&/ B = \&/.This(self)
    final def wrapThat[B]: B \&/ A = \&/.That(self)
  }
  implicit class ThesePairOps[A, B](self: (A, B)) {
    final def both: A \&/ B = \&/.Both(self._1, self._2)
  }

These tiene instancias de una typeclass para

  • Monad
  • Bitraverse
  • Traverse
  • Cobind

y dependiendo del contenido

  • Semigroup / Monoid / Band
  • Equal / Order
  • Show

These (\&/) tiene muchos de los métodos que que esperamos de ``Disjunction (\/) y Validation (?/`)

  sealed abstract class \&/[+A, +B] {
    def fold[X](s: A => X, t: B => X, q: (A, B) => X): X = ...
    def swap: (B \&/ A) = ...

    def append[X >: A: Semigroup, Y >: B: Semigroup](o: =>(X \&/ Y)): X \&/ Y = ...

    def &&&[X >: A: Semigroup, C](t: X \&/ C): X \&/ (B, C) = ...
    ...
  }

.append tiene 9 posibles arreglos y los datos nunca se tiran porque los casos de This y That siempre pueden convertirse en Both.

.flatMap está sesgado hacia la derecha (Both y That), tomando un Semigroup del contenido de la izquierda (This) para combinar en lugar de fallar temprano. &&& es una manera conveniente de hacer un binding sobre dos valores de tipo These, creando una tupla a la derecha y perdiendo datos si no está presente en cada uno de estos (these).

Aunque es tentador usar \&/ en los tipos de retorno, el uso excesivo es un antipatrón. La razón principal para usar \&/ es para combinar o dividir streams de datos potencialmente infinitos en memoria finita. En el objeto compañero existen funciones convenientes para lidiar con EphemeralStream (con un alias para que quepan en una sola línea) o cualquier cosa con un MonadPlus.

  type EStream[A] = EphemeralStream[A]

  object \&/ {
    def concatThisStream[A, B](x: EStream[A \&/ B]): EStream[A] = ...
    def concatThis[F[_]: MonadPlus, A, B](x: F[A \&/ B]): F[A] = ...

    def concatThatStream[A, B](x: EStream[A \&/ B]): EStream[B] = ...
    def concatThat[F[_]: MonadPlus, A, B](x: F[A \&/ B]): F[B] = ...

    def unalignStream[A, B](x: EStream[A \&/ B]): (EStream[A], EStream[B]) = ...
    def unalign[F[_]: MonadPlus, A, B](x: F[A \&/ B]): (F[A], F[B]) = ...

    def merge[A: Semigroup](t: A \&/ A): A = ...
    ...
  }

6.7.5 Higher Kinded Either

El tipo de datos Coproduct (no confunda con el concepto más general de un coproducto en un ADT) envuelve una Disjunction para los constructores de tipo:

  final case class Coproduct[F[_], G[_], A](run: F[A] \/ G[A]) { ... }
  object Coproduct {
    def leftc[F[_], G[_], A](x: F[A]): Coproduct[F, G, A] = Coproduct(-\/(x))
    def rightc[F[_], G[_], A](x: G[A]): Coproduct[F, G, A] = Coproduct(\/-(x))
    ...
  }

Las instancias de un typeclass simplemente delegan a aquellos de F[_] y G[_].

El caso de uso más popular para un Coproduct es cuando deseamos crear un coproducto anónimo de múltiples ADTs.

6.7.6 No tan estricta

Las tuplas de Scala integradas, y los tipos de datos básicos como Maybe y la Disjunction son tipos de valores evaluados de manera estricta.

Por conveniencia, las alternativas por nombre para Name se proporcionan, teniendo las instancias de typeclass esperadas:

  sealed abstract class LazyTuple2[A, B] {
    def _1: A
    def _2: B
  }
  ...
  sealed abstract class LazyTuple4[A, B, C, D] {
    def _1: A
    def _2: B
    def _3: C
    def _4: D
  }

  sealed abstract class LazyOption[+A] { ... }
  private final case class LazySome[A](a: () => A) extends LazyOption[A]
  private case object LazyNone extends LazyOption[Nothing]

  sealed abstract class LazyEither[+A, +B] { ... }
  private case class LazyLeft[A, B](a: () => A) extends LazyEither[A, B]
  private case class LazyRight[A, B](b: () => B) extends LazyEither[A, B]

El lector astuto notará que Lazy no está bien nombrado, y estos tipos de datos quizá deberían ser: ByNameTupleX, ByNameOption y ByNameEither.

6.7.7 Const

Const, para constante, es un envoltorio para un valor de tipo A, junto con un parámetro de tipo B.

  final case class Const[A, B](getConst: A)

Const proporciona una instancia de Applicative[Const[A, ?]] si hay un Monoid[A] disponible:

  implicit def applicative[A: Monoid]: Applicative[Const[A, ?]] =
    new Applicative[Const[A, ?]] {
      def point[B](b: =>B): Const[A, B] =
        Const(Monoid[A].zero)
      def ap[B, C](fa: =>Const[A, B])(fbc: =>Const[A, B => C]): Const[A, C] =
        Const(fbc.getConst |+| fa.getConst)
    }

La cosa más importante sobre este Applicative es que ignora los parámetros B, continuando sin fallar y únicamente combinando los valores constantes que encuentra.

Volviendo atrás a nuestra aplicación de ejemplo drone-dynamic-agents, deberíamos refactorizar nuestro archivo logic.scala para usar Applicative en lugar de Monad. Escribimos logic.scala antes de que supieramos sobre Applicative y ahora lo sabemos mejor:

  final class DynAgentsModule[F[_]: Applicative](D: Drone[F], M: Machines[F])
    extends DynAgents[F] {
    ...
    def act(world: WorldView): F[WorldView] = world match {
      case NeedsAgent(node) =>
        M.start(node) >| world.copy(pending = Map(node -> world.time))

      case Stale(nodes) =>
        nodes.traverse { node =>
          M.stop(node) >| node
        }.map { stopped =>
          val updates = stopped.strengthR(world.time).toList.toMap
          world.copy(pending = world.pending ++ updates)
        }

      case _ => world.pure[F]
    }
    ...
  }

Dado que nuestra lógica de negocio únicamente requiere de un Applicative, podemos escribir implementaciones simuladas con F[a] como Const[String, a]. En tal caso, devolvemos los nombres de la función que se invoca:

  object ConstImpl {
    type F[a] = Const[String, a]

    private val D = new Drone[F] {
      def getBacklog: F[Int] = Const("backlog")
      def getAgents: F[Int]  = Const("agents")
    }

    private val M = new Machines[F] {
      def getAlive: F[Map[MachineNode, Epoch]]     = Const("alive")
      def getManaged: F[NonEmptyList[MachineNode]] = Const("managed")
      def getTime: F[Epoch]                        = Const("time")
      def start(node: MachineNode): F[Unit]        = Const("start")
      def stop(node: MachineNode): F[Unit]         = Const("stop")
    }

    val program = new DynAgentsModule[F](D, M)
  }

Con nuestra interpretación de nuestro programa, podemos realizar aserciones sobre los métodos que son invocados:

  it should "call the expected methods" in {
    import ConstImpl._

    val alive    = Map(node1 -> time1, node2 -> time1)
    val world    = WorldView(1, 1, managed, alive, Map.empty, time4)

    program.act(world).getConst shouldBe "stopstop"
  }

De manera alternativa, podríamos haber contado el total de métodos al usar Const[Int, ?] o en un IMap[String, Int].

Con esta prueba, hemos ido más allá de realizar pruebas con implementaciones simuladas con una prueba Const que hace aserciones sobre lo que se invoca sin tener que proporcionar implementaciones. Esto es útil si nuestra especificación demanda que hagamos ciertas llamadas para ciertas entradas, por ejemplo, para propósitos de contabilidad. Además, hemos conseguido esto con seguridad en tiempo de compilación.

Tomando esta línea de pensamiento un poco más allá, digamos que deseamos monitorear (en tiempo de producción) los nodos que estamos deteniendo en act. Podemos crear implementaciones de Drone y Machines con Const, invocándolos desde nuestra versión envuelta de act

  final class Monitored[U[_]: Functor](program: DynAgents[U]) {
    type F[a] = Const[Set[MachineNode], a]
    private val D = new Drone[F] {
      def getBacklog: F[Int] = Const(Set.empty)
      def getAgents: F[Int]  = Const(Set.empty)
    }
    private val M = new Machines[F] {
      def getAlive: F[Map[MachineNode, Epoch]]     = Const(Set.empty)
      def getManaged: F[NonEmptyList[MachineNode]] = Const(Set.empty)
      def getTime: F[Epoch]                        = Const(Set.empty)
      def start(node: MachineNode): F[Unit]        = Const(Set.empty)
      def stop(node: MachineNode): F[Unit]         = Const(Set(node))
    }
    val monitor = new DynAgentsModule[F](D, M)

    def act(world: WorldView): U[(WorldView, Set[MachineNode])] = {
      val stopped = monitor.act(world).getConst
      program.act(world).strengthR(stopped)
    }
  }

Podemos hacer esto porque monitor es puro y ejecutarlo no produce efectos laterales.

Esto ejecuta el programa con ConstImpl, extrayendo todas las llamadas a Machines.stop, entonces devolviéndolos junto con la WorldView. Podemos hacer pruebas unitarias así:

  it should "monitor stopped nodes" in {
    val underlying = new Mutable(needsAgents).program

    val alive = Map(node1 -> time1, node2 -> time1)
    val world = WorldView(1, 1, managed, alive, Map.empty, time4)
    val expected = world.copy(pending = Map(node1 -> time4, node2 -> time4))

    val monitored = new Monitored(underlying)
    monitored.act(world) shouldBe (expected -> Set(node1, node2))
  }

Hemos usado Const para hacer algo que es como la Programación Orientada a Aspectos (Aspect Oriented Programming), que alguna vez fue popular en Java. Construimos encima de nuestra lógica de negocios para soportar una preocupación de monitoreo, sin tener que complicar la lógica de negocios.

Se pone incluso mejor. Podemos ejecutar ConstImpl en producción para reunir lo que deseamos para detenernos (stop), y entonces proporcionar una implementación optimizada de act que puede usar llamadas por batches/lotes que puede ser de implementación específica.

El héroe siliencioso de esta historia es Applicative. Const nos deja ver lo que es posible. Si necesitamos cambiar nuestro programa para que requiera una Monad, no podemos seguir usando Const y es necesario escribir mocks completos para poder hacer aserciones sobre lo que se llama sobre ciertas entradas. La Regla del Poder Mínimo demanda que usemos Applicative en lugar de Monad siempre que podamos.

6.8 Colecciones

A diferencia de la API de colecciones de la librería estándar, Scalaz describe comportamientos en las colecciones en la jerarquía de typeclases, por ejemplo, Foldable, Traverse, Monoid. Lo que resta por estudiar son las implementaciones en términos de estructuras de datos, que tienen características de rendimiento distintas y métodos muy específicos.

Esta sección estudia detalles de implementación para cada tipo de datos. No es esencial recordar todo lo que se presenta aquí: la meta es ganar entendimiento a un nivel de abstracción alto de cómo funciona cada estructura de datos.

Debido a que los tipos de datos de todas las colecciones nos proporcionan más o menos la misma lista de instancias de typeclases, debemos evitar repetir la lista, que siempre es una variación de la lista:

  • Monoid
  • Traverse / Foldable
  • MonadPlus / IsEmpty
  • Cobind / Comonad
  • Zip / Unzip
  • Align
  • Equal / Order
  • Show

Las estructuras de datos que es posible probar que no son vacías pueden proporcionar:

  • Traverse1 / Foldable1

y proporcionar Semigroup en lugar de Monoid, Plus en lugar de IsEmpty.

6.8.1 Listas

Ya hemos usado IList[A] y NonEmptyList[A] tantas veces al momento que a estas alturas deberían ser familiares. Codifican una estructura de datos clásica, la lista ligada:

  sealed abstract class IList[A] {
    def ::(a: A): IList[A] = ...
    def :::(as: IList[A]): IList[A] = ...
    def toList: List[A] = ...
    def toNel: Option[NonEmptyList[A]] = ...
    ...
  }
  final case class INil[A]() extends IList[A]
  final case class ICons[A](head: A, tail: IList[A]) extends IList[A]

  final case class NonEmptyList[A](head: A, tail: IList[A]) {
    def <::(b: A): NonEmptyList[A] = nel(b, head :: tail)
    def <:::(bs: IList[A]): NonEmptyList[A] = ...
    ...
  }

La ventaja principal de IList sobre List de la librería estándar es que no hay métodos inseguros, como .head que lanza una excepción sobre listas vacías.

Adicionalmente, IList es mucho más simple, sin tener una jerarquía de clases y un tamaño del bytecode mucho más pequeño. Además, List de la librería estándar tiene una implementación terrible que usa var para parchar problemas de rendimiento en el diseño de las colecciones de la librería estándar:

  package scala.collection.immutable

  sealed abstract class List[+A]
    extends AbstractSeq[A]
    with LinearSeq[A]
    with GenericTraversableTemplate[A, List]
    with LinearSeqOptimized[A, List[A]] { ... }
  case object Nil extends List[Nothing] { ... }
  final case class ::[B](
    override val head: B,
    private[scala] var tl: List[B]
  ) extends List[B] { ... }

La creación de una instancia de List requiere de la creación cuidadosa, y lenta, de sincronización de Threads para asegurar una publicación segura. IList no requiere de tales hacks y por lo tanto puede superar a List.

6.8.2 EphemeralStream

Stream de la librería estándar es una versión perezosa de List, pero está plagada con fugas de memoria y métodos inseguros. EphemeralStream no mantiene referencias a valores calculados, ayudando a mitigar los problemas de retención de memoria, removiendo los métodos inseguros en el mismo espíritu que IList.

  sealed abstract class EphemeralStream[A] {
    def headOption: Option[A]
    def tailOption: Option[EphemeralStream[A]]
    ...
  }
  // private implementations
  object EphemeralStream extends EphemeralStreamInstances {
    type EStream[A] = EphemeralStream[A]

    def emptyEphemeralStream[A]: EStream[A] = ...
    def cons[A](a: =>A, as: =>EStream[A]): EStream[A] = ...
    def unfold[A, B](start: =>B)(f: B => Option[(A, B)]): EStream[A] = ...
    def iterate[A](start: A)(f: A => A): EStream[A] = ...

    implicit class ConsWrap[A](e: =>EStream[A]) {
      def ##::(h: A): EStream[A] = cons(h, e)
    }
    object ##:: {
      def unapply[A](xs: EStream[A]): Option[(A, EStream[A])] =
        if (xs.isEmpty) None
        else Some((xs.head(), xs.tail()))
    }
    ...
  }

.cons, .unfold e .iterate son mecanismos para la creación de streams, y la sintaxis conveniente ##:: pone un nuevo elemento en la cabeza de una referencia por nombre EStream. .unfold es para la creación de un stream finito (pero posiblemente infinito) al aplicar repetidamente una función f para obtener el siguiente valor y la entrada para la siguiente f. .iterate crea un stream infinito al aplicar repetidamente una función f en el elemento previo.

EStream puede aparecer en patrones de emparejamiento con el símbolo ##::, haciendo un match para la sintaxis para .cons.

Aunque EStream lidia con el problema de retención de valores de memoria, todavía es posible sufrir de fugas de memoria lentas si una referencia viva apunta a la cabeza de un stream infinito. Los problemas de esta naturaleza, así como la necesidad de realizar composición de streams con efectos, es la razón de que fs2 exista.

6.8.3 CorecursiveList

La correcursión es cuando empezamos de un estado base y producimos pasos subsecuentes de manera determinística, como el método EphemeralStream.unfold que acabamos de estudiar:

  def unfold[A, B](b: =>B)(f: B => Option[(A, B)]): EStream[A] = ...

Contraste con una recursion, que pone datos en un estado base y entonces termina.

Una CorecursiveList es una codificación de datos de un EphemeralStream.unfold, ofreciendo una alternativa a EStream que puede lograr un rendimiento mejor en algunas circunstancias:

  sealed abstract class CorecursiveList[A] {
    type S
    def init: S
    def step: S => Maybe[(S, A)]
  }

  object CorecursiveList {
    private final case class CorecursiveListImpl[S0, A](
      init: S0,
      step: S0 => Maybe[(S0, A)]
    ) extends CorecursiveList[A] { type S = S0 }

    def apply[S, A](init: S)(step: S => Maybe[(S, A)]): CorecursiveList[A] =
      CorecursiveListImpl(init, step)

    ...
  }

La correcursión es útil cuando estamos implementando Comonad.cojoin, como en nuestro ejemplo de Hood. CorecursiveList es una buena manera de codificar ecuaciones con recurrencia no lineales como las que se usan en el modelado de poblaciones de biología, sistemas de control, macro economía, y los modelos de inversión de bancos.

6.8.4 ImmutableArray

Un simple wrapper alrededor del Array mutable de la librería estándar, con especializaciones primitivas:

  sealed abstract class ImmutableArray[+A] {
    def ++[B >: A: ClassTag](o: ImmutableArray[B]): ImmutableArray[B]
    ...
  }
  object ImmutableArray {
    final class StringArray(s: String) extends ImmutableArray[Char] { ... }
    sealed class ImmutableArray1[+A](as: Array[A]) extends ImmutableArray[A] { ... }
    final class ofRef[A <: AnyRef](as: Array[A]) extends ImmutableArray1[A](as)
    ...
    final class ofLong(as: Array[Long]) extends ImmutableArray1[Long](as)

    def fromArray[A](x: Array[A]): ImmutableArray[A] = ...
    def fromString(str: String): ImmutableArray[Char] = ...
    ...
  }

Array no tiene rival en términos de rendimiento al hacer lectura y el tamaño del heap. Sin embargo, no se está comportiendo memoria estructuralmente cuando se crean nuevos arreglos, y por lo tanto los arreglos se usan típicamente cuando no se espera que los contenidos cambien, o como una manera segura de envolver de manera segura datos crudos de un sistema antiguo/legacy.

6.8.5 Dequeue

Un Dequeue (pronunciado como en “deck of cards”) es una lista ligada que permite que los elementos se coloquen o se devuelvan del frente (cons) o en la parte trasera (snoc) en tiempo constante. Remover un elemento de cualquiera de los extremos es una operación de tiempo constante, en promedio.

  sealed abstract class Dequeue[A] {
    def frontMaybe: Maybe[A]
    def backMaybe: Maybe[A]

    def ++(o: Dequeue[A]): Dequeue[A] = ...
    def +:(a: A): Dequeue[A] = cons(a)
    def :+(a: A): Dequeue[A] = snoc(a)
    def cons(a: A): Dequeue[A] = ...
    def snoc(a: A): Dequeue[A] = ...
    def uncons: Maybe[(A, Dequeue[A])] = ...
    def unsnoc: Maybe[(A, Dequeue[A])] = ...
    ...
  }
  private final case class SingletonDequeue[A](single: A) extends Dequeue[A] { ... }
  private final case class FullDequeue[A](
    front: NonEmptyList[A],
    fsize: Int,
    back: NonEmptyList[A],
    backSize: Int) extends Dequeue[A] { ... }
  private final case object EmptyDequeue extends Dequeue[Nothing] { ... }

  object Dequeue {
    def empty[A]: Dequeue[A] = EmptyDequeue()
    def apply[A](as: A*): Dequeue[A] = ...
    def fromFoldable[F[_]: Foldable, A](fa: F[A]): Dequeue[A] = ...
    ...
  }

La forma en la que funciona es que existen dos listas, una para los datos al frente y otra para los datos en la parte trasera. Considere una instancia para mantener los símbolos a0, a1, a2, a3, a4, a5, a6

  FullDequeue(
    NonEmptyList('a0, IList('a1, 'a2, 'a3)), 4,
    NonEmptyList('a6, IList('a5, 'a4)), 3)

que puede visualizarse como

Note que la lista que mantiene back está en orden inverso.

Leer el elemento final, snoc, es una simple lectura en back.head. Añadir un elemento al final del Dequeue significa añadir un nuevo elemento al frente de la lista back, y recrear el envoltorio FullDequeue (que incrementará el tamaño de backSize en uno). Casi toda la estructura original es compartida. Compare a agregar un nuevo elemento al final de una IList, que envolvería recrear la estructura completa.

El frontSize y el backSize se usan para rebalancear el front y el back de modo que casi siempre son del mismo tamaño. Rebalancear significa que algunas operaciones sean más lentas que otras (por ejemplo, cuando la estructura de datos debe ser reconstruida) pero debido a que únicamente ocurre ocasionalmente, podríamos tomar el promedio del costo y decir que es constante.

6.8.6 DList

Las listas ligadas tienen características de rendimiento muy pobres cuando se añaden grandes listas. Considere el trabajo que está envuelto en evaluar lo siguiente:

  ((as ::: bs) ::: (cs ::: ds)) ::: (es ::: (fs ::: gs))

Esto crea seis listas intermedias, recorriendo y reconstruyendo cada lista tres veces (con la excepción de gs que se comparte durante todas las etapas).

La DList (de lista por diferencias) es la solución más eficiente para este escenario. En lugar de realizar cálculos en cada etapa, es representada como la función IList[A] => IList[A]

  final case class DList[A](f: IList[A] => IList[A]) {
    def toIList: IList[A] = f(IList.empty)
    def ++(as: DList[A]): DList[A] = DList(xs => f(as.f(xs)))
    ...
  }
  object DList {
    def fromIList[A](as: IList[A]): DList[A] = DList(xs => as ::: xs)
  }

El cálculo equivalente es (los símbolos son creados a partir de DList.fromIList)

  (((a ++ b) ++ (c ++ d)) ++ (e ++ (f ++ g))).toIList

que reparte el trabajo en appends asociativos a la derecha (es decir, rápidos)

  (as ::: (bs ::: (cs ::: (ds ::: (es ::: (fs ::: gs))))))

utilizando el constructor rápido sobre IList.

Como siempre, no hay nada gratis. Existe un costo extra de asignación dinámica de memoria que puede reducir la velocidad del código que resulta naturalmente en appends asociativos a la derecha. El incremento de velocidad más grande ocurre cuando operaciones IList son asociativas hacia la izquierda, por ejemplo

  ((((((as ::: bs) ::: cs) ::: ds) ::: es) ::: fs) ::: gs)

Las listas de diferencia sufren de un marketing malo. Tal vez si su nombre fuera ListBuilderFactory estarían en la librería estándar.

6.8.7 ISet

Las estructuras de árboles son excelentes para almacenar datos ordenados, con cada nodo binario manteniendo elementos que son menores en una rama, y mayores en la otra. Sin embargo, implementaciones ingenuas de la estructura de datos árbol pueden desbalancearse dependiendo del orden de inserción. Es posible mantener un árbol perfectamente balanceado, pero es increíblemente ineficiente dado que cada inserción efectivamente reconstruye el árbol completo.

ISet es una implementación de un árbol con balanceo acotado, significando que está aproximadamente balanceado, usando el tamaño (size) de cada rama para balancear el nodo.

  sealed abstract class ISet[A] {
    val size: Int = this match {
      case Tip()        => 0
      case Bin(_, l, r) => 1 + l.size + r.size
    }
    ...
  }
  object ISet {
    private final case class Tip[A]() extends ISet[A]
    private final case class Bin[A](a: A, l: ISet[A], r: ISet[A]) extends ISet[A]

    def empty[A]: ISet[A] = Tip()
    def singleton[A](x: A): ISet[A] = Bin(x, Tip(), Tip())
    def fromFoldable[F[_]: Foldable, A: Order](xs: F[A]): ISet[A] =
      xs.foldLeft(empty[A])((a, b) => a insert b)
    ...
  }

ISet requiere que A tenga un Order. La instancia Order[A] debe permanecer igual entre llamadas o las invariantes internas serán inválidas, llevándonos a tener datos corrompidos: es decir, estamos asumiendo la coherencia de typeclases tales que Order[A] es única para cualquier A.

La ADT ISet desgraciadamente permite árboles inválidos. Nos esforzamos por escribir ADTs que describan completamente lo que es y no es válido usando restricciones de tipo, pero algunas veces tenemos situaciones donde únicamente es posible lograrlo con el toque inspirado de un inmortal. En lugar de esto, Tip / Bin son private, para evitar que los usuarios construyan, accidentalmente, árboles inválidos. .insert es la única manera de construir un ISet, y por lo tanto definir lo que es un árbol válido.

  sealed abstract class ISet[A] {
    ...
    def contains(x: A)(implicit o: Order[A]): Boolean = ...
    def union(other: ISet[A])(implicit o: Order[A]): ISet[A] = ...
    def delete(x: A)(implicit o: Order[A]): ISet[A] = ...

    def insert(x: A)(implicit o: Order[A]): ISet[A] = this match {
      case Tip() => ISet.singleton(x)
      case self @ Bin(y, l, r) => o.order(x, y) match {
        case LT => balanceL(y, l.insert(x), r)
        case GT => balanceR(y, l, r.insert(x))
        case EQ => self
      }
    }
    ...
  }

Los métodos internos .balanceL y .balanceR son espejos uno del otro, de modo que únicamente estudiamos .balanceL, que también se llama cuando el valor que estamos insertando es menor que el nodo actual. También se invoca por el método .delete.

  def balanceL[A](y: A, left: ISet[A], right: ISet[A]): ISet[A] = (left, right) match {
  ...

El balanceo requiere que clasifiquemos los escenarios que pueden ocurrir. Estudiaremos cada posible escenario, visualizando (y, left, right) al lado izquierdo de la página, con la estructura balanceada a la derecha, también conocido como el árbol rotado.

  • círculos llenos visualizan un Tip
  • tres columbas visualizan los campos left | value | right de Bin
  • los diamantes visualizan cualquier ISet

El primer escenario es el caso trivial, que es cuando, tanto el lado left y el right son Tip. De hecho, nunca encontraremos este escenario a partir de .insert, pero lo encontramos en .delete.

  case (Tip(), Tip()) => singleton(y)

El segundo caso es cuando left es un Bin que contiene únicamente a Tip, y no necesitamos balancear nada, simplemente creamos la conexión obvia:

  case (Bin(lx, Tip(), Tip()), Tip()) => Bin(y, left, Tip())

El tercer caso es cuando esto empieza a ponerse interesante: left es un Bin conteniendo únicamente un Bin a su right.

  case (Bin(lx, Tip(), Bin(lrx, _, _)), Tip()) =>
    Bin(lrx, singleton(lx), singleton(y))

Pero qué ocurrió a los dos diamantes que están debajo de lrx? No acabamos de perder información? No, no perdimos información, porque podemos razonar (basándonos en el balanceo del tamaño) que siempre son Tip! No hay regla en cuanto a cualquiera de los siguientes escenarios (o en .balanceR) que pueden producir un árbol donde los diamantes son Bin.

El cuarto caso es el opuesto del tercer caso.

  case (Bin(lx, ll, Tip()), Tip()) => Bin(lx, ll, singleton(y))

El quinto caso es cuando tenemos árboles completos en ambos lados del lado left y de todos modos debemos usar sus tamañaos relativos para decidir cómo rebalancear.

  case (Bin(lx, ll, lr), Tip()) if (2*ll.size > lr.size) =>
    Bin(lx, ll, Bin(y, lr, Tip()))
  case (Bin(lx, ll, Bin(lrx, lrl, lrr)), Tip()) =>
    Bin(lrx, Bin(lx, ll, lrl), Bin(y, lrr, Tip()))

Para la primera rama, 2*ll.size > lr.size

y para la segunda rama 2*ll.size <= lr.size

El sexto escenario introduce un árbol en lado (derecho) right. Cuando el lado left está vacío creamos la conexión obvia. Ese escenario nunca surge a partir de .insert porque el lado .left siempre está no vacío:

  case (Tip(), r) => Bin(y, Tip(), r)

El escenario final ocurre cuando tenemos árboles no vacíos en ambos lados. A menos que el lado left sea tres veces o más el tamaño del lado right, podemos hacer lo más sencillo y crear un nuevo Bin.

  case _ if l.size <= 3 * r.size => Bin(y, l, r)

Sin embargo, si el lado left debiera tener más de tres veces el tamaño del lado right, debemos balancear basándonos en los tamaños relativos de ll y lr, como en el escenario cinco.

  case (Bin(lx, ll, lr), r) if (2*ll.size > lr.size) =>
    Bin(lx, ll, Bin(y, lr, r))
  case (Bin(lx, ll, Bin(lrx, lrl, lrr)), r) =>
    Bin(lrx, Bin(lx, ll, lrl), Bin(y, lrr, r))

Esto concluye nuestro estudio del método .insert y cómo se construye un ISet. No debería ser sorpresivo que Foldable esté implementado en términos de una búsqueda en lo profundo, junto con left y right, como es apropiado. Métodos tales como .minimum y .maximum son óptimos porque la estructura de datos ya codifica el orden.

Es valioso hacer ntar quealgunos métodos de typeclass no pueden ser implementados de manera tan eficiente como quisieramos. Considere la signatura de Foldable.element

  @typeclass trait Foldable[F[_]] {
    ...
    def element[A: Equal](fa: F[A], a: A): Boolean
    ...
  }

Una implememntación obiva para .element es (de manera práctica) delegar a la búsqueda binaria ISet.contains. Sin embargo, no es posible debido a que .element proporciona Equal mientras que .contains requiere de Order.

ISet es incapaz de proporcionar un Functor por la misma razón. En la práctica esto es una restricción sensible: realizar un .map significaría reconstruir toda la estructura de datos completa. Tiene sentido convertir a un tipo de datos distinto, tales como IList, realizando el .map, y convirtiendo de vuelta. Una consecuencia es que ya no es posible tener un Traverse[ISet] o Applicative[Set].

6.8.8 IMap

  sealed abstract class ==>>[A, B] {
    val size: Int = this match {
      case Tip()           => 0
      case Bin(_, _, l, r) => 1 + l.size + r.size
    }
  }
  object ==>> {
    type IMap[A, B] = A ==>> B

    private final case class Tip[A, B]() extends (A ==>> B)
    private final case class Bin[A, B](
      key: A,
      value: B,
      left: A ==>> B,
      right: A ==>> B
    ) extends ==>>[A, B]

    def apply[A: Order, B](x: (A, B)*): A ==>> B = ...

    def empty[A, B]: A ==>> B = Tip[A, B]()
    def singleton[A, B](k: A, x: B): A ==>> B = Bin(k, x, Tip(), Tip())
    def fromFoldable[F[_]: Foldable, A: Order, B](fa: F[(A, B)]): A ==>> B = ...
    ...
  }

¡Esto es muy familiar! En realidad, IMap (un alias para el operador de la velocidad de la luz) es otro árbol balanceado en tamaño, pero con un campo extra value: B en cada rama del árbol binario, permitiendo almacenar pares de llave/valor. Únicamente el tipo clave A necesita un Order y una clase conveniente de métodos son proporcionados para permitir una actualización fácil de entradas.

  sealed abstract class ==>>[A, B] {
    ...
    def adjust(k: A, f: B => B)(implicit o: Order[A]): A ==>> B = ...
    def adjustWithKey(k: A, f: (A, B) => B)(implicit o: Order[A]): A ==>> B = ...
    ...
  }

Tree es una versión by need (por necesidad) de StrictTree con constructores convenientes

  class Tree[A](
    rootc: Need[A],
    forestc: Need[Stream[Tree[A]]]
  ) {
    def rootLabel = rootc.value
    def subForest = forestc.value
  }
  object Tree {
    object Node {
      def apply[A](root: =>A, forest: =>Stream[Tree[A]]): Tree[A] = ...
    }
    object Leaf {
      def apply[A](root: =>A): Tree[A] = ...
    }
  }

Se espera que el usuario de un Rose Tree lo balancee manualmente, lo que lo hace adecuado para casos donde es útil codificar conocimiento especializado de cierta jerarquía del dominio en la estructura de datos. Por ejemplo, en el campo de la inteligencia artificial, un árbol de Rose puede ser usado en algoritmos de agrupamiento para organizar datos en una jerarquía de cosas cada vez más similares. Es posible representar documentos XML con un árbol Rose.

Cuando trabajamos con datos jerárquicos, considere usar un árbol Rose en lugar de hacer una estructura de datos a la medida.

6.8.9 FingerTree

Los árboles de dedo son secuencias generalizadas con un costo de búsqueda amortizado y concatenación logarítmica. A es el tipo de datos, ignore V por ahora:

  sealed abstract class FingerTree[V, A] {
    def +:(a: A): FingerTree[V, A] = ...
    def :+(a: =>A): FingerTree[V, A] = ...
    def <++>(right: =>FingerTree[V, A]): FingerTree[V, A] = ...
    ...
  }
  object FingerTree {
    private class Empty[V, A]() extends FingerTree[V, A]
    private class Single[V, A](v: V, a: =>A) extends FingerTree[V, A]
    private class Deep[V, A](
      v: V,
      left: Finger[V, A],
      spine: =>FingerTree[V, Node[V, A]],
      right: Finger[V, A]
    ) extends FingerTree[V, A]

    sealed abstract class Finger[V, A]
    final case class One[V, A](v: V, a1: A) extends Finger[V, A]
    final case class Two[V, A](v: V, a1: A, a2: A) extends Finger[V, A]
    final case class Three[V, A](v: V, a1: A, a2: A, a3: A) extends Finger[V, A]
    final case class Four[V, A](v: V, a1: A, a2: A, a3: A, a4: A) extends Finger[V, A]

    sealed abstract class Node[V, A]
    private class Node2[V, A](v: V, a1: =>A, a2: =>A) extends Node[V, A]
    private class Node3[V, A](v: V, a1: =>A, a2: =>A, a3: =>A) extends Node[V, A]
    ...
  }

Visualizando FingerTree como puntos, Finger como cajas y Node como cajas dentro de cajas:

Agregar elementos al frente de un FingerTree con +: es eficiente porque Deep simplemente añade un nuevo elemento al dedo del lado left. Si el dedo es un Four, reconstruimos la espina (spine) para tomar 3 de los elementos como un Node3. Agregando al final, :+, es lo mismo pero en reversa.

Agregar cosas |+| (también <++>) es más eficiente que agregar un elemento a la vez debido a que el caso de dos árboles Deep pueden retener las ramas externas, reconstruyendo la espina basándose en las 16 posibles combinaciones de los dos valores Finger en la mitad.

En la discusión anterior nos saltamos V. En la descripción de la ADT no se muestra una implicit measurer: Reducer[A, V] en cada elemento de la ADT.

Reducer es una extensión de Monoid que permite que un solo elemento se agregue a una M.

  class Reducer[C, M: Monoid] {
    def unit(c: C): M

    def snoc(m: M, c: C): M = append(m, unit(c))
    def cons(c: C, m: M): M = append(unit(c), m)
  }

Por ejemplo, Reducer[A, IList[A]] puede proporcionar un .cons eficiente

  implicit def reducer[A]: Reducer[A, IList[A]] = new Reducer[A, IList[A]] {
    override def unit(a: A): IList[A] = IList.single(a)
    override def cons(a: A, as: IList[A]): IList[A] = a :: as
  }
6.8.9.1 IndSeq

Si usamos Int como V, podemos obtener una secuencia indexada, donde la medida es el tamaño, permitiéndonos realizar una búsqueda basada en el índice al comparar el índice deseado con el tamaño de cada rama en la estructura:

  final class IndSeq[A](val self: FingerTree[Int, A])
  object IndSeq {
    private implicit def sizer[A]: Reducer[A, Int] = _ => 1
    def apply[A](as: A*): IndSeq[A] = ...
  }

Otro uso de FingerTree es una secuencia ordenada, donde la medida almacena el valor más largo contenido en cada rama:

6.8.9.2 OrdSeq
  final class OrdSeq[A: Order](val self: FingerTree[LastOption[A], A]) {
    def partition(a: A): (OrdSeq[A], OrdSeq[A]) = ...
    def insert(a: A): OrdSeq[A] = ...
    def ++(xs: OrdSeq[A]): OrdSeq[A] = ...
  }
  object OrdSeq {
    private implicit def keyer[A]: Reducer[A, LastOption[A]] = a => Tag(Some(a))
    def apply[A: Order](as: A*): OrdSeq[A] = ...
  }

OrdSeq no tiene instancias de typeclases de modo que únicamente es útil para construir incrementalmente una secuencia ordenada, con duplicados. Podemos acceder al FingerTree subyacente cuando sea necesario.

6.8.9.3 Cord

El caso más común de FingerTree es un almacén intermedio para representaciones de String en Show. Construir una sola String puede ser miles de veces más rápido que la implementación default de case class de .toString anidadas, que construye una String para cada capa en la ADT.

  final case class Cord(self: FingerTree[Int, String]) {
    override def toString: String = {
      val sb = new java.lang.StringBuilder(self.measure)
      self.foreach(sb.append) // locally scoped side effect
      sb.toString
    }
    ...
  }

Por ejemplo, la instancia Cord[String] devuelve una Three con la cadena a la mitad y comillas en ambos lados

  implicit val show: Show[String] = s => Cord(FingerTree.Three("\"", s, "\""))

Por lo tanto una String se muestra tal y como se escribe en el código fuente

  scala> val s = "foo"
         s.toString
  res: String = foo

  scala> s.show
  res: Cord = "foo"

6.8.10 Cola de prioridad Heap

Una cola de prioridad es una estructura de datos que permite una rápida inserción de elementos ordenados, permitiendo duplicados, con un rápido acceso al valor mínimo (la prioridad más alta). La estructura no es necesaria para almacenar los elementos no mínimos en orden. Una implementación ingenua de una cola de prioridad podría ser

  final case class Vip[A] private (val peek: Maybe[A], xs: IList[A]) {
    def push(a: A)(implicit O: Order[A]): Vip[A] = peek match {
      case Maybe.Just(min) if a < min => Vip(a.just, min :: xs)
      case _                          => Vip(peek, a :: xs)
    }

    def pop(implicit O: Order[A]): Maybe[(A, Vip[A])] = peek strengthR reorder
    private def reorder(implicit O: Order[A]): Vip[A] = xs.sorted match {
      case INil()           => Vip(Maybe.empty, IList.empty)
      case ICons(min, rest) => Vip(min.just, rest)
    }
  }
  object Vip {
    def fromList[A: Order](xs: IList[A]): Vip[A] = Vip(Maybe.empty, xs).reorder
  }

Este push es muy rápido O(1), pero el reorder (y por lo tanto pop) depende de IList.sorted con un costo de O(n log n).

Scalaz codifica una cola de prioridad con una estructura de árbol donde cada nodo tiene un valor menor que sus hijos. Heap tiene un operaciones rápidas push (insert), union, size, pop (uncons) y peek (minimumO):

  sealed abstract class Heap[A] {
    def insert(a: A)(implicit O: Order[A]): Heap[A] = ...
    def +(a: A)(implicit O: Order[A]): Heap[A] = insert(a)

    def union(as: Heap[A])(implicit O: Order[A]): Heap[A] = ...

    def uncons(implicit O: Order[A]): Option[(A, Heap[A])] = minimumO strengthR deleteMin
    def minimumO: Option[A] = ...
    def deleteMin(implicit O: Order[A]): Heap[A] = ...

    ...
  }
  object Heap {
    def fromData[F[_]: Foldable, A: Order](as: F[A]): Heap[A] = ...

    private final case class Ranked[A](rank: Int, value: A)

    private final case class Empty[A]() extends Heap[A]
    private final case class NonEmpty[A](
      size: Int,
      tree: Tree[Ranked[A]]
    ) extends Heap[A]

    ...
  }

Heap está implementado con un Rose Tree de valores Ranked, donde rank es la profundidad del sub-árbol, permitiéndonos balancear la profundidad del árbol. Manualmente mantenemos el árbol de modo que el valor mínimo está en la raíz. Una ventaja de que se codifique el valor mínimo en la estructura de datos es que minimum0 (conocido también como peek) es una búsqueda inmediata y sin costo alguno:

  def minimumO: Option[A] = this match {
    case Empty()                        => None
    case NonEmpty(_, Tree.Node(min, _)) => Some(min.value)
  }

Cuando insertamos una nueva entrada, comparamos el valor mínimo actual y lo reemplazamos si la nueva entrada es más pequeña:

  def insert(a: A)(implicit O: Order[A]): Heap[A] = this match {
    case Empty() =>
      NonEmpty(1, Tree.Leaf(Ranked(0, a)))
    case NonEmpty(size, tree @ Tree.Node(min, _)) if a <= min.value =>
      NonEmpty(size + 1, Tree.Node(Ranked(0, a), Stream(tree)))
  ...

Las inserciones de valores que no son mínimos resulta en una estructura de datos desordenada en las ramas del nodo mínimo. Cuando encontramos dos o más sub-árboles de rango idéntico, colocamos el valor mínimo de manera optimista al frente:

  ...
    case NonEmpty(size, Tree.Node(min,
           (t1 @ Tree.Node(Ranked(r1, x1), xs1)) #::
           (t2 @ Tree.Node(Ranked(r2, x2), xs2)) #:: ts)) if r1 == r2 =>
      lazy val t0 = Tree.Leaf(Ranked(0, a))
      val sub =
        if (x1 <= a && x1 <= x2)
          Tree.Node(Ranked(r1 + 1, x1), t0 #:: t2 #:: xs1)
        else if (x2 <= a && x2 <= x1)
          Tree.Node(Ranked(r2 + 1, x2), t0 #:: t1 #:: xs2)
        else
          Tree.Node(Ranked(r1 + 1, a), t1 #:: t2 #:: Stream())

      NonEmpty(size + 1, Tree.Node(Ranked(0, min.value), sub #:: ts))

    case NonEmpty(size,  Tree.Node(min, rest)) =>
      val t0 = Tree.Leaf(Ranked(0, a))
      NonEmpty(size + 1, Tree.Node(Ranked(0, min.value), t0 #:: rest))
  }

Al evitar un ordenamiento completo del árbol hacemos que insert sea muy rápido, O(1), de modo que los productores que agregan elementos a la cola no son penalizados. Sin embargo, el consumidor paga el costo cuando invoca uncons, con deleteMin teniendo un costo O(log n) debido a que debe buscar el valor mínimo, y removerlo del árbol al reconstruirlo. Esto es rápido cuando se compara con una implementación ingenua.

La operación union también retrasa el ordenamiento permitiéndo que se realice en O(1).

Si Order[Foo] no captura correctamente la prioridad que deseamos para el Heap[Foo], podemos usar Tag y proporcionar un Order[Foo @@ Custom] ad-hoc para un Heap[Foo @@ Custom].

6.8.11 Diev (Discrete Intervals)

Podemos codificar eficientemente los valores enteros (desordenados) 6, 9, 2, 13, 8, 14, 10, 7, 5 como intervalos inclusivos [2, 2], [5, 10], [13, 14]. Diev tiene una codificación eficiente de intervalos para elementos A que tienen un Enum[A], haciéndose más eficientes a medida que el contenido se hace más denso.

  sealed abstract class Diev[A] {
    def +(interval: (A, A)): Diev[A]
    def +(value: A): Diev[A]
    def ++(other: Diev[A]): Diev[A]

    def -(interval: (A, A)): Diev[A]
    def -(value: A): Diev[A]
    def --(other: Diev[A]): Diev[A]

    def intervals: Vector[(A, A)]
    def contains(value: A): Boolean
    def contains(interval: (A, A)): Boolean
    ...
  }
  object Diev {
    private final case class DieVector[A: Enum](
      intervals: Vector[(A, A)]
    ) extends Diev[A]

    def empty[A: Enum]: Diev[A] = ...
    def fromValuesSeq[A: Enum](values: Seq[A]): Diev[A] = ...
    def fromIntervalsSeq[A: Enum](intervals: Seq[(A, A)]): Diev[A] = ...
  }

Cuando actualizamos el Diev, los intervalos adyacentes se fusionan (y entonces ordenados) tales que hay una representación única para un conjunto dado de valores.

  scala> Diev.fromValuesSeq(List(6, 9, 2, 13, 8, 14, 10, 7, 5))
  res: Diev[Int] = ((2,2)(5,10)(13,14))

  scala> Diev.fromValuesSeq(List(6, 9, 2, 13, 8, 14, 10, 7, 5).reverse)
  res: Diev[Int] = ((2,2)(5,10)(13,14))

Un gran caso de uso para Diev es para almacenar periodos de tiempo. Por ejemplo, en nuestro TradeTemplate del capítulo anterior

  final case class TradeTemplate(
    payments: List[java.time.LocalDate],
    ccy: Option[Currency],
    otc: Option[Boolean]
  )

si encontramos que los payments son muy densos, tal vez deseemos intercambiar a una representación Diev por razones de rendimiento, sin ningún cambio en nuestra lógica de negocios porque usamos un Monoid, no ningún método específico de List. Sin embargo, tendríamos que proporcionar un Enum[LocalDate], que de otra manera sería algo útil y bueno que tener.

6.8.12 OneAnd

Recuerde que Foldable es el equivalente de Scalaz de una API de colecciones y que Foldable1 es para colecciones no vacías. Hasta el momento hemos revisado únicamente NonEmptyList para proporcionar un Foldable1. La estructura de datos simple OneAnd envuelve cualquier otra colección y la convierte en un Foldable1.

  final case class OneAnd[F[_], A](head: A, tail: F[A])

NonEmptyList[A] podría ser un alias a OneAnd[IList, A]. De manera similar, podemos crear Stream no vacío, y estructuras DList y Tree. Sin embargo, podría terminar con las características de ordenamiento y unicidad de la estructura de datos subyacente: un OneAnd[ISet, A] no es un ISet no vacío, se trata de un ISet con la garantía de que se tiene un primer elemento que también puede estar en el ISet.

6.9 Sumario

En este capítulo hemos revisado rápidamente los tipos de datos que Scalaz tiene que ofrecer.

No es necesario recordad todo lo estudiado en este capítulo: piense que cada sección es la semilla o corazón de una idea.

El mundo de las estructuras de datos funcionales es una área activa de investigación. Publicaciones académicas aparecen regularmente con nuevos enfoques a viejos problemas. La implementación de una estructura de datos funcionales a partir de la literatura sería una buena contribución al ecosistema de Scalaz.

7. Mónadas avanzadas

Usted tiene que conocer cosas como las mónadas avanzadas para ser un programador funcional avanzado.

Sin embargo, somos desarrolladores buscando una vida simple, y nuestra idea de “avanzado” es modesta. Para ponernos en contexto: scala.concurrent.Future es más complicada y con más matices que cualquier Monad en este capítulo.

En este capítulo estudiaremos algunas de las implementaciones más importantes de Monad.

7.1 Future siempre está en movimiento

El problema más grande con Future es que programa trabajo rápidamente durante su construcción. Como descubrimos en la introducción, Future mezcla la definición del programa con su interpretación (es decir, con su ejecución).

Future también es malo desde una perspectiva de rendimiento: cada vez que .flatMap es llamado, una cerradura se manda al Ejecutor, resultando en una programación de hilos y en cambios de contexto innecesarios. No es inusual ver 50% del poder de nuestro CPU lidiando con planeación de hilos. Tanto es así que la paralelización de trabajo con Futuro puede volverlo más lento.

En combinación, la evaluación estricta y el despacho de ejecutores significa que es imposible de saber cuándo inició un trabajo, cuando terminó, o las sub-tareas que se despacharon para calcular el resultado final. No debería sorprendernos que las “soluciones” para el monitoreo de rendimiento para frameworks basados en Future sean buenas opciones para las ventas.

Además, Future.flatMap requiere de un ExecutionContext en el ámbito implícito: los usuarios están forzados a pensar sobre la lógica de negocios y la semántica de ejecución a la vez.

7.2 Efectos y efectos laterales

Si no podemos llamar métodos que realicen efectos laterales en nuestra lógica de negocios, o en Future (or Id, or Either, o Const, etc), entonces, ¿cuándo podemos escribirlos? La respuesta es: en una Monad que retrasa la ejecución hasta que es interpretada por el punto de entrada de la aplicación. En este punto, podemos referirnos a I/O y a la mutación como un efecto en el mundo, capturado por el sistema de tipos, en oposición a tener efectos laterales ocultos.

La implementación simple de tal Monad es IO, formalizando la versión que escribimos en la introducción:

  final class IO[A](val interpret: () => A)
  object IO {
    def apply[A](a: =>A): IO[A] = new IO(() => a)

    implicit val monad: Monad[IO] = new Monad[IO] {
      def point[A](a: =>A): IO[A] = IO(a)
      def bind[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = IO(f(fa.interpret()).interpret())
    }
  }

El método .interpret se llama únicamente una vez, en el punto de entrada de una aplicación.

  def main(args: Array[String]): Unit = program.interpret()

Sin embargo, hay dos grandes problemas con este simple IO:

  1. Puede ocasionar un sobreflujo de la pila
  2. No soporta cómputos paralelos

Ambos problemas serán abordados en este capítulo. Sin embargo, sin importar lo complicado de una implementación interna de una Monad, los principios aquí descritos seguirán siendo ciertos: estamos modularizando la definición de un programa y su ejecución, tales que podemos capturar los efectos en la signatura de los tipos, permitiéndonos razonar sobre ellos, y reusar más código.

7.3 Seguridad de la pila

En la JVM, toda invocación a un método, agrega una entrada a la pila de llamadas del hilo (Thread), como agregar al frente de una List. Cuando el método completa, el método en la cabeza (head) es descartado/eliminado. La longitud máxima de la pila de llamadas es determinada por la bandera -Xss cuando se llama a java. Los métodos que realizan una recursión de cola, son detectados por el compilador de Scala y no agregan una entrada. Si alcanzamos el límite, al invocar demasiados métodos encadenados, obtenemos una excepción de StackOverflowError.

Desgraciadamente, toda invocación anidada de .flatMap (sobre una instancia de IO) agrega otro método de invocación a la pila. La forma más sencilla de ver esto es repetir esta acción por siempre, y ver si logra sobrevivir más de unos cuántos segundos. Podemos usar .forever, que viene de Apply (un “padre” de Monad):

  scala> val hello = IO { println("hello") }
  scala> Apply[IO].forever(hello).interpret()

  hello
  hello
  hello
  ...
  hello
  java.lang.StackOverflowError
      at java.io.FileOutputStream.write(FileOutputStream.java:326)
      at ...
      at monadio.IO$$anon$1.$anonfun$bind$1(monadio.scala:18)
      at monadio.IO$$anon$1.$anonfun$bind$1(monadio.scala:18)
      at ...

Scalaz tiene una typeclass que las instancias de Monad pueden implementar si son seguras: BindRec requiere de un espacio de pila constante para bind recursivo:

  @typeclass trait BindRec[F[_]] extends Bind[F] {
    def tailrecM[A, B](f: A => F[A \/ B])(a: A): F[B]

    override def forever[A, B](fa: F[A]): F[B] = ...
  }

No necesitamos BindRec para todos los programas, pero es esencial para una implementación de propósito general de Monad.

La manera de conseguir seguridad en la pila es la conversión de invocaciones de métodos en referencias a una ADT, la mónada Free:

  sealed abstract class Free[S[_], A]
  object Free {
    private final case class Return[S[_], A](a: A)     extends Free[S, A]
    private final case class Suspend[S[_], A](a: S[A]) extends Free[S, A]
    private final case class Gosub[S[_], A0, B](
      a: Free[S, A0],
      f: A0 => Free[S, B]
    ) extends Free[S, B] { type A = A0 }
    ...
  }

La ADT Free es una es una representación del tipo de datos natural de la interfaz Monad:

  1. Return representa .point
  2. Gosub representa .bind / .flatMap

Cuando una ADT es un reflejo de los argumentos de las funciones relacionadas, recibe el nombre de una codificación Church.

Free recibe este nombre debido a que puede ser generada de manera gratuita para cualquier S[_]. Por ejemplo, podríamos hacer que S sea una de las álgebras Drone o Machines del Capítulo 3 y generar una representación de la estructura de datos de nuestro programa. Regresaremos más tarde a este punto para explicar por qué razón esto es de utilidad.

7.3.1 Trampoline

Free es más general de lo necesario. Haciendo que el álgebra S[_] sea () => ?, un cálculo diferido o un thunk, obtenemos Trampoline que puede implementar una Monad de manera que se conserva el uso seguro de la pila.

  object Free {
    type Trampoline[A] = Free[() => ?, A]
    implicit val trampoline: Monad[Trampoline] with BindRec[Trampoline] =
      new Monad[Trampoline] with BindRec[Trampoline] {
        def point[A](a: =>A): Trampoline[A] = Return(a)
        def bind[A, B](fa: Trampoline[A])(f: A => Trampoline[B]): Trampoline[B] =
          Gosub(fa, f)

        def tailrecM[A, B](f: A => Trampoline[A \/ B])(a: A): Trampoline[B] =
          bind(f(a)) {
            case -\/(a) => tailrecM(f)(a)
            case \/-(b) => point(b)
          }
      }
    ...
  }

La implementación de BindRec, .tailrecM, ejecuta .bind hasta que obtenemos una B. Aunque no se trata, técnicamente, de una implementación @tailrec, usa un espacio de pila constante debido a que cada llamada devuelve un objeto en el heap, con recursión postergada.

Se proporcionan funciones convenientes para la creación estricta de un Trampoline (.done) o por nombre (.delay). También podemos crear un Trampoline a partir de un Trampoline por nombre (.suspend):

  object Trampoline {
    def done[A](a: A): Trampoline[A]                  = Return(a)
    def delay[A](a: =>A): Trampoline[A]               = suspend(done(a))
    def suspend[A](a: =>Trampoline[A]): Trampoline[A] = unit >> a

    private val unit: Trampoline[Unit] = Suspend(() => done(()))
  }

Cuando vemos un Trampoline[A] en el código, siempre es posible sustituirlo mentalmente con una A, debido a que únicamente está añadiendo seguridad al uso de la pila a un cómputo puro. Obtenemos la A al interpretar Free, provisto por .run.

7.3.2 Ejemplo: DList con seguridad en el manejo de la pila

En el capítulo anterior hemos descrito el tipo de datos DList como

  final case class DList[A](f: IList[A] => IList[A]) {
    def toIList: IList[A] = f(IList.empty)
    def ++(as: DList[A]): DList[A] = DList(xs => f(as.f(xs)))
    ...
  }

Sin embargo, la implementación actual se ve más parecida a:

  final case class DList[A](f: IList[A] => Trampoline[IList[A]]) {
    def toIList: IList[A] = f(IList.empty).run
    def ++(as: =>DList[A]): DList[A] = DList(xs => suspend(as.f(xs) >>= f))
    ...
  }

En lugar de aplicar llamadas anidadas a f, usamos un Trampoline suspendido. Interpretamos el trampolín con .run únicamente cuando es necesario, por ejemplo, en toIList. Los cambios son mínimo, pero ahora tenemos una DList con uso seguro de la pila que puede reordenar la concatenación de un número largo de listas sin ocasionar un sobreflujo de la pila.

7.3.3 IO con uso seguro de la pila

De manera similar, nuestra IO puede hacerse segura (respecto al uso de la pila), gracias a Trampoline:

  final class IO[A](val tramp: Trampoline[A]) {
    def unsafePerformIO(): A = tramp.run
  }
  object IO {
    def apply[A](a: =>A): IO[A] = new IO(Trampoline.delay(a))

    implicit val Monad: Monad[IO] with BindRec[IO] =
      new Monad[IO] with BindRec[IO] {
        def point[A](a: =>A): IO[A] = IO(a)
        def bind[A, B](fa: IO[A])(f: A => IO[B]): IO[B] =
          new IO(fa.tramp >>= (a => f(a).tramp))
        def tailrecM[A, B](f: A => IO[A \/ B])(a: A): IO[B] = ...
      }
  }

El intérprete, .unsafePerformIO(), ahora tiene un nombre intencionalmente terrorífico para desalentar su uso, con la excepción del punto de entrada de una aplicación.

Esta vez, no obtendremos un error por sobreflujo de la pila:

  scala> val hello = IO { println("hello") }
  scala> Apply[IO].forever(hello).unsafePerformIO()

  hello
  hello
  hello
  ...
  hello

El uso de un Trampoline típicamente introduce una regresión en el desempeño comparado a la implementación normal de referencia. Es Free en el sentido de que se genera de manera automática, no en el sentido literal.

7.4 Librería de transformadores de mónadas

Las transformadores de mónadas son estructuras de datos que envuelven un valor subyacente y proporcionan un efecto monádico.

Por ejemplo, en el capítulo 2 usamos OptionT para que pudiéramos usar F[Option[A]] en una comprehensión for como si se tratase de F[A]. Esto le dio a nuestro programa el efecto de un valor opcional. De manera alternativa, podemos conseguir el efcto de opcionalidad si tenemos una MonadPlus.

A este subconjunto de tipos de datos y extensiones a Monad con frecuencia se le conoce como una Librería de Transformadores de Mónadas (MTL, por sus siglas en inglés), como se resume enseguida. En esta sección, explicaremos cada uno de los transformadores, por qué razón son útiles, y cómo funcionan.

Efecto Equivalencia Transformador Typeclass
opcionalidad F[Maybe[A]] MaybeT MonadPlus
errores F[E \/ A] EitherT MonadError
un valor en tiempo de ejecución A => F[B] ReaderT MonadReader
journal / multi tarea F[(W, A)] WriterT MonadTell
estado en cambio S => F[(S, A)] StateT MonadState
mantén la calma y continúa F[E \&/ A] TheseT  
control de flujo (A => F[B]) => F[B] ContT  

7.4.1 MonadTrans

Cada transformador tiene la forma general T[F[_], A], proporcionando al menos una instancia de Monad y Hoist (y por lo tanto de MonadTrans):

  @typeclass trait MonadTrans[T[_[_], _]] {
    def liftM[F[_]: Monad, A](a: F[A]): T[F, A]
  }

  @typeclass trait Hoist[F[_[_], _]] extends MonadTrans[F] {
    def hoist[M[_]: Monad, N[_]](f: M ~> N): F[M, ?] ~> F[N, ?]
  }

.liftM nos permite crear un transformador de mónadas si tenemos un F[A]. Por ejemplo, podemos crear un OptionT[IO, String] al invocar ` .liftM[OptionT] en una IO[String]`.

.hoist es la misma idea, pero para transformaciones naturales.

Generalmente, hay tres maneras de crear un transformador de mónadas:

  • A partir de la estructura equivalente, usando el constructor del transformador
  • A partir de un único valor A, usando .pure usando la sintaxis de Monad
  • A partir de F[A], usando .liftM usando la sintaxis de MonadTrans

Debido a la forma en la que funciona la inferencia de tipos en Scala, esto con frecuencia significa que un parámetro de tipo complejo debe escribirse de manera explícita. Como una forma de lidiar con el problema, los transformadores proporcionan constructores convenientes en su objeto compañero que los hacen más fáciles de usar.

7.4.2 MaybeT

OptionT, MaybeT y LazyOption tienen implementaciones similares, proporcionando opcionalidad a través de Option, Maybe y LazyOption, respectivamente. Nos enfocaremos en MaybeT para evitar la repetición.

  final case class MaybeT[F[_], A](run: F[Maybe[A]])
  object MaybeT {
    def just[F[_]: Applicative, A](v: =>A): MaybeT[F, A] =
      MaybeT(Maybe.just(v).pure[F])
    def empty[F[_]: Applicative, A]: MaybeT[F, A] =
      MaybeT(Maybe.empty.pure[F])
    ...
  }

proporcionando una MonadPlus

  implicit def monad[F[_]: Monad] = new MonadPlus[MaybeT[F, ?]] {
    def point[A](a: =>A): MaybeT[F, A] = MaybeT.just(a)
    def bind[A, B](fa: MaybeT[F, A])(f: A => MaybeT[F, B]): MaybeT[F, B] =
      MaybeT(fa.run >>= (_.cata(f(_).run, Maybe.empty.pure[F])))

    def empty[A]: MaybeT[F, A] = MaybeT.empty
    def plus[A](a: MaybeT[F, A], b: =>MaybeT[F, A]): MaybeT[F, A] = a orElse b
  }

Esta mónada se ve un poco complicada, pero simplemente está delegando todo a la Monad[F] y entonces envolviendo todo dentro de un MaybeT. Simplemente es código para cumplir con el trabajo.

Con esta mónada podemos escribir lógica que maneja la opcionalidad en el contexto de F[_], más bien que estar lidiando con Option o Maybe.

Por ejemplo, digamos que estamos interactuando con un sitio social para contar el número de estrellas que tiene el usuario, y empezamos con una cadena (String) que podría o no corresponder al usuario. Tenemos esta álgebra:

  trait Twitter[F[_]] {
    def getUser(name: String): F[Maybe[User]]
    def getStars(user: User): F[Int]
  }
  def T[F[_]](implicit t: Twitter[F]): Twitter[F] = t

Necesitamos invocar getUser seguido de getStars. Si usamos Monad como nuestro contexto, nuestra función es difícil porque tendremos que lidiar con el caso Empty:

  def stars[F[_]: Monad: Twitter](name: String): F[Maybe[Int]] = for {
    maybeUser  <- T.getUser(name)
    maybeStars <- maybeUser.traverse(T.getStars)
  } yield maybeStars

Sin embargo, si tenemos una MonadPlus como nuestro contexto, podemos poner Maybe dentro de F[_] usando .orEmpty, y olvidarnos del asunto:

  def stars[F[_]: MonadPlus: Twitter](name: String): F[Int] = for {
    user  <- T.getUser(name) >>= (_.orEmpty[F])
    stars <- T.getStars(user)
  } yield stars

Sin embargo, agregar un requerimiento de MonadPlus puede ocasionar problemas más adelante en el proceso, si el contexto no tiene una. La solución es, o cambiar el contexto del programa a MaybeT[F, ?] (elevando el contexto de Monad[F] a una MonadPlus), o para usar explícitamente MaybeT en el tipo de retorno, a costa de un poco de código adicional:

  def stars[F[_]: Monad: Twitter](name: String): MaybeT[F, Int] = for {
    user  <- MaybeT(T.getUser(name))
    stars <- T.getStars(user).liftM[MaybeT]
  } yield stars

La decisión de requerir una Monad más poderosa vs devolver un transformador es algo que cada equipo puede decidir por sí mismo basándose en los intérpretes que planee usar en sus programas.

7.4.3 EitherT

Un valor opcional es el caso especial de un valor que puede ser un error, pero no sabemos nada sobre el error. EitherT (y la variante perezosa LazyEitherT) nos permite usar cualquier tipo que deseemos como el valor del error, porporcionando información contextual sobre lo que pasó mal.

EitherT es un envoltorio sobre F[A \/ B]

  final case class EitherT[F[_], A, B](run: F[A \/ B])
  object EitherT {
    def either[F[_]: Applicative, A, B](d: A \/ B): EitherT[F, A, B] = ...
    def leftT[F[_]: Functor, A, B](fa: F[A]): EitherT[F, A, B] = ...
    def rightT[F[_]: Functor, A, B](fb: F[B]): EitherT[F, A, B] = ...
    def pureLeft[F[_]: Applicative, A, B](a: A): EitherT[F, A, B] = ...
    def pure[F[_]: Applicative, A, B](b: B): EitherT[F, A, B] = ...
    ...
  }

Monad es una MonadError

  @typeclass trait MonadError[F[_], E] extends Monad[F] {
    def raiseError[A](e: E): F[A]
    def handleError[A](fa: F[A])(f: E => F[A]): F[A]
  }

.raiseError y .handleError son descriptivos en sí mismos: el equivalente de lanzar (throw) y atrapar (catch) una excepción, respectivamente.

MonadError tiene sintaxis adicional para lidiar con problemas comunes:

  implicit final class MonadErrorOps[F[_], E, A](self: F[A])(implicit val F: MonadError[F, E])\
 {
    def attempt: F[E \/ A] = ...
    def recover(f: E => A): F[A] = ...
    def emap[B](f: A => E \/ B): F[B] = ...
  }

.attempt trae los errores dentro del valor, lo cual es útil para exponer los errores en los subsistemas como valores de primera clase.

.recover es para convertir un error en un valor en todos los casos, en oposición a .handleError que toma una F[A] y por lo tanto permite una recuperación parcial.

.emap, es para aplicar transformaciones que pueden fallar.

El MonadError para EitherT es:

  implicit def monad[F[_]: Monad, E] = new MonadError[EitherT[F, E, ?], E] {
    def monad[F[_]: Monad, E] = new MonadError[EitherT[F, E, ?], E] {
    def bind[A, B](fa: EitherT[F, E, A])
                  (f: A => EitherT[F, E, B]): EitherT[F, E, B] =
      EitherT(fa.run >>= (_.fold(_.left[B].pure[F], b => f(b).run)))
    def point[A](a: =>A): EitherT[F, E, A] = EitherT.pure(a)

    def raiseError[A](e: E): EitherT[F, E, A] = EitherT.pureLeft(e)
    def handleError[A](fa: EitherT[F, E, A])
                      (f: E => EitherT[F, E, A]): EitherT[F, E, A] =
      EitherT(fa.run >>= {
        case -\/(e) => f(e).run
        case right => right.pure[F]
      })
  }

No debería sorprender que podamos reescribir el ejemplo con MonadPlus con MonadError, insertando mensajes informativos de error:

  def stars[F[_]: Twitter](name: String)
                          (implicit F: MonadError[F, String]): F[Int] = for {
    user  <- T.getUser(name) >>= (_.orError(s"user '$name' not found")(F))
    stars <- T.getStars(user)
  } yield stars

donde .orError es un método conveniente sobre Maybe

  sealed abstract class Maybe[A] {
    ...
    def orError[F[_], E](e: E)(implicit F: MonadError[F, E]): F[A] =
      cata(F.point(_), F.raiseError(e))
  }

La versión que usa EitherT directamente es:

  def stars[F[_]: Monad: Twitter](name: String): EitherT[F, String, Int] = for {
    user <- EitherT(T.getUser(name).map(_ \/> s"user '$name' not found"))
    stars <- EitherT.rightT(T.getStars(user))
  } yield stars

La instancia más simple de MonadError es la de \/, perfecta para probar lógica de negocios que es requerida por una MonadError. Por ejemplo,

  final class MockTwitter extends Twitter[String \/ ?] {
    def getUser(name: String): String \/ Maybe[User] =
      if (name.contains(" ")) Maybe.empty.right
      else if (name === "wobble") "connection error".left
      else User(name).just.right

    def getStars(user: User): String \/ Int =
      if (user.name.startsWith("w")) 10.right
      else "stars have been replaced by hearts".left
  }

Nuestras pruebas unitarias para .stars pueden cubrir estos casos:

  scala> stars("wibble")
  \/-(10)

  scala> stars("wobble")
  -\/(connection error)

  scala> stars("i'm a fish")
  -\/(user 'i'm a fish' not found)

  scala> stars("fommil")
  -\/(stars have been replaced by hearts)

Así como hemos visto varias veces, podemos enfocarnos en probar la lógica de negocios sin distracciones.

Finalmente, si devolvemos nuestra álgebra JsonClient del capítulo 4.3

  trait JsonClient[F[_]] {
    def get[A: JsDecoder](
      uri: String Refined Url,
      headers: IList[(String, String)]
    ): F[A]
    ...
  }

recuerde que únicamente hemos codificado el camino feliz en la API. Si nuestro intérprete para esta álgebra únicamente funciona para una F que tiene una MonadError, podemos definir los tipos de errores como una preocupación tangencial. En verdad, podemos tener dos capas de errores si definimos el intérprete para una EitherT[IO, JsonClient.Error, ?]

  object JsonClient {
    sealed abstract class Error
    final case class ServerError(status: Int)       extends Error
    final case class DecodingError(message: String) extends Error
  }

que cubre los problemas de I/O (red), problemas de estado del servidor, y asuntos con el modelado de los payloads JSON de nuestro servidor.

7.4.3.1 Escogiendo un tipo de errores

La comunidad no se ha decidido sobre la mejor estrategia para el tipo de errores E en MonadError.

Una escuela de pensamiento dice que deberíamos escoger algo general, como una cadena (String). La otra escuela de pensamiento dice que una aplicación debería tener una ADT de errores, permitiendo que errores distintos sean reportados o manejados de manera diferente. Una “pandilla” de gente prefiere usar Throwable para tener máxima compatibilidad con la JVM.

Hay dos problemas con una ADT de errores a nivel de la aplicación:

  • Es muy torpe crear un nuevo error. Una archivo se vuelve un repositorio monolítico de errores, agregando las ADTs de subsistemas individuales.
  • Sin importar qué tan granular sea el reporte de errores, la solución es con frecuencia la misma, realice un log de los datos e intente de nuevo, o ríndase. No necesitamos una ADT para esto.

Una ADT de errores es útil si cada valor permite la ejecución de una estrategia distinta de recuperación.

El compromiso entre una ADT de errores y una cadena String está en un formato intermedio. JSON es una buena elección si puede entenderse por la mayoría de los frameworks de loggeo y monitoreo.

Un problema con la ausencia de stacktraces es que puede ser complicado ubicar cúal porción de código fue la causa del error. Con sourcecode de Li Haoyi, podemos incluir información contextual como metadatos sobre nuestros errores:

  final case class Meta(fqn: String, file: String, line: Int)
  object Meta {
    implicit def gen(implicit fqn: sourcecode.FullName,
                              file: sourcecode.File,
                              line: sourcecode.Line): Meta =
      new Meta(fqn.value, file.value, line.value)
  }

  final case class Err(msg: String)(implicit val meta: Meta)

Aunque Err es referencialmente transparente, la construcción implícita de Meta no parece ser referencialmente transparente desde una lectura natural: dos invocaciones a Meta.gen (que se invoca implícitamente cuando se crea un Err) producirán diferentes valores porque la ubicación en el código fuente impacta el valor retornado:

  scala> println(Err("hello world").meta)
  Meta(com.acme,<console>,10)

  scala> println(Err("hello world").meta)
  Meta(com.acme,<console>,11)

Para entender esto, tenemos que apreciar que los métodos sourcecode.* son macros que están generando código fuente para nosotros. Si tuvieramos que escribir el código arriba de manera explícita sería claro lo que está sucediendo:

  scala> println(Err("hello world")(Meta("com.acme", "<console>", 10)).meta)
  Meta(com.acme,<console>,10)

  scala> println(Err("hello world")(Meta("com.acme", "<console>", 11)).meta)
  Meta(com.acme,<console>,11)

Sí, hemos usado macros, pero también pudimos escribir Meta manualmente y hubiera sido necesario hacerla obsoleta antes que nuestra documentación.

7.4.4 ReaderT

La mónada reader envuelve A => F[B] permitiendo que un programa F[B] dependa del valor de tiempo de ejecución A. Para aquellos que etán familiarizados con la inyección de dependencias, la mónada reader es el equivalente funcional de la inyección @Inject de Spring o de Guice, sin el uso de XML o reflexión.

ReaderT es simplemente un alias de otro tipo de datos que con frecuencia es más general, y que recibe su nombre en honor al matemático Heinrich Kleisli.

  type ReaderT[F[_], A, B] = Kleisli[F, A, B]

  final case class Kleisli[F[_], A, B](run: A => F[B]) {
    def dimap[C, D](f: C => A, g: B => D)(implicit F: Functor[F]): Kleisli[F, C, D] =
      Kleisli(c => run(f(c)).map(g))

    def >=>[C](k: Kleisli[F, B, C])(implicit F: Bind[F]): Kleisli[F, A, C] = ...
    def >==>[C](k: B => F[C])(implicit F: Bind[F]): Kleisli[F, A, C] = this >=> Kleisli(k)
    ...
  }
  object Kleisli {
    implicit def kleisliFn[F[_], A, B](k: Kleisli[F, A, B]): A => F[B] = k.run
    ...
  }

Una conversión implícita en el objeto companion nos permite usar Kleisli en lugar de una función, de modo que puede proporcionarse como el parámetro de .bind, o >>=.

El uso más común para ReaderT es proporcionar información del contexto a un programa. En drone-dynamic-agents necesitamos acceso al token Oauth 2.0 del usuario para ser capaz de contactar a Google. El proceder obvio es cargar RefreshTokens del disco al momento de arranque, y hacer que cada método tome un parámetro RefreshToken. De hecho, se trata de un requerimiento tan común que Martin Odersky propuso las funciones implícitas.

Una mejor solución para nuestro programa es tener un álgebra de configuración que proporcione la configuración cuando sea necesario, es decir

  trait ConfigReader[F[_]] {
    def token: F[RefreshToken]
  }

Hemos reinventado MonadReader, la typeclass que está asociada a ReaderT, donde .ask es la misma que nuestra .token y S es RefreshToken:

  @typeclass trait MonadReader[F[_], S] extends Monad[F] {
    def ask: F[S]

    def local[A](f: S => S)(fa: F[A]): F[A]
  }

con la implementación

  implicit def monad[F[_]: Monad, R] = new MonadReader[Kleisli[F, R, ?], R] {
    def point[A](a: =>A): Kleisli[F, R, A] = Kleisli(_ => F.point(a))
    def bind[A, B](fa: Kleisli[F, R, A])(f: A => Kleisli[F, R, B]) =
      Kleisli(a => Monad[F].bind(fa.run(a))(f))

    def ask: Kleisli[F, R, R] = Kleisli(_.pure[F])
    def local[A](f: R => R)(fa: Kleisli[F, R, A]): Kleisli[F, R, A] =
      Kleisli(f andThen fa.run)
  }

Una ley de MonadReader es que S no puede cambiar entre invocaciones, es decir ask >> ask === ask. Para nuestro caso de uso, esto significa que la configuración se lee una única vez. Si decidimos después que deseamos recargar nuestra configuración cada vez que sea necesario, por ejemplo, al permitir el cambio de token sin reiniciar la aplicación, podemos reintroducir ConfigReader que no tiene tal ley.

En nuestra implementación OAuth 2.0 podemos primero mover la evidencia de Monad a los métodos:

  def bearer(refresh: RefreshToken)(implicit F: Monad[F]): F[BearerToken] =
    for { ...

y entonces refactorizar el parámetro refresh para que sea parte de Monad

  def bearer(implicit F: MonadReader[F, RefreshToken]): F[BearerToken] =
    for {
      refresh <- F.ask

Cualquier parámetro puede moverse dentro de MonadReader. Esto es del mayor valor posible para los usuarios que simplemente desean pasar esta información. Con ReaderT, podemos reservar bloques de parámetros implícitos, para un uso exclusivo de typeclases, reduciendo la carga mental que viene con el uso de Scala.

El otro método en MonadReader es .local

  def local[A](f: S => S)(fa: F[A]): F[A]

Podemos cambiar S y ejecutar un programa fa cdentro del contexto local, devolviendo la S original. Un caso de uso para .local es la generación de un stacktrace que tenga sentido para nuestro dominio, ¡proporcionándonos un logging anidado! A partir de lo que aprendimos en nuestra estructura de datos Meta de la sección anterior, definimos una función para realizar un chequeo:

  def traced[A](fa: F[A])(implicit F: MonadReader[F, IList[Meta]]): F[A] =
    F.local(Meta.gen :: _)(fa)

y la podemos usar para envolver funciones que operan en este contexto

  def foo: F[Foo] = traced(getBar) >>= barToFoo

automáticamente pasando cualquier información que no se esté rastreando de manera explícita. Un plugin para el compilador o una macro podría realizar lo opuesto, haciendo que todo se realice por default.

Si accedemos a .ask podemos ver el rastro completo de cómo se realizaron las llamadas, sin la distracci[on de los detalles en la implementación del bytecode. ¡Un stacktrace referencialmente transparente!

Un programador a la defensiva podría truncar la IList[Meta] a cierta longitud para evitar el equivalente de un sobreflujo de pila. En realidad, una estructura de datos más apropiada es Dequeue.

.local puede también usarse para registrar la información contextual que es directamente relavante a la tarea que se está realizando, como el número de espacios que debemos indentar una línea cuando se está imprimiendo un archivo con formato legible por humanos, haciendo que esta indentación aumente en dos espacios cuando introducimos una estructura anidada.

Finalmente, si no podemos pedir una MonadReader porque nuestra aplicación no proporciona una, siempre podemos devolver un ReaderT.

  def bearer(implicit F: Monad[F]): ReaderT[F, RefreshToken, BearerToken] =
    ReaderT( token => for {
    ...

Si alguien que recibe un ReaderT, y tienen el parámetro token a la mano, entonces pueden invocar access.run(token) y tener de vuelta un F[BearerToken].

Dado que no tenemos muchos callers, deberíamos simplemente revertir a un parámetro de función regular. MonadReader es de mayor utilidad cuando:

  1. Deseamos refactorizar el código más tarde para recargar la configuración
  2. El valor no es necesario por usuarios (llamadas) intermedias
  3. o, cuando deseamos restringir el ámbito/alcance para que sea local

Dotty puede quedarse con sus funciones implícitas… nosotros ya tenemos ReaderT y MonadReader.

7.4.5 WriterT

Lo opuesto a la lectura es la escritura. El transformador de mónadas WriterT es usado típicamente para escribir a un journal.

  final case class WriterT[F[_], W, A](run: F[(W, A)])
  object WriterT {
    def put[F[_]: Functor, W, A](value: F[A])(w: W): WriterT[F, W, A] = ...
    def putWith[F[_]: Functor, W, A](value: F[A])(w: A => W): WriterT[F, W, A] = ...
    ...
  }

El tipo envuelto es F[(W, A)] y el journal se acumula en W.

¡No hay únicamente una mónada asociada, sino dos! MonadTell y MonadListen

  @typeclass trait MonadTell[F[_], W] extends Monad[F] {
    def writer[A](w: W, v: A): F[A]
    def tell(w: W): F[Unit] = ...

    def :++>[A](fa: F[A])(w: =>W): F[A] = ...
    def :++>>[A](fa: F[A])(f: A => W): F[A] = ...
  }

  @typeclass trait MonadListen[F[_], W] extends MonadTell[F, W] {
    def listen[A](fa: F[A]): F[(A, W)]

    def written[A](fa: F[A]): F[W] = ...
  }

MonadTell es para escribir al journal y MonadListen es para recuperarlo. La implementación de WriterT es

  implicit def monad[F[_]: Monad, W: Monoid] = new MonadListen[WriterT[F, W, ?], W] {
    def point[A](a: =>A) = WriterT((Monoid[W].zero, a).point)
    def bind[A, B](fa: WriterT[F, W, A])(f: A => WriterT[F, W, B]) = WriterT(
      fa.run >>= { case (wa, a) => f(a).run.map { case (wb, b) => (wa |+| wb, b) } })

    def writer[A](w: W, v: A) = WriterT((w -> v).point)
    def listen[A](fa: WriterT[F, W, A]) = WriterT(
      fa.run.map { case (w, a) => (w, (a, w)) })
  }

El ejemplo más obvio es usar MonadTell para loggear información, o para reportar la auditoría. Reusar Meta para reportar errores podría servir para crear una estructura de log como

  sealed trait Log
  final case class Debug(msg: String)(implicit m: Meta)   extends Log
  final case class Info(msg: String)(implicit m: Meta)    extends Log
  final case class Warning(msg: String)(implicit m: Meta) extends Log

y usar Dequeue[Log] como nuestro tipo de journal. Podríamos cambiar nuestro método OAuth2 authenticate a

  def debug(msg: String)(implicit m: Meta): Dequeue[Log] = Dequeue(Debug(msg))

  def authenticate: F[CodeToken] =
    for {
      callback <- user.start :++> debug("started the webserver")
      params   = AuthRequest(callback, config.scope, config.clientId)
      url      = config.auth.withQuery(params.toUrlQuery)
      _        <- user.open(url) :++> debug(s"user visiting $url")
      code     <- user.stop :++> debug("stopped the webserver")
    } yield code

Incluso podríamos conbinar esto con las trazas de ReaderT y tener logs estructurados.

El que realiza la llamada puede recuperar los logs con .written y hacer algo con ellos.

Sin embargo, existe el argumento fuerte de que el logging merece su propia álgebra. El nivel de log es con frecuencia necesario en el momento de creación por razones de rendimiento y la escritura de logs es típicamente manejado a nivel de la aplicación más bien que algo sobre lo que cada componente necesite estar preocupado.

La W en WriterT tiene un Monoid, permitiéndonos escribir al journal cualquier clase de cálculoo monoidal como un valor secundario junto con nuestro programa principal. Por ejemplo, el conteo del número de veces que hacemos algo, construyendo una explicación del cálculo, o la construcción de una TradeTemplate para una nueva transacción mientras que asignamos un precio.

Una especialización popular de WriterT ocurre cuando la mónada es Id, indicando que el valor subyacente run es simplemente la tupla simple (W, A).

  type Writer[W, A] = WriterT[Id, W, A]
  object WriterT {
    def writer[W, A](v: (W, A)): Writer[W, A] = WriterT[Id, W, A](v)
    def tell[W](w: W): Writer[W, Unit] = WriterT((w, ()))
    ...
  }
  final implicit class WriterOps[A](self: A) {
    def set[W](w: W): Writer[W, A] = WriterT(w -> self)
    def tell: Writer[A, Unit] = WriterT.tell(self)
  }

que nos permite dejar que cualquier valor lleve consigo un cálculo monoidal secundario, sin la necesidad de un contexto F[_].

En resumen, WriterT / MonadTell es la manera de conseguir multi tareas en la programación funcional.

7.4.6 StateT

StateT nos permite ejecutar .put, .get y .modify sobre un valor que es manejado por el contexto monádico. Es el reemplazo funcional de var.

Si fueramos a escribir un método impuro que tiene acceso a algún estado mutable, y contenido en un var, pudiera haber tenido la firma () => F[A] y devolver un valor diferente en cada invocación, rompiendo con la transparencia referencial. Con la programación funcional pura, la función toma el estado como la entrada y devuelve el estado actualizado como la salida, que es la razón por la que el tipo subyacente de StateT es S => F[(S, A)].

La mónada asociada es MonadState

  @typeclass trait MonadState[F[_], S] extends Monad[F] {
    def put(s: S): F[Unit]
    def get: F[S]

    def modify(f: S => S): F[Unit] = get >>= (s => put(f(s)))
    ...
  }

StateT se implementa de manera ligeramente diferente que los transformadores de mónada que hemos estudiado hasta el momento. En lugar de usar un case class es una ADT con dos miembros:

  sealed abstract class StateT[F[_], S, A]
  object StateT {
    def apply[F[_], S, A](f: S => F[(S, A)]): StateT[F, S, A] = Point(f)

    private final case class Point[F[_], S, A](
      run: S => F[(S, A)]
    ) extends StateT[F, S, A]
    private final case class FlatMap[F[_], S, A, B](
      a: StateT[F, S, A],
      f: (S, A) => StateT[F, S, B]
    ) extends StateT[F, S, B]
    ...
  }

que es una forma especializada de Trampoline, proporcionandonos seguridad en el uso de la pila cuando deseamos recuperar la estructura de datos subyacente, .run:

  sealed abstract class StateT[F[_], S, A] {
    def run(initial: S)(implicit F: Monad[F]): F[(S, A)] = this match {
      case Point(f) => f(initial)
      case FlatMap(Point(f), g) =>
        f(initial) >>= { case (s, x) => g(s, x).run(s) }
      case FlatMap(FlatMap(f, g), h) =>
        FlatMap(f, (s, x) => FlatMap(g(s, x), h)).run(initial)
    }
    ...
  }

StateT puede implemnentar de manera directa MonadState con su ADT:

  implicit def monad[F[_]: Applicative, S] = new MonadState[StateT[F, S, ?], S] {
    def point[A](a: =>A) = Point(s => (s, a).point[F])
    def bind[A, B](fa: StateT[F, S, A])(f: A => StateT[F, S, B]) =
      FlatMap(fa, (_, a: A) => f(a))

    def get       = Point(s => (s, s).point[F])
    def put(s: S) = Point(_ => (s, ()).point[F])
  }

con .pure reflejado en el objeto compañero como .stateT:

  object StateT {
    def stateT[F[_]: Applicative, S, A](a: A): StateT[F, S, A] = ...
    ...
  }

y MonadTrans.liftM proporcionando el constructor F[A] => StateT[F, S, A] como es usual.

Una variante común de StateT es cuando F = Id, proporcionando la signatura de tipo subyacente S => (S, A). Scalaz proporciona un alias de tipo y funciones convenientes para interactuar con el transformador de mónadas State de manera directa, y reflejando MonadState:

  type State[a] = StateT[Id, a]
  object State {
    def apply[S, A](f: S => (S, A)): State[S, A] = StateT[Id, S, A](f)
    def state[S, A](a: A): State[S, A] = State((_, a))

    def get[S]: State[S, S] = State(s => (s, s))
    def put[S](s: S): State[S, Unit] = State(_ => (s, ()))
    def modify[S](f: S => S): State[S, Unit] = ...
    ...
  }

Para un ejemplo podemos regresar a las pruebas de lógica de negocios de drone-dynamic-agents. Recuerde del Capítulo 3 que creamos Mutablep como intérpretes de prueba para nuestra aplicación y que almacenamos el número de started y los nodos stopped en una var`.

  class Mutable(state: WorldView) {
    var started, stopped: Int = 0

    implicit val drone: Drone[Id] = new Drone[Id] { ... }
    implicit val machines: Machines[Id] = new Machines[Id] { ... }
    val program = new DynAgentsModule[Id]
  }

Ahora sabemos que podemos escribir un mucho mejor simulador de pruebas con State. Tomaremos la oportunidad de hacer un ajuste a la precisión de nuestra simulación al mismo tiempo. Recuerde que un objeto clave de nuestro dominio de aplicación es la visión del mundo:

  final case class WorldView(
    backlog: Int,
    agents: Int,
    managed: NonEmptyList[MachineNode],
    alive: Map[MachineNode, Epoch],
    pending: Map[MachineNode, Epoch],
    time: Epoch
  )

Dado que estamos escribiendo una simulación del mundo para nuestras pruebas, podemos crear un tipo de datos que capture la realidad de todo

  final case class World(
    backlog: Int,
    agents: Int,
    managed: NonEmptyList[MachineNode],
    alive: Map[MachineNode, Epoch],
    started: Set[MachineNode],
    stopped: Set[MachineNode],
    time: Epoch
  )

La diferencia principal es que los nodos que están en los estados started y stopped pueden ser separados. Nuestr intérprete puede ser implementado en términos de State[World, a] y popdemos escribir nuestras pruebas para realizar aserciones sobre la forma en la que se ve el World y WorldView después de haber ejecutado la lógica de negocios.

Los intérpretes, que están simulando el contacto externo con los servicios Drone y Google, pueden implementarse como:

  import State.{ get, modify }
  object StateImpl {
    type F[a] = State[World, a]

    private val D = new Drone[F] {
      def getBacklog: F[Int] = get.map(_.backlog)
      def getAgents: F[Int]  = get.map(_.agents)
    }

    private val M = new Machines[F] {
      def getAlive: F[Map[MachineNode, Epoch]]   = get.map(_.alive)
      def getManaged: F[NonEmptyList[MachineNode]] = get.map(_.managed)
      def getTime: F[Epoch]                      = get.map(_.time)

      def start(node: MachineNode): F[Unit] =
        modify(w => w.copy(started = w.started + node))
      def stop(node: MachineNode): F[Unit] =
        modify(w => w.copy(stopped = w.stopped + node))
    }

    val program = new DynAgentsModule[F](D, M)
  }

y podemos reescribir nuestras pruebas para seguir la convención donde:

  • world1 es el estado del mundo antes de ejecutar el programa
  • view1 es la creencia/visión de la aplicación sobre el mundo
  • world2 es el estado del mundo después de ejecutar el programa
  • view2 es la creencia/visión sobre la aplicación después de ejecutar el programa

Por ejemplo,

  it should "request agents when needed" in {
    val world1          = World(5, 0, managed, Map(), Set(), Set(), time1)
    val view1           = WorldView(5, 0, managed, Map(), Map(), time1)

    val (world2, view2) = StateImpl.program.act(view1).run(world1)

    view2.shouldBe(view1.copy(pending = Map(node1 -> time1)))
    world2.stopped.shouldBe(world1.stopped)
    world2.started.shouldBe(Set(node1))
  }

Esperemos que el lector nos perdone al mirar atrás a nuestro bucle anterior con implementación de lógica de negocio

  state = initial()
  while True:
    state = update(state)
    state = act(state)

y usar StateT para manejar el estado (state). Sin embargo, nuestra lógica de negocios DynAgents requiere únicamente de Applicative y estaríamos violando la ley del poder mínimo al requerir MonadState que es estrictamente más poderoso. Es, por lo tanto, enteramente razonable manejar el estado manualmente al pasarlo en un update y act, y dejar que quien realiza una llamada use un StateT si así lo desea.

7.4.7 IndexedStateT

El código que hemos estudiado hasta el momento no es como Scalaz implementa StateT. En lugar de esto, un type alias apunta a IndexedStateT

  type StateT[F[_], S, A] = IndexedStateT[F, S, S, A]

La implementación de IndexedStateT es básicamente la que ya hemos estudiado, con un parámetro de tipo extra que permiten que los estados de entrada S1 y S2 difieran:

  sealed abstract class IndexedStateT[F[_], -S1, S2, A] {
    def run(initial: S1)(implicit F: Bind[F]): F[(S2, A)] = ...
    ...
  }
  object IndexedStateT {
    def apply[F[_], S1, S2, A](
      f: S1 => F[(S2, A)]
    ): IndexedStateT[F, S1, S2, A] = Wrap(f)

    private final case class Wrap[F[_], S1, S2, A](
      run: S1 => F[(S2, A)]
    ) extends IndexedStateT[F, S1, S2, A]
    private final case class FlatMap[F[_], S1, S2, S3, A, B](
      a: IndexedStateT[F, S1, S2, A],
      f: (S2, A) => IndexedStateT[F, S2, S3, B]
    ) extends IndexedStateT[F, S1, S3, B]
    ...
  }

IndexedStateT no tiene una MonadState cuando S1 != s2, aunque sí tiene una Monad.

El siguiente ejemplo está adaptado de Index your State de Vincent Marquez.

Considere el escenario en el que deseamos diseñar una interfaz algebraica para una búsqueda de un mapeo de un Inta un String. Se puede tratar de una implementación en red y el orden de las invocaciones es esencial. Nuestro primer intento de realizar la API es algo como lo siguiente:

  trait Cache[F[_]] {
    def read(k: Int): F[Maybe[String]]

    def lock: F[Unit]
    def update(k: Int, v: String): F[Unit]
    def commit: F[Unit]
  }

con errores en tiempo de ejecución si .update o .commit es llamada sin un .lock. Un diseño más complejo puede envolver múltiples traits y una DSL a la medida que nadie recuerde cómo usar.

En vez de esto, podemos usar IndexedStateT para requerir que la invocación se realice en el estado correcto. Primero definimos nuestros estados posibles como una ADT.

  sealed abstract class Status
  final case class Ready()                          extends Status
  final case class Locked(on: ISet[Int])            extends Status
  final case class Updated(values: Int ==>> String) extends Status

y entonces revisitar nuestra álgebra

  trait Cache[M[_]] {
    type F[in, out, a] = IndexedStateT[M, in, out, a]

    def read(k: Int): F[Ready, Ready, Maybe[String]]
    def readLocked(k: Int): F[Locked, Locked, Maybe[String]]
    def readUncommitted(k: Int): F[Updated, Updated, Maybe[String]]

    def lock: F[Ready, Locked, Unit]
    def update(k: Int, v: String): F[Locked, Updated, Unit]
    def commit: F[Updated, Ready, Unit]
  }

lo que nos ocasionará un error en tiempo de compilación si intentamos realizar un .update sin un .lock

  for {
        a1 <- C.read(13)
        _  <- C.update(13, "wibble")
        _  <- C.commit
      } yield a1

  [error]  found   : IndexedStateT[M,Locked,Ready,Maybe[String]]
  [error]  required: IndexedStateT[M,Ready,?,?]
  [error]       _  <- C.update(13, "wibble")
  [error]          ^

pero nos permite construir funciones que pueden componerse al incluirlas explícitamente en su estado:

  def wibbleise[M[_]: Monad](C: Cache[M]): F[Ready, Ready, String] =
    for {
      _  <- C.lock
      a1 <- C.readLocked(13)
      a2 = a1.cata(_ + "'", "wibble")
      _  <- C.update(13, a2)
      _  <- C.commit
    } yield a2

7.4.8 IndexedReaderWriterStateT

Aquellos que deseen una combinación de ReaderT, WriterT e IndexedStateT no serán decepcionados. El transformador IndexedReaderWriterStateT envuelve (R, S1) => F[(W, A, S2)] con R teniendo una semántica de Reader, W es para escrituras monoidales, y los parámetros S para actualizaciones de estado indexadas.

  sealed abstract class IndexedReaderWriterStateT[F[_], -R, W, -S1, S2, A] {
    def run(r: R, s: S1)(implicit F: Monad[F]): F[(W, A, S2)] = ...
    ...
  }
  object IndexedReaderWriterStateT {
    def apply[F[_], R, W, S1, S2, A](f: (R, S1) => F[(W, A, S2)]) = ...
  }

  type ReaderWriterStateT[F[_], -R, W, S, A] = IndexedReaderWriterStateT[F, R, W, S, S, A]
  object ReaderWriterStateT {
    def apply[F[_], R, W, S, A](f: (R, S) => F[(W, A, S)]) = ...
  }

Las abreviaturas se proporcionan porque de otra manera, con toda honestidad, estos tipos son tan largos que parecen que son parte de una API de J2EE:

  type IRWST[F[_], -R, W, -S1, S2, A] = IndexedReaderWriterStateT[F, R, W, S1, S2, A]
  val IRWST = IndexedReaderWriterStateT
  type RWST[F[_], -R, W, S, A] = ReaderWriterStateT[F, R, W, S, A]
  val RWST = ReaderWriterStateT

IRWST es una implementación más eficiente que una pila de transformadores creadas como ReaderT[WriterT[IndexedStateT[F, ...], ...], ...].

7.4.9 TheseT

TheseT permite que los errores aborten los cálculos o que se acumulen si existe un éxito parcial. Por esta razón recibe el nombre de mantén la calma y continúa.

El tipo de datos subyacente es F[A \&/ B] con una A siendo el tipo del error, requiriendo que exista un Semigroup para permitir la acumulación de errores

  final case class TheseT[F[_], A, B](run: F[A \&/ B])
  object TheseT {
    def `this`[F[_]: Functor, A, B](a: F[A]): TheseT[F, A, B] = ...
    def that[F[_]: Functor, A, B](b: F[B]): TheseT[F, A, B] = ...
    def both[F[_]: Functor, A, B](ab: F[(A, B)]): TheseT[F, A, B] = ...

    implicit def monad[F[_]: Monad, A: Semigroup] = new Monad[TheseT[F, A, ?]] {
      def bind[B, C](fa: TheseT[F, A, B])(f: B => TheseT[F, A, C]) =
        TheseT(fa.run >>= {
          case This(a) => a.wrapThis[C].point[F]
          case That(b) => f(b).run
          case Both(a, b) =>
            f(b).run.map {
              case This(a_)     => (a |+| a_).wrapThis[C]
              case That(c_)     => Both(a, c_)
              case Both(a_, c_) => Both(a |+| a_, c_)
            }
        })

      def point[B](b: =>B) = TheseT(b.wrapThat.point[F])
    }
  }

No hay mónada especial asociada con TheseT, que simplemente es una Monad regular. Si desearamos abortar el cálculo podríamos devolver un valor This, pero estamos acumulando errores cuando devolvemos una Both que también contiene la parte exitosa del cálculo.

TheseT también puede analizarse desde una perspectiva distinta: A no necesita ser un error. De manera similar a WriterT, la A puede ser un cálculo secundario que estamos realizando junto con un cálculo primario B. TheseT permite la salida temprana cuando algo especial sobre A lo demanda.

7.4.10 ContT

El estilo de programación conocido como CPS (por sus siglas en inglés Continuation Passing Style) consiste en el uso de funciones que nunca regresan, y en lugar de esto, continúan al siguiente cómputo. CPS es popular en JavaScript y en Lisp dado que permiten el uso de callbacks cuando los datos están disponibles. Una implementación equivalente del patrón en Scala impuro se vería como

  def foo[I, A](input: I)(next: A => Unit): Unit = next(doSomeStuff(input))

y podríamos hacer que el cómputo fuera puro al introducir el contexto F[_]

  def foo[F[_], I, A](input: I)(next: A => F[Unit]): F[Unit]

y refactorizar para devolver una función para la entrada provista

  def foo[F[_], I, A](input: I): (A => F[Unit]) => F[Unit]

ContT es simplemente un contenedor con esta signatura, con una instancia de Monad

  final case class ContT[F[_], B, A](_run: (A => F[B]) => F[B]) {
    def run(f: A => F[B]): F[B] = _run(f)
  }
  object IndexedContT {
    implicit def monad[F[_], B] = new Monad[ContT[F, B, ?]] {
      def point[A](a: =>A) = ContT(_(a))
      def bind[A, C](fa: ContT[F, B, A])(f: A => ContT[F, B, C]) =
        ContT(c_fb => fa.run(a => f(a).run(c_fb)))
    }
  }

y sintaxis conveniente para crear una ContT a partir de un valor monádico:

  implicit class ContTOps[F[_]: Monad, A](self: F[A]) {
    def cps[B]: ContT[F, B, A] = ContT(a_fb => self >>= a_fb)
  }

Sin embargo, el uso simple de callbacks en las continuaciones no trae nada a la programación funcional pura debido a que ya conocemos cómo secuenciar cómputos que no bloqueen, potencialmente distribuidos: es para esto que sirve Monad y podemos hacer esto con un .bind o con una flecha Kleisli. Para observar por qué razón las continuaciones son útiles necesitamos considerar un ejemplo más complejo bajo una restricción de diseño más rígida.

7.4.10.1 Control de Flujo

Digamos que hemos modularizado nuestra aplicación usando componentes que pueden realizar I/O, y que cada componente es desarrollado por equipo distintos:

  final case class A0()
  final case class A1()
  final case class A2()
  final case class A3()
  final case class A4()

  def bar0(a4: A4): IO[A0] = ...
  def bar2(a1: A1): IO[A2] = ...
  def bar3(a2: A2): IO[A3] = ...
  def bar4(a3: A3): IO[A4] = ...

Nuestra meta es producir una A0 dada una A1. Mientras que JavaScript y Lisp usarían continuaciones para resolver este problema (debido a que la I/O es bloqueante), podríamos simplemente encadenar las funciones

  def simple(a: A1): IO[A0] = bar2(a) >>= bar3 >>= bar4 >>= bar0

Podríamos elevar .simple a su forma escrita con continuaciones al usar sintaxis conveniente .cps y un poco de código extra (boilerplate) para cada paso:

  def foo1(a: A1): ContT[IO, A0, A2] = bar2(a).cps
  def foo2(a: A2): ContT[IO, A0, A3] = bar3(a).cps
  def foo3(a: A3): ContT[IO, A0, A4] = bar4(a).cps

  def flow(a: A1): IO[A0]  = (foo1(a) >>= foo2 >>= foo3).run(bar0)

De modo que, ¿qué nos brinda esto? Primeramente, es digno de mención que el flujo de control de la aplicación es de izquierda a derecha

¿Que tal si fuéramos los autores de foo2 y desearamos post-procesar la a0 que recivimos de la derecha (de más adelante en el proceso de cómputo), es decir, deseamos dividir nuestro foo2 en foo2a y foo2b

Agregue la restricción de que no es posible cambiar la definición de flow o bar0. Tal vez no es nuestro código y está definido por el framework que estemos usando.

No es posible procesar la salida de a0 al modificar cualquiera de los métodos restantes barX. Sin embargo, con ContT podemos modificar foo2 para procesar el resultado de la continuación next:

que puede definirse con

  def foo2(a: A2): ContT[IO, A0, A3] = ContT { next =>
    for {
      a3  <- bar3(a)
      a0  <- next(a3)
    } yield process(a0)
  }

No estamos limitados a mapear sobre el valor devuelto, ¡también podemos realizar un .bind en otro control de flujo devolviendo el flujo lineal en un grafo!

  def elsewhere: ContT[IO, A0, A4] = ???
  def foo2(a: A2): ContT[IO, A0, A3] = ContT { next =>
    for {
      a3  <- bar3(a)
      a0  <- next(a3)
      a0_ <- if (check(a0)) a0.pure[IO]
             else elsewhere.run(bar0)
    } yield a0_
  }

O podemos mantenernos dentro del flujo original y reintentar todo lo que sigue

  def foo2(a: A2): ContT[IO, A0, A3] = ContT { next =>
    for {
      a3  <- bar3(a)
      a0  <- next(a3)
      a0_ <- if (check(a0)) a0.pure[IO]
             else next(a3)
    } yield a0_
  }

Se trata de únicamente un reintento, no de un ciclo infinito. Por ejemplo, podríamos solicitar que cómputos subsiguientes reconfirmaran una acción potencialmente peligrosa.

Finalmente, podemos realizar acciones que son específicas dentro del contexto de ContT, en este caso IO que nos deja hacer un manejo de errores y limpieza de recursos.

  def foo2(a: A2): ContT[IO, A0, A3] = bar3(a).ensuring(cleanup).cps
7.4.10.2 Cuándo ordenar espagueti

No es un accidente que estos diagramas se vean como espagueti, y eso es exactamente lo que ocurre cuando empezamos a manipular el control de flujo. Todos los mecanismos que hemos discutido en esta sección son simples de implementar directamente si podemos editar la definición de flow, y por lo tanto no requerimos usar ContT.

Sin embargo, si estamos diseñando un framework, deberíamos podemos considerar el sistema de plugins como callbacks de ContT para permitir a nuestros usuarios más poder sobre el control de flujo. Algunas veces el cliente simplemente quiere el espagueti.

Por ejemplo, si el compilador de Scala estuviera escrito usando CPS, permitiría un enfoque basado en principios para comunicar las fases del compilador. Un plugin para el compilador sería capaz de realizar algunas acciones basándose en el tipo inferido de una expresión, calculado en una etapa posterior del proceso de compilación. De manera similar, las continuaciones serían una buena API para una herramienta extensible para los builds, o para un editor de texto.

Algo que debería considerarse con ContT es que no tiene un uso seguro de la pila, de modo que no es posible usarla para programas que se ejecutan por siempre.

7.4.10.3 No use ContT

Una variante más compleja de ContT llamada IndexedContT envuelve (A => F[B]) => F[C]. El nuevo parámetro de tipo C permite al tipo de retorno del cómputo completo sea diferente del tipo de retorno entre cada componente. Pero si B no es igual a C entonces no hay una Monad.

Sin perder la oportunidad de generalizar tanto como sea posible, IndexedContT es realmente implementada en términos de una estructura aún más general (note la s estra antes de la T)

  final case class IndexedContsT[W[_], F[_], C, B, A](_run: W[A => F[B]] => F[C])

  type IndexedContT[f[_], c, b, a] = IndexedContsT[Id, f, c, b, a]
  type ContT[f[_], b, a]           = IndexedContsT[Id, f, b, b, a]
  type ContsT[w[_], f[_], b, a]    = IndexedContsT[w, f, b, b, a]
  type Cont[b, a]                  = IndexedContsT[Id, Id, b, b, a]

donde W[_] tiene una Comonad, y ContT está implementada como un alias de tipo. Los objetos compañeros existen para contener los aliases de tipo con constructores de tipo convenientes.

La verdad es que, cinco parámetros de tipo es tal vez una generalización bastante amplia (tal vez demasiado). Pero de nuevo, la sobre-generalización es consistente con las sensibilidades de las continuaciones.

7.4.11 Las pilas de transformadores y los implícitos ambiguos

Esto concluye nuestro tour de los transformadores de mónadas en Scalaz.

Cuando se combinan múltiples transformadores, llamamos a esto una pila de transformadores y aunque es muy verboso, es posible leer las características al leer los transformadores. Por ejemplo, si construimos un contexto F[_] que sea un conjunto de transformadores compuestos, tales como

  type Ctx[A] = StateT[EitherT[IO, E, ?], S, A]

sabemos que estamos agregando manejadores de errores con un tipo de error E (existe un MonadError[Ctx, E]) y estamos manejando el estado A (existe una MonadState[Ctx, S]).

Lamentablemente, existen desventajas prácticas al uso de transformadores de mónadas y sus compañeras de typeclases Monad:

  1. Múltiples parámetros implícitos de Monad significan que el compilador no puede encontrar la sintaxis correcta que debe usarse para el contexto.
  2. Las mónadas no se pueden componer en el caso general, lo que significa que el orden de anidación de los transformadores es importante.
  3. Todos los intérpretes deben elevarse al contexto común. Por ejemplo, podríamos tener una implementación de alguna álgebra que use IO y ahora es necesario envolverla dentro de StateT y EitherT incluso cuando son usados dentro del intérprete.
  4. Existen costos de desempeño asociados a cada capa. Y algunos transformadores de mónadas son peores que otros. StateT es particularmente malo pero incluso EitherT puede ocasionar problemas de asignación de memoria para aplicaciones de alto desempeño.

Por eso es necesario considerar soluciones.

7.4.12 Sin sintaxis

Digamos que tenemos un álgebra

  trait Lookup[F[_]] {
    def look: F[Int]
  }

y algunos tipos de datos

  final case class Problem(bad: Int)
  final case class Table(last: Int)

que deseemos usar en nuestra lógica de negocios

  def foo[F[_]](L: Lookup[F])(
    implicit
      E: MonadError[F, Problem],
      S: MonadState[F, Table]
  ): F[Int] = for {
    old <- S.get
    i   <- L.look
    _   <- if (i === old.last) E.raiseError(Problem(i))
           else ().pure[F]
  } yield i

El primer problema que encontramos es que esto no compila

  [error] value flatMap is not a member of type parameter F[Table]
  [error]     old <- S.get
  [error]              ^

Existen algunas soluciones tácticas a este problema. La más obvia es hacer que todos los parámetros sean explícitos

  def foo1[F[_]: Monad](
    L: Lookup[F],
    E: MonadError[F, Problem],
    S: MonadState[F, Table]
  ): F[Int] = ...

y requerir únicamente que Monad sea pasado de manera implícita por medio de límites de contexto. Sin embargo, esto significa que debemos alambrar manualmente la MonadError y MonadState cuando invocamos foo1 y cuando llamamos otro método que requiere un implícito.

Una segunda solución es dejar los parámetros como implícitos y usar el shadowing de nombres para hacer todos excepto uno de los parámetros explícitos. Esto permite que computos previos usen la resolución implícita cuando nos llaman pero todavía necesitamos pasar los parámetros de manera explícita si las llamamos.

  @inline final def shadow[A, B, C](a: A, b: B)(f: (A, B) => C): C = f(a, b)

  def foo2a[F[_]: Monad](L: Lookup[F])(
    implicit
    E: MonadError[F, Problem],
    S: MonadState[F, Table]
  ): F[Int] = shadow(E, S) { (E, S) => ...

o podríamos hacer shadow de una sola Monad, dejando que la otra proporcione nuestra sintaxis y que esté disponible para cuando llamamos a otros métodos.

  @inline final def shadow[A, B](a: A)(f: A => B): B = f(a)
  ...

  def foo2b[F[_]](L: Lookup[F])(
    implicit
    E: MonadError[F, Problem],
    S: MonadState[F, Table]
  ): F[Int] = shadow(E) { E => ...

Una tercera opción, con un costo un poco más elevado, es la creación de una typeclass de Monad a la medida, que contenga referencias implícitas a las dos clases de Monad que nos interesan

  trait MonadErrorState[F[_], E, S] {
    implicit def E: MonadError[F, E]
    implicit def S: MonadState[F, S]
  }

y una derivación de la typeclass dada por una MonadError y MonadState

  object MonadErrorState {
    implicit def create[F[_], E, S](
      implicit
        E0: MonadError[F, E],
        S0: MonadState[F, S]
    ) = new MonadErrorState[F, E, S] {
      def E: MonadError[F, E] = E0
      def S: MonadState[F, S] = S0
    }
  }

Ahora, si deseamos acceder a S o E podemos obtenerlas por medio de F.S o F.E

  def foo3a[F[_]: Monad](L: Lookup[F])(
    implicit F: MonadErrorState[F, Problem, Table]
  ): F[Int] =
    for {
      old <- F.S.get
      i   <- L.look
      _ <- if (i === old.last) F.E.raiseError(Problem(i))
      else ().pure[F]
    } yield i

Como segunda solución, podemos escoger una de las instancias de Monad para que sea implícita dentro del bloque, y esto se puede conseguir al importarla

  def foo3b[F[_]](L: Lookup[F])(
    implicit F: MonadErrorState[F, Problem, Table]
  ): F[Int] = {
    import F.E
    ...
  }
7.4.12.1 Composición de transformadores

Un EitherT[StateT[...], ...] tiene un MonadError pero no tiene un MonadState, mientras que StateT[EitherT[...], ...] puede proporcionar ambos.

La solución es estudiar las derivaciones implícitas en el objeto compañero de los transformadores y podemos asegurarnos de que los transformadores más externos proporcionan todo lo que necesitamos.

Una regla de oro es que hay que usar los transformadores más complejos en el exterior, y este capítulo presentó los transformadores en orden creciente de complejidad.

7.4.12.2 Elevando transformadores

Continuando con el mismo ejemplo, digamos que nuestra álgebra Lookup tiene un intérprete IO

  object LookupRandom extends Lookup[IO] {
    def look: IO[Int] = IO { util.Random.nextInt }
  }

pero deseamos que nuestro contexto sea

  type Ctx[A] = StateT[EitherT[IO, Problem, ?], Table, A]

para proporcionarnos un MonadError y un MonadState. Esto significa que necesitamos envolver LookupRandom para que opere sobre nuestro Ctx.

Primero, deseamos usar la sintaxis .liftM disponible en Monad, que usa MonadTrans para elevar desde nuestro F[A] hacia G[F, A]

  final class MonadOps[F[_]: Monad, A](fa: F[A]) {
    def liftM[G[_[_], _]: MonadTrans]: G[F, A] = ...
    ...
  }

Es importante darse cuenta de que los parámetros de tipo de .liftM tienen dos hoyos, uno de la forma _[_] y el otro de forma _. Si creamos aliases de tipo para estas formas

  type Ctx0[F[_], A] = StateT[EitherT[F, Problem, ?], Table, A]
  type Ctx1[F[_], A] = EitherT[F, Problem, A]
  type Ctx2[F[_], A] = StateT[F, Table, A]

podemos abstraer sobre MonadTrans para elevar un Lookup[F] a cualquier Lookup[G[F, ?]] donde G es un transformador de mónadas.

  def liftM[F[_]: Monad, G[_[_], _]: MonadTrans](f: Lookup[F]) =
    new Lookup[G[F, ?]] {
      def look: G[F, Int] = f.look.liftM[G]
    }

Permitiéndonos envolver una vez para EitherT, y de nuevo para StateT

  val wrap1 = Lookup.liftM[IO, Ctx1](LookupRandom)
  val wrap2: Lookup[Ctx] = Lookup.liftM[EitherT[IO, Problem, ?], Ctx2](wrap1)

Otra forma de lograr esto, en un único paso, es usar MonadIO que permite elevar una IO en una pila de transformadores:

  @typeclass trait MonadIO[F[_]] extends Monad[F] {
    def liftIO[A](ioa: IO[A]): F[A]
  }

con instancias de MonadIO para todas las combinaciones comunes de transformadores.

El costo extra del código repetitivo para elevar un intérprete IO a cualquier instancia de MonadIO es por lo tanto de dos líneas de código (para la definición del intérprete), más una linea por elemento del álgebra, y una línea final para invocarla:

  def liftIO[F[_]: MonadIO](io: Lookup[IO]) = new Lookup[F] {
    def look: F[Int] = io.look.liftIO[F]
  }

  val L: Lookup[Ctx] = Lookup.liftIO(LookupRandom)
7.4.12.3 Desempeño

El problema más grande con los transformadores de mónadas es el costo adicional en términos de rendimiento. EitherT tenía un costo extra relativamente bajo, donde cada invocación de .flatMap generaba un grupo de objetos, pero esto puede afectar aplicaciones de alto rendimiento donde cada asignación de memoria adicional importa. Otros transformadores, tales como StateT, efectivamente agregan un trampolín, y ContT mantiene la cadena de invocaciones entera retenida en memoria.

Si el rendimiento se vuelve un problema, la solución es no usar transformadores de mónadas. Al menos no las estructuras de datos de los transformadores. Una gran ventaja de las typeclases de Monad es que podemos crear una versión optimizada F[_] para nuestra aplicación que proporcione las typeclasses naturalmente. Aprenderemos cómo crear una F[_] en los próximos dos capítulos, cuando estudiemos seriamente dos estructuras que ya hemos visto: Free y IO.

7.5 Una comida gratis

Nuestra industria ruega el uso de lenguajes de alto nivel que sean seguros, intercambiando la eficiencia de los desarrolladores y la confiabilidad por un tiempo reducido durante la ejecución.

El compilador JIT (por sus siglas en inglés: Just In Time) en la JVM tiene un desempeño tan alto que funciones simples tienen un desempeño comparable a sus equivalentes en C o C++, ignorando el costo del recolector de basura. Sin embargo, el JIT realiza únicamente optimizaciones de bajo nivel: predicción de bifurcaciones, el inlining (sustitución) de métodos, despliegue de ciclos, y así sucesivamente.

El JIT no realiza optimizaciones de nuestra lógica de negocios, por ejemplo realizar un agrupamiento de las llamadas de red, o la paralelización de tareas independientes. El desarrollador es responsable de escribir la lógica de negocio y las optimizaciones a la vez, reduciendo la legibilidad y haciendo el código más difícil de mantener. Sería muy bueno si las optimizaciones fueran asuntos de importancia secundaria.

Si, en vez de esto, tenemos una estructura de datos que describe nuestra lógica de negocios en términos de conceptos de alto nivel, no de instrucciones de máquina, podemos realizar optimizaciones de alto nivel. Las estructuras de datos de esta naturaleza se llaman, típicamente, estructuras Free (libres) y pueden generarse automáticamente para los miembros de interfaces algebraicas de nuestro programa. Por ejemplo, un Free Applicative puede ser generado para permitirnos ejecutar agrupamiento por lotes o la simplificación de I/O costosa a través de la red.

En esta sección aprenderemos cómo crear estructuras libres, y cómo pueden ser usadas.

7.5.1 Free (Monad)

Fundamentalmente, las mónadas describen un programa secuencial donde cada paso depende del previo. Por lo tanto estamos limitados a modificaciones que sólo saben sobre cosas que ya hemos ejecutado y la próxima que vamos a ejecutar.

Como un recordatorio, Free es la representación de la estructura de datos de una Monad y está definido por tres miembros

  sealed abstract class Free[S[_], A] {
    def mapSuspension[T[_]](f: S ~> T): Free[T, A] = ...
    def foldMap[M[_]: Monad](f: S ~> M): M[A] = ...
    ...
  }
  object Free {
    implicit def monad[S[_], A]: Monad[Free[S, A]] = ...

    private final case class Suspend[S[_], A](a: S[A]) extends Free[S, A]
    private final case class Return[S[_], A](a: A)     extends Free[S, A]
    private final case class Gosub[S[_], A0, B](
      a: Free[S, A0],
      f: A0 => Free[S, B]
    ) extends Free[S, B] { type A = A0 }

    def liftF[S[_], A](value: S[A]): Free[S, A] = Suspend(value)
    ...
  }
  • Suspend representa un programa que todavía no ha sido interpretado
  • Return es .pure
  • Gosub es .bind

Un valor Free[S, A] puede ser generada de manera automática para cualquier álgebra S. Para hacer esto explícito, considere nuesra aplicación del álgebra de Machines

  trait Machines[F[_]] {
    def getTime: F[Epoch]
    def getManaged: F[NonEmptyList[MachineNode]]
    def getAlive: F[Map[MachineNode, Epoch]]
    def start(node: MachineNode): F[Unit]
    def stop(node: MachineNode): F[Unit]
  }

Podemos definir una estructura Free generada para Machines al crear una ADT con un tipo de datos para cada elemento en el álgebra. Cada tipo de datos tiene los mismos tipos de parámetros como su elemento correspondiente, está parametrizado sobre el tipo de retorno, y tiene el mismo nombre:

  object Machines {
    sealed abstract class Ast[A]
    final case class GetTime()                extends Ast[Epoch]
    final case class GetManaged()             extends Ast[NonEmptyList[MachineNode]]
    final case class GetAlive()               extends Ast[Map[MachineNode, Epoch]]
    final case class Start(node: MachineNode) extends Ast[Unit]
    final case class Stop(node: MachineNode)  extends Ast[Unit]
    ...

La ADT define un AST (por sus siglas en inglés Abstract Syntax Tree) debido a que cada miembro está representando un cómputo en un programa.

Entonces definimos .liftF, una implementación de Machines, con Free[Ast, ?] siendo el contexto. Todo método simplemente delega a Free.liftT para crear un Suspend

  ...
    def liftF = new Machines[Free[Ast, ?]] {
      def getTime = Free.liftF(GetTime())
      def getManaged = Free.liftF(GetManaged())
      def getAlive = Free.liftF(GetAlive())
      def start(node: MachineNode) = Free.liftF(Start(node))
      def stop(node: MachineNode) = Free.liftF(Stop(node))
    }
  }

cuando construimos un programa, parametrizado sobre un valor Free, lo ejecutamos al proporcionar un intérprete (una transformación natural Ast ~> M) al método .foldMap. Por ejemplo, si pudieramos proporcionar un intérprete que mapee a IO podemos construir un IO[Unit] por medio del AST libre

  def program[F[_]: Monad](M: Machines[F]): F[Unit] = ...

  val interpreter: Machines.Ast ~> IO = ...

  val app: IO[Unit] = program[Free[Machines.Ast, ?]](Machines.liftF)
                        .foldMap(interpreter)

En aras de la exhaustividad, un intérprete que delega a una implementación directa es fácil de escribir. Esto puede ser útil si el resto de la aplicación está usando Free como el contexto y nosotros tenemos una implementación IO que deseamos usar:

  def interpreter[F[_]](f: Machines[F]): Ast ~> F = λ[Ast ~> F] {
    case GetTime()    => f.getTime
    case GetManaged() => f.getManaged
    case GetAlive()   => f.getAlive
    case Start(node)  => f.start(node)
    case Stop(node)   => f.stop(node)
  }

Pero nuestra lógica de negocios requiere de más que simplemente Machines, también requerimos acceder a nuestra álgebra de Drone, y recuerde que está definido como

  trait Drone[F[_]] {
    def getBacklog: F[Int]
    def getAgents: F[Int]
  }
  object Drone {
    sealed abstract class Ast[A]
    ...
    def liftF = ...
    def interpreter = ...
  }

Lo que deseamos es que nuestra AST sea la combinación de ASTs para Machines y Drone. Estudiamos Coproduct en el capítulo 6, que es una disjunción de alta clase (una higher kinded disjunction):

  final case class Coproduct[F[_], G[_], A](run: F[A] \/ G[A])

Podemos usar el contexto Free[Coproduct[Machines.Ast, Drone.Ast, ?], ?].

Podríamos crear manualmente el coproducto pero estaríamos nadando en código repetivo, y tendríamos que repetirlo todo de nuevo si desearamos agregar una tercer álgebra.

La typeclass con el nombre scalaz.Inject ayuda:

  type :<:[F[_], G[_]] = Inject[F, G]
  sealed abstract class Inject[F[_], G[_]] {
    def inj[A](fa: F[A]): G[A]
    def prj[A](ga: G[A]): Option[F[A]]
  }
  object Inject {
    implicit def left[F[_], G[_]]: F :<: Coproduct[F, G, ?]] = ...
    ...
  }

Las derivaciones implícitas generan instancias de Inject cuando las requerimos, permitiéndonos reescribir nuestros liftF para que funcionen con cualquier combinación de ASTs:

  def liftF[F[_]](implicit I: Ast :<: F) = new Machines[Free[F, ?]] {
    def getTime                  = Free.liftF(I.inj(GetTime()))
    def getManaged               = Free.liftF(I.inj(GetManaged()))
    def getAlive                 = Free.liftF(I.inj(GetAlive()))
    def start(node: MachineNode) = Free.liftF(I.inj(Start(node)))
    def stop(node: MachineNode)  = Free.liftF(I.inj(Stop(node)))
  }

Sería bueno que F :<: G se leyera como si nuestro Ast fuera un miembro del conjunto completo de instrucciones F: esta sintaxis es intencional.

Poniendo todo junto, digamos que tenemos un program que escribimos abstrayendo sobre Monad

  def program[F[_]: Monad](M: Machines[F], D: Drone[F]): F[Unit] = ...

y tenemos algunas implementaciones existentes de Machines y Drone, y podemos crear interpretes a partir de ellos:

  val MachinesIO: Machines[IO] = ...
  val DroneIO: Drone[IO]       = ...

  val M: Machines.Ast ~> IO = Machines.interpreter(MachinesIO)
  val D: Drone.Ast ~> IO    = Drone.interpreter(DroneIO)

y combinarlos en un conjunto de instrucciones más grande usando un método conveniente de nuestro objeto compañero en NaturalTransformation

  object NaturalTransformation {
    def or[F[_], G[_], H[_]](fg: F ~> G, hg: H ~> G): Coproduct[F, H, ?] ~> G = ...
    ...
  }

  type Ast[a] = Coproduct[Machines.Ast, Drone.Ast, a]

  val interpreter: Ast ~> IO = NaturalTransformation.or(M, D)

Y entonces usarlo para produir un IO

  val app: IO[Unit] = program[Free[Ast, ?]](Machines.liftF, Drone.liftF)
                        .foldMap(interpreter)

¡Pero hemos viajado en círculos! Podríamos haber usado IO como el contexto de nuestro programa en primer lugar y haber evitado Free. De modo que por qué pasamos por todos estos problemas? A continuación tenemos algunos ejemplos de cuando Free podría ser de utilidad.

7.5.1.1 Pruebas con Mocks y Stubs

Puede parecer hipócrita proponer que Free pueda usarse para reducir el boilerplate, dada la gran cantidad de código que hemos escrito. Sin embargo, existe un punto de inflexión cuando el Ast paga por sí mismo cuando tenemos muchas pruebas que requieren implementaciones stub.

Si los métodos .Ast y .liftF están definidos para un álgebra, podemos crear intérpretes parciales

  val M: Machines.Ast ~> Id = stub[Map[MachineNode, Epoch]] {
    case Machines.GetAlive() => Map.empty
  }
  val D: Drone.Ast ~> Id = stub[Int] {
    case Drone.GetBacklog() => 1
  }

que puede ser usado para probar nuestro program

  program[Free[Ast, ?]](Machines.liftF, Drone.liftF)
    .foldMap(or(M, D))
    .shouldBe(1)

Al usar funciones parciales, y no funciones totales, estamos exponiéndonos a errores en tiempo de ejecución. Muchos equipos están felices de aceptar este costo en sus pruebas unitarias dado que el test fallaría si hay un error del programador.

También es posible lograr lo mismo con implementaciones de nuestras álgebras que implementen cada método con ???, haciendo un override de cada cosa necesaria según sea el caso.

7.5.1.2 Monitoreo

Es típico que las aplicaciones de servidor sean monitoreadas por agentes de ejecución que manipulan el bytecode para insertar profilers y extraer información de distintas clases de uso o rendimiento.

Si el contexto de nuestra aplicación es Free, no es necesario recurrir a la manipulación de bytecode, y en vez de esto es posible implementar un monitor de efectos laterales como un intérprete sobre el que tenemos completo control.

Por ejemplo, considere usar este agente Ast ~> Ast:

  val Monitor = λ[Demo.Ast ~> Demo.Ast](
    _.run match {
      case \/-(m @ Drone.GetBacklog()) =>
        JmxAbstractFactoryBeanSingletonProviderUtilImpl.count("backlog")
        Coproduct.rightc(m)
      case other =>
        Coproduct(other)
    }
  )

que almacena invocaciones del método: podríamos usar una rutina específica del vendedor en código real. También podríamos monitorear mensajes de interés específicos y registrarlos como una ayuda de depuración.

Podemos adjuntar Monitor a nuestra aplicación de producción Free con

  .mapSuspension(Monitor).foldMap(interpreter)

o combinar las transformaciones naturales y ejecutarlas con un único

  .foldMap(Monitor.andThen(interpreter))
7.5.1.3 Parches al código

Como ingenieros, estamos acostumbrados a peticiones de soluciones bizarras que es necesario añadir a la lógica central de la aplicación. Tal vez deseemos programar tales casos especiales como excepciones a la regla y manejarlos de manera tangencial a la lógica central de nuestro programa.

Por ejemplo, suponga que obtenemos un memo de contabilidad indicándonos lo siguiente

URGENTE: Bob está usando el nodo #c0fee para ejecutar el final del año. ¡NO DETENGA ESTA MÁQUINA!

No hay posibilidad de discutir porqué Bob no debería estar usando nuestras máquinas para sus cuentas super importantes, de modo que es necesario hackear nuestra lógica de negocio y poner un release a producción tan pronto como sea posible.

Nuestro parche al código puede mapear a una estructura Free, permitiéndonos regresar un resultado predeterminado (Free.pure) en lugar de programar la ejecución de la instrucción. Creamos un caso especial para la instrucción en una transformación natural personalizada con su valor de retorno:

  val monkey = λ[Machines.Ast ~> Free[Machines.Ast, ?]] {
    case Machines.Stop(MachineNode("#c0ffee")) => Free.pure(())
    case other                                 => Free.liftF(other)
  }

después de haber revisado que funcione, subimos el cambio a producción, y ponemos una alarma para la próxima semana para recordarnos de removerla, y quitar los permisos de accesso de Bob de nuestros servidores.

Nuestra prueba unitaria podría usar State como el contexto objetivo, de modo que podamos darle seguimiento a todos los nodos que detuvimos:

  type S = Set[MachineNode]
  val M: Machines.Ast ~> State[S, ?] = Mocker.stub[Unit] {
    case Machines.Stop(node) => State.modify[S](_ + node)
  }

  Machines
    .liftF[Machines.Ast]
    .stop(MachineNode("#c0ffee"))
    .foldMap(monkey)
    .foldMap(M)
    .exec(Set.empty)
    .shouldBe(Set.empty)

junto con una prueba de que los nodos “normales” no son afectados.

Una ventaja de usar Free para evitar detener los nodos #c0fee es que podemos estar seguros de atrapar todos los usos en lugar de tener que revisar la lógica del negocio y buscar todos los usos de .stop. Si nuestro contexto de la aplicación es simplemente IO podríamos, por supuesto, implementar esta lógica en la implementación de Machines[IO] pero una ventaja de usar Free es que no necesitamos tocar el código existente y podemos aislar y probar este comportamiento (temporal) sin seguir atado a las implementaciones de IO.

7.5.2 FreeApp (Applicative)

A pesar de que este capítulo se llama Mónadas avanzadas, el punto que debemos recordar es: no deberíamos usar mónadas a menos que realmente tengamos que usarlas. En esta sección, veremos porqué FreeAp (un aplicativo free), es preferible a las mónadas Free.

FreeAp se define como la representación de la estructura de datos de los métodos ap y pure de la typeclass Applicative:

  sealed abstract class FreeAp[S[_], A] {
    def hoist[G[_]](f: S ~> G): FreeAp[G,A] = ...
    def foldMap[G[_]: Applicative](f: S ~> G): G[A] = ...
    def monadic: Free[S, A] = ...
    def analyze[M:Monoid](f: F ~> λ[α => M]): M = ...
    ...
  }
  object FreeAp {
    implicit def applicative[S[_], A]: Applicative[FreeAp[S, A]] = ...

    private final case class Pure[S[_], A](a: A) extends FreeAp[S, A]
    private final case class Ap[S[_], A, B](
      value: () => S[B],
      function: () => FreeAp[S, B => A]
    ) extends FreeAp[S, A]

    def pure[S[_], A](a: A): FreeAp[S, A] = Pure(a)
    def lift[S[_], A](x: =>S[A]): FreeAp[S, A] = ...
    ...
  }

Los métodos .hoist y .foldMap son como los análogos de Free .mapSuspension y .foldMap.

Como una conveniencia, podemos generar una Free[S, A] a partir de nuestro FreeAp[S, A] con .monadic. Esto es de especial utilidad para optimizar subsistemas más pequeños y sin embargo usarlos como parte de un programa Free más grande.

Como Free, donde debemos crear una FreeAp para nuestras ASTs, más boilerplate…

  def liftA[F[_]](implicit I: Ast :<: F) = new Machines[FreeAp[F, ?]] {
    def getTime = FreeAp.lift(I.inj(GetTime()))
    ...
  }
7.5.2.1 Haciendo llamadas agrupadas a la red

Iniciamos este capítulo con afirmaciones grandes sobre rendimiento. Es tiempo de cumplir.

La versión humanizada de Philip Stark de los Números de latencia de Peter Norvig sirven de motivación para enfocarnos en la reducción de las llamadas a la red para optimisar una aplicación:

Computadora Escala de tiempo humana Analogía humana
Referencia a cache L1 0.5 segs Un latido del corazón
Error en la predicción de bifurcación 5 segs Bostezo
Referencia a cache L2 7 segs Bostezo largo
Mutex lock/unlock 25 segs Preparar una taza de té
Referencia a memoria principal 100 segs Cepillarse los dientes
Comprimir 1 KB con Zippy 50 min Pipeline de CI de scalac
Enviar 2 KB por una red de 1 Gbps 5.5 hr Tren de Londres a Edinburgo
Lectura aleatoria de SSD 1.7 días Fin de semana
Lectura sequencial de 1 MB de memoria 2.9 días Fin de semana largo
Viaje redondo dentro del mismo centro de datos 5.8 días Vacaciones largas de EE.UU.
Lectura sequencial de 1 MB de SSD 11.6 días Fiesta corta de la Union Europea
Búsqueda en disco 16.5 semanas Periodo de la universidad
Lectura sequencial de 1 MB de disco 7.8 meses Maternidad pagada completamente en Noruega
Envío de un paquete de CA->Holanda->CA 4.8 años Periodo de gobierno

Aunque Free y ``FreeAp` incurren en un costo extra de asignación dinámica de memoria, el equivalente a 100 segundos en la tabla humanizada, cada vez que convertimos dos invocaciones de red en una invocación agrupada, ahorramos casi 5 años.

Cuando estamos en un contexto Applicative, podemos optimizar con seguridad nuestra aplicación sin incumplir ninguna de las expectativas del programa original, y sin llenar desordenadamente nuestra lógica de negocios.

Felizmente, nuestra lógica de negocio únicamente requiere de un Applicative, recuerde

  final class DynAgentsModule[F[_]: Applicative](D: Drone[F], M: Machines[F])
      extends DynAgents[F] {
    def act(world: WorldView): F[WorldView] = ...
    ...
  }

Para empezar, crearemos el código repetitivo de lift para una álgebra nueva Batch

  trait Batch[F[_]] {
    def start(nodes: NonEmptyList[MachineNode]): F[Unit]
  }
  object Batch {
    sealed abstract class Ast[A]
    final case class Start(nodes: NonEmptyList[MachineNode]) extends Ast[Unit]

    def liftA[F[_]](implicit I: Ast :<: F) = new Batch[FreeAp[F, ?]] {
      def start(nodes: NonEmptyList[MachineNode]) = FreeAp.lift(I.inj(Start(nodes)))
    }
  }

y entonces crearemos una instancia de DynAgentsModule con FreeAp como el contexto

  type Orig[a] = Coproduct[Machines.Ast, Drone.Ast, a]

  val world: WorldView = ...
  val program = new DynAgentsModule(Drone.liftA[Orig], Machines.liftA[Orig])
  val freeap  = program.act(world)

En el capítulo 6, estudiamos el tipo de datos Const, que nos permite analizar un programa. No debería sorprendernos que FreeAp.analize esté implementado en términos de Const:

  sealed abstract class FreeAp[S[_], A] {
    ...
    def analyze[M: Monoid](f: S ~> λ[α => M]): M =
      foldMap(λ[S ~> Const[M, ?]](x => Const(f(x)))).getConst
  }

Proporcionamos una transformación natural para registrar todos los inicios de nodos y analizar (.analize) nuestro programa para obtener todos los nodos que necesitan iniciarse:

  val gather = λ[Orig ~> λ[α => IList[MachineNode]]] {
    case Coproduct(-\/(Machines.Start(node))) => IList.single(node)
    case _                                    => IList.empty
  }
  val gathered: IList[MachineNode] = freeap.analyze(gather)

El siguiente paso es extender el conjunto de instrucciones de Orig a Extended, lo que incluye el Batch.Ast y escribir un programa FreeApp que inicie todos nuestros nodos reunidos (gathered) en una única invocación a la red.

  type Extended[a] = Coproduct[Batch.Ast, Orig, a]
  def batch(nodes: IList[MachineNode]): FreeAp[Extended, Unit] =
    nodes.toNel match {
      case None        => FreeAp.pure(())
      case Some(nodes) => FreeAp.lift(Coproduct.leftc(Batch.Start(nodes)))
    }

También necesitamos remover todas las llamadas a Machines.Start, lo cual podemos hacer con una transformación natural

  val nostart = λ[Orig ~> FreeAp[Extended, ?]] {
    case Coproduct(-\/(Machines.Start(_))) => FreeAp.pure(())
    case other                             => FreeAp.lift(Coproduct.rightc(other))
  }

Ahora tenemos dos programas, y necesitamos combinarlos. Recuerde la sintaxis *> de Apply

  val patched = batch(gathered) *> freeap.foldMap(nostart)

Poniéndolo todo junto en un mismo método:

  def optimise[A](orig: FreeAp[Orig, A]): FreeAp[Extended, A] =
    (batch(orig.analyze(gather)) *> orig.foldMap(nostart))

¡Eso es todo! Llamamos .optimise en cada invocación de act en nuestro ciclo principal, lo cual es simplemente una cuestión mecánica.

7.5.3 Coyoneda (Functor)

Nombrada en honor al matemático Nobuo Yoneda, podemos generar de manera gratuita una estructura de datos Functor para cualquier álgebra S[_]

  sealed abstract class Coyoneda[S[_], A] {
    def run(implicit S: Functor[S]): S[A] = ...
    def trans[G[_]](f: F ~> G): Coyoneda[G, A] = ...
    ...
  }
  object Coyoneda {
    implicit def functor[S[_], A]: Functor[Coyoneda[S, A]] = ...

    private final case class Map[F[_], A, B](fa: F[A], f: A => B) extends Coyoneda[F, A]
    def apply[S[_], A, B](sa: S[A])(f: A => B) = Map[S, A, B](sa, f)
    def lift[S[_], A](sa: S[A]) = Map[S, A, A](sa, identity)
    ...
  }

y también existe una versión contravariante

  sealed abstract class ContravariantCoyoneda[S[_], A] {
    def run(implicit S: Contravariant[S]): S[A] = ...
    def trans[G[_]](f: F ~> G): ContravariantCoyoneda[G, A] = ...
    ...
  }
  object ContravariantCoyoneda {
    implicit def contravariant[S[_], A]: Contravariant[ContravariantCoyoneda[S, A]] = ...

    private final case class Contramap[F[_], A, B](fa: F[A], f: B => A)
      extends ContravariantCoyoneda[F, A]
    def apply[S[_], A, B](sa: S[A])(f: B => A) = Contramap[S, A, B](sa, f)
    def lift[S[_], A](sa: S[A]) = Contramap[S, A, A](sa, identity)
    ...
  }

La API es algo más simple que Free y FreeAp, permitiendo una transformación natural con .trans y un .run (tomando un Functor o Contravariant, respectivamente) para escapar de la estructura libre.

Coyo y cocoyo pueden ser de utilidad si deseamos realizar un .map o .contramap sobre un tipo, y sabemos que podemos convertir en un tipo de datos que tiene un Functor pero no deseamos comprometernos demasiado temprano con la estructura de datos final. Por ejemplo, podemos crear un Coyoneda[ISet, ?] (recuerde que ISet no tiene un Functor) para usar métodos que requieren un Functor, y después convertir en una IList.

Si deseamos optimizar un programa con coyo o cocoyo tenemos que proporcionar el boilerplate esperada para cada álgebra:

  def liftCoyo[F[_]](implicit I: Ast :<: F) = new Machines[Coyoneda[F, ?]] {
    def getTime = Coyoneda.lift(I.inj(GetTime()))
    ...
  }
  def liftCocoyo[F[_]](implicit I: Ast :<: F) = new Machines[ContravariantCoyoneda[F, ?]] {
    def getTime = ContravariantCoyoneda.lift(I.inj(GetTime()))
    ...
  }

Una optimización que obtenemos al usar Coyoneda es la fusión de mapeos (y fusión de contramapeos), lo que nos permite reescribir

  xs.map(a).map(b).map(c)

en

  xs.map(x => c(b(a(x))))

evitando representaciones intermedias. Por ejemplo, si xs es una List de mil elementos, evitamos la creación de dos mil objetos porque sólo realizamos un mapeo sobre la estructura de datos una sola vez.

Sin embargo, es mucho más fácil realizar este cambio en la función de manera manual, o esperar a que el proyecto scalaz-plugin sea liberado y que realice automáticamente este tipo de optimizaciones.

7.5.4 Efectos extensibles

Los programas son simplemente datos: las estructuras libres hacen esto explícito y nos dan la habilidad de reordenar y optimizar estos datos.

Free es más especial de lo que aparente: puede secuenciar álgebras y typeclasses arbitrarias.

Por ejemplo, está disponible una estructura libre para MonadState. Ast y .liftF son más complicadas de lo usual debido a que tenemos que tomar en cuenta el parámetro de tipo en MonadState, y la herencia a partir de Monad:

  object MonadState {
    sealed abstract class Ast[S, A]
    final case class Get[S]()     extends Ast[S, S]
    final case class Put[S](s: S) extends Ast[S, Unit]

    def liftF[F[_], S](implicit I: Ast[S, ?] :<: F) =
      new MonadState[Free[F, ?], S] with BindRec[Free[F, ?]] {
        def get       = Free.liftF(I.inj(Get[S]()))
        def put(s: S) = Free.liftF(I.inj(Put[S](s)))

        val delegate         = Free.freeMonad[F]
        def point[A](a: =>A) = delegate.point(a)
        ...
      }
    ...
  }

Esto nos brinda la oportunidad de usar intérpretes optimizados. Por ejemplo, podríamos almacenar la S en un campo atómico en lugar de construir un trampolin StateT anidado.

¡Podríamos crear una Ast y un .liftF para casi cualquier álgebra o typeclass! La única restricción es que la F[_] no aparezca como un parámetro de alguna de las instrucciones, es decir, debe ser posible que el álgebra tenga una instancia de Functor. Tristemente, esto descarta a MonadError y Monoid.

A medida que el AST de un programa libre crece, el rendimiento se degrada debido a que el intérprete debe realizar un match sobre el conjunto de instrucciones con un costo O(n). Una alternativa a scalaz.Coproduct es la codificación de iotaz, que usa una estructura de datos optimizada para realizar un despacho dinámico de costo O(1) (usando enteros que son asignados a cada coproducto en tiempo de compilación).

Por razones históricas una AST libre para un álgebra o typeclass se conoce como Codificación Inicial, y una implementación directa (por ejemplo, con IO) se conoce como Sin Etiqueta Final. Aunque hemos explorado ideas interesantes con Free, generalmente se acepta que Sin Etiqueta Final es superior. Pero para poder usar el estilo de Sin Etiqueta Final, requerimos de un tipo para efectos de alto rendimiento que proporcione todas las typeclasses que hemos cubierto en este capítulo. También necesitaríamos ser capaces de ejecutar nuestro código Applicative en paralelo. Esto es exactamente lo que estudiaremos a continuación.

7.6 Parallel

Existen dos operaciones con efectos que casi siempre querremos ejecutar en paralelo:

  1. Realizar un .map sobre una colección de efectos, devolviendo un único efecto. Esto se consigue por medio de .traverse, que delega al método .apply2 del efecto.
  2. Ejecutar un número fijo de efectos con el operador de grito |@|, y combinar sus salidas, de nuevo delegando al método .apply2.

Sin embargo, en la práctica, ninguna de estas operaciones se ejecutan en paralelo por default. La razón es que si nuestra F[_] se implementa por una Monad, entonces las leyes derivadas para los combinadores .apply2 deben satisfacerse, las cuales dicen:

  @typeclass trait Bind[F[_]] extends Apply[F] {
    ...
    override def apply2[A, B, C](fa: =>F[A], fb: =>F[B])(f: (A, B) => C): F[C] =
      bind(fa)(a => map(fb)(b => f(a, b)))
    ...
  }

En otras palabras, Monad está explícitamente prohibido para ejecutar efectos en paralelo.

Sin embargo, si tenemos una F[_] que no sea monádica, entonces puede implementar .apply2 en paralelo. Podemos usar el mecanismo de (etiqueta) @@ para crear una instancia de Applicative para F[_] @@ Parallel, que de manera conveniente se le asigna el alias de tipo Applicative.Par

  object Applicative {
    type Par[F[_]] = Applicative[λ[α => F[α] @@ Tags.Parallel]]
    ...
  }

Los programas monádicos pueden entonces demandar un Par implícito además de su Monad

  def foo[F[_]: Monad: Applicative.Par]: F[Unit] = ...

La sintaxis de Traverse de Scalaz soporta paralelismo:

  implicit class TraverseSyntax[F[_], A](self: F[A]) {
    ...
    def parTraverse[G[_], B](f: A => G[B])(
      implicit F: Traverse[F], G: Applicative.Par[G]
    ): G[F[B]] = Tag.unwrap(F.traverse(self)(a => Tag(f(a))))
  }

Si el implícito Applicative.Par[IO] está dentro del alcance, podemos escoger entre recorrer de manera secuencial o paralela:

  val input: IList[String] = ...
  def network(in: String): IO[Int] = ...

  input.traverse(network): IO[IList[Int]] // one at a time
  input.parTraverse(network): IO[IList[Int]] // all in parallel

De manera similar, podemos invocar .parApply o .parTupled después de usar los operadores de grito

  val fa: IO[String] = ...
  val fb: IO[String] = ...
  val fc: IO[String] = ...

  (fa |@| fb).parTupled: IO[(String, String)]

  (fa |@| fb |@| fc).parApply { case (a, b, c) => a + b + c }: IO[String]

Es digno de mención que cuando tenemos programas Applicative, tales como

  def foo[F[_]: Applicative]: F[Unit] = ...

podemos usar F[A] @@ Parallel como nuestro contexto del programa y obtener paralelismo como el default en .traverse y |@|. La conversión entre las versiones normal y @@ Parallel de F[_] debe hacerse manualmente en el código, lo que puede resultar doloroso. Por lo tanto, con frecuencia es más simple demandar ambas formas de Applicative

  def foo[F[_]: Applicative: Applicative.Par]: F[Unit] = ...

7.6.1 Rompiendo la ley

Podríamos tomar un enfoque aún más audaz para el paralelismo: salirnos de la ley que demanda que .apply2 sea secuencial para Monad. Esto es altamente controversial, pero funciona bien para la mayoría de las aplicaciones del mundo real. Es necesario auditar nuestro código (incluyendo las dependencias de terceros) para asegurarnos de que nada está haciendo uso de la ley implicada de .apply2.

Envolvemos IO

  final class MyIO[A](val io: IO[A]) extends AnyVal

y proporcionamos nuestra propia implementación de Monad que ejecute .apply2 en paralelo al delegar a la instancia @@ Parallel

  object MyIO {
    implicit val monad: Monad[MyIO] = new Monad[MyIO] {
      override def apply2[A, B, C](fa: MyIO[A], fb: MyIO[B])(f: (A, B) => C): MyIO[C] =
        Applicative[IO.Par].apply2(fa.io, fb.io)(f)
      ...
    }
  }

Ahora es posible usar MyIO como nuestro contexto de la aplicación en lugar de IO, y obtener paralelismo por default.

Para ser exhaustivos: una implementación ingenua e ineficiente de Applicative.Par para nuestro IO de juguete podría usar Future:

  object IO {
    ...
    type Par[a] = IO[a] @@ Parallel
    implicit val ParApplicative = new Applicative[Par] {
      override def apply2[A, B, C](fa: =>Par[A], fb: =>Par[B])(f: (A, B) => C): Par[C] =
        Tag(
          IO {
            val forked = Future { Tag.unwrap(fa).interpret() }
            val b      = Tag.unwrap(fb).interpret()
            val a      = Await.result(forked, Duration.Inf)
            f(a, b)
          }
        )
  }

y debido a un error en el compilador de Scala que trata todas las instancias de @@ como huérfanas, debemos importar de manera explícita el implícito:

  import IO.ParApplicative

En la sección final de este capítulo veremos cómo está implementado IO en realidad.

7.7 IO

IO de Scalaz es la construcción de programación asíncrona más rápida del ecosistema de Scala: hasta 50 veces más rápida que Future. IO es una estructura de datos libre especializada para usarse como una mónada de efectos generalizados.

  sealed abstract class IO[E, A] { ... }
  object IO {
    private final class FlatMap         ... extends IO[E, A]
    private final class Point           ... extends IO[E, A]
    private final class Strict          ... extends IO[E, A]
    private final class SyncEffect      ... extends IO[E, A]
    private final class Fail            ... extends IO[E, A]
    private final class AsyncEffect     ... extends IO[E, A]
    ...
  }

IO tiene dos parámetros de tipo: tiene un Bifunctor que permite que el tipo de error sea un ADT específico de la aplicación. Pero debido a que estamos trabajando sobre la JVM, y debemos interactuar con librerías antiguas, se proporciona un type alias conveniente que usa excepciones para el tipo de error:

  type Task[A] = IO[Throwable, A]

7.7.1 Creación

Existen múltiples formas de crear una IO que cubra una variedad de bloques de código que cubran situaciones para bloques de código estrictos, perezosos (lazy), seguros e inseguros:

  object IO {
    // eager evaluation of an existing value
    def now[E, A](a: A): IO[E, A] = ...
    // lazy evaluation of a pure calculation
    def point[E, A](a: =>A): IO[E, A] = ...
    // lazy evaluation of a side-effecting, yet Total, code block
    def sync[E, A](effect: =>A): IO[E, A] = ...
    // lazy evaluation of a side-effecting code block that may fail
    def syncThrowable[A](effect: =>A): IO[Throwable, A] = ...

    // create a failed IO
    def fail[E, A](error: E): IO[E, A] = ...
    // asynchronously sleeps for a specific period of time
    def sleep[E](duration: Duration): IO[E, Unit] = ...
    ...
  }

con constructores convenientes para Task:

  object Task {
    def apply[A](effect: =>A): Task[A] = IO.syncThrowable(effect)
    def now[A](effect: A): Task[A] = IO.now(effect)
    def fail[A](error: Throwable): Task[A] = IO.fail(error)
    def fromFuture[E, A](io: Task[Future[A]])(ec: ExecutionContext): Task[A] = ...
  }

Los constructores más comunes, por mucho, cuando es necesario lidiar con código antiguo, son Task.apply y Task.fromFuture:

  val fa: Task[Future[String]] = Task { ... impure code here ... }

  Task.fromFuture(fa)(ExecutionContext.global): Task[String]

No podemos pasar Future (sin envolverlos), porque se evalúan de manera estricta, de modo que siempre es necesario construirlos dentro de un bloque seguro.

Noten que el ExecutionContext no es implícito, contrario a la convención. Recuerde que en Scalaz reservamos la palabra implicit para la derivación de typeclasses, para simplificar el lenguaje: ExecutionContext es una configuración que debe proporcionarse de manera explícita.

7.7.2 Ejecución

El intérprete IO se llama RTS, para el sistema de ejecución. Su implementación está más allá del alcance de este libro. Nos enfocaremos en las características que proporciona IO.

IO es simplemente una estructura de datos, y es interpretada al final del mundo al extender SafeApp e implementando .run

 trait SafeApp extends RTS {

    sealed trait ExitStatus
    object ExitStatus {
      case class ExitNow(code: Int)                         extends ExitStatus
      case class ExitWhenDone(code: Int, timeout: Duration) extends ExitStatus
      case object DoNotExit                                 extends ExitStatus
    }

    def run(args: List[String]): IO[Void, ExitStatus]

    final def main(args0: Array[String]): Unit = ... calls run ...
  }

Si estamos integrando con un sistema antiguo y no estamos en control del punto de entrada de nuestra aplicación, podemos extender el RTS y ganar acceso a métodos inseguros para evaluar el IO en el punto de entrada de nuestro código con principios que usa PF.

7.7.3 Características

IO proporciona instancias de typeclasses para Bifunctor, MonadError[E, ?], BindRec, Plus, MonadPlus (si E forma un Monoid), y un Applicative[IO.Par[E, ?]].

En adición a la funcionalidad que viene a partir de las typeclasses, hay métodos de implementación específicos:

  sealed abstract class IO[E, A] {
    // retries an action N times, until success
    def retryN(n: Int): IO[E, A] = ...
    // ... with exponential backoff
    def retryBackoff(n: Int, factor: Double, duration: Duration): IO[E, A] = ...

    // repeats an action with a pause between invocations, until it fails
    def repeat[B](interval: Duration): IO[E, B] = ...

    // cancel the action if it does not complete within the timeframe
    def timeout(duration: Duration): IO[E, Maybe[A]] = ...

    // runs `release` on success or failure.
    // Note that IO[Void, Unit] cannot fail.
    def bracket[B](release: A => IO[Void, Unit])(use: A => IO[E, B]): IO[E, B] = ...
    // alternative syntax for bracket
    def ensuring(finalizer: IO[Void, Unit]): IO[E, A] =
    // ignore failure and success, e.g. to ignore the result of a cleanup action
    def ignore: IO[Void, Unit] = ...

    // runs two effects in parallel
    def par[B](that: IO[E, B]): IO[E, (A, B)] = ...
    ...

Es posible que un IO tenga un estado terminado, que representa trabajo que al final será descartado (no es un error ni un éxito). Las utilidades relacionadas a la terminación son:

  ...
    // terminate whatever actions are running with the given throwable.
    // bracket / ensuring is honoured.
    def terminate[E, A](t: Throwable): IO[E, A] = ...

    // runs two effects in parallel, return the winner and terminate the loser
    def race(that: IO[E, A]): IO[E, A] = ...

    // ignores terminations
    def uninterruptibly: IO[E, A] = ...
  ...

7.7.4 Fiber

Una IO puede engendrar fibers, una abstracción liviana sobre un Thread de la JVM. Podemos invocar .fork sobre una instancia de IO, y .supervise sobre cualquier fibra incompleta para asegurar que son terminadas cuando se completa la acción IO.

  ...
    def fork[E2]: IO[E2, Fiber[E, A]] = ...
    def supervised(error: Throwable): IO[E, A] = ...
  ...

Cuando tenemos una Fiber podemos invocar .join para regresar a la IO, o interrumpt el trabajo subyacente.

  trait Fiber[E, A] {
    def join: IO[E, A]
    def interrupt[E2](t: Throwable): IO[E2, Unit]
  }

Podemos usar fibras para lograr una forma de control de concurrencia optimista. Considere el caso donde tenemos data que requerimos analizar, pero también tenemos que validarlo. Podemos iniciar el análisis de manera optimista y cancelar el trabajo si la validación falla, lo que se realiza en paralelo.

  final class BadData(data: Data) extends Throwable with NoStackTrace

  for {
    fiber1   <- analysis(data).fork
    fiber2   <- validate(data).fork
    valid    <- fiber2.join
    _        <- if (!valid) fiber1.interrupt(BadData(data))
                else IO.unit
    result   <- fiber1.join
  } yield result

Otro caso de uso para las fibras es cuando necesitamos realizar una acción de dispara y olvida. Por ejemplo, un logging de baja prioridad sobre la red.

  final class Promise[E, A] private (ref: AtomicReference[State[E, A]]) {
    def complete[E2](a: A): IO[E2, Boolean] = ...
    def error[E2](e: E): IO[E2, Boolean] = ...
    def get: IO[E, A] = ...

    // interrupts all listeners
    def interrupt[E2](t: Throwable): IO[E2, Boolean] = ...
  }
  object Promise {
    def make[E, A]: IO[E, Promise[E, A]] = ...
  }

Promise no es algo que típicamente se use en código de aplicación. Es un bloque de construcción para frameworks de concurrencia de alto nivel.

7.7.5 IORef

IORef es el equivalente de IO para una variable mutable atómica.

Podemos leer la variable y tenemos una variedad de maneras de escribirla o actualizarla.

  final class IORef[A] private (ref: AtomicReference[A]) {
    def read[E]: IO[E, A] = ...

    // write with immediate consistency guarantees
    def write[E](a: A): IO[E, Unit] = ...
    // write with eventual consistency guarantees
    def writeLater[E](a: A): IO[E, Unit] = ...
    // return true if an immediate write succeeded, false if not (and abort)
    def tryWrite[E](a: A): IO[E, Boolean] = ...

    // atomic primitives for updating the value
    def compareAndSet[E](prev: A, next: A): IO[E, Boolean] = ...
    def modify[E](f: A => A): IO[E, A] = ...
    def modifyFold[E, B](f: A => (B, A)): IO[E, B] = ...
  }
  object IORef {
    def apply[E, A](a: A): IO[E, IORef[A]] = ...
  }

IORef es otro bloque de construcción y puede ser usado para proporcionar un MonadState de alto rendimiento. Por ejemplo, para crear un newtype especializado a Task

  final class StateTask[A](val io: Task[A]) extends AnyVal
  object StateTask {
    def create[S](initial: S): Task[MonadState[StateTask, S]] =
      for {
        ref <- IORef(initial)
      } yield
        new MonadState[StateTask, S] {
          override def get       = new StateTask(ref.read)
          override def put(s: S) = new StateTask(ref.write(s))
          ...
        }
  }

Podemos hacer uso de esta implementación optimizada de MonadState en un SafeApp, donde nuestro .program depende de typeclasses optimizadas MTL:

  object FastState extends SafeApp {
    def program[F[_]](implicit F: MonadState[F, Int]): F[ExitStatus] = ...

    def run(@unused args: List[String]): IO[Void, ExitStatus] =
      for {
        stateMonad <- StateTask.create(10)
        output     <- program(stateMonad).io
      } yield output
  }

Una aplicación más realista tomaría una variedad de álgebras y de typeclasses como entrada.

7.7.5.1 MonadIO

El MonadIO que estudiamos previamente estaba simplificado para esconder el parámetro de tipo E. La typeclass real es

  trait MonadIO[M[_], E] {
    def liftIO[A](io: IO[E, A])(implicit M: Monad[M]): M[A]
  }

con un cambio menor en el código repetitivo del objeto compañero de nuestra álgebra, tomando en cuenta la E extra:

  trait Lookup[F[_]] {
    def look: F[Int]
  }
  object Lookup {
    def liftIO[F[_]: Monad, E](io: Lookup[IO[E, ?]])(implicit M: MonadIO[F, E]) =
      new Lookup[F] {
        def look: F[Int] = M.liftIO(io.look)
      }
    ...
  }

7.8 Resumen

  1. El Future está descompuesto, no vaya allá.
  2. Administre la seguridad de la pila con un Trampoline.
  3. La Librería de Transformadores de Mónadas (MTL) abstrae sobre efectos comunes de typeclasses.
  4. Los Transformadores de Mónadas proporcionan implementaciones por defecto de la MTL.
  5. Las estructuras de datos Free nos permiten analizar, optimizar y probar fácilmente nuestros programas.
  6. IO nos da la habilidad de implementar álgebras como efectos sobre el mundo.
  7. IO puede ejecutar efectos en paralelo y es una columna vertebral para cualquier aplicación.

8. Derivación de typeclasses

Las typeclasses proporcionan funcionalidad polimórfica a nuestras aplicaciones. Pero para usar una typeclass necesitamos instancias para nuestros objetos del dominio de negocios.

La creación de una instancia de typeclass a partir de instancias existentes se conoce como derivación de typeclasses y es el tema de este capítulo.

Existen cuatro enfoques para la derivación de typeclasses:

  1. Instancias manuales para cada objeto del dominio. Esto no es factible para aplicaciones del mundo real porque requiere cientos de lineas de código par cada línea de una case class. Es útil sólo para propósitos educacionales y optimización de rendimiento a la medida.
  2. Abstraer sobre la typeclass por una typeclass de Scalaz existente. Este es el enfoque usado por scalaz-deriving, produciendo pruebas automáticas y derivaciones para productos y coproductos.
  3. Macros. Sin embargo, la escritura de macros para cada typeclass requiere de un desarrollador avanzado y experimentado. Felizmente, la librería Magnolia de Jon Pretty abstrae sobre macros escritas a mano con una API simple, centralizando la interacción compleja con el compilador.
  4. Escribir un programa genérico usando la librería Shapeless. El mecanismo implicit es un lenguaje dentro del lenguaje Scala y puede ser usado para escribir programas a nivel de tipo.

En este capítulo estudiaremos typeclasses con complejidad creciente y sus derivaciones. Empezaremos con scalaz-deriving como el mecanismo con más principios, repitiendo algunas lecciones del Capítulo 5 “Typeclasses de Scalaz”, y después con Magnolia (el más fácil de usar), terminando con Shapeless (el más poderoso) para las typeclasses con lógica de derivación compleja.

8.1 Ejemplos

Este capítulo mostrará cómo definir derivaciones para cinco typeclasses específicas. Cada ejemplo exhibe una característica que puede ser generalizada:

  @typeclass trait Equal[A]  {
    // type parameter is in contravariant (parameter) position
    @op("===") def equal(a1: A, a2: A): Boolean
  }

  // for requesting default values of a type when testing
  @typeclass trait Default[A] {
    // type parameter is in covariant (return) position
    def default: String \/ A
  }

  @typeclass trait Semigroup[A] {
    // type parameter is in both covariant and contravariant position (invariant)
    @op("|+|") def append(x: A, y: =>A): A
  }

  @typeclass trait JsEncoder[T] {
    // type parameter is in contravariant position and needs access to field names
    def toJson(t: T): JsValue
  }

  @typeclass trait JsDecoder[T] {
    // type parameter is in covariant position and needs access to field names
    def fromJson(j: JsValue): String \/ T
  }

8.2 scalaz-deriving

La librería scalaz-deriving es una extensión de Scalaz y puede agregarse al build.sbt con

  val derivingVersion = "1.0.0"
  libraryDependencies += "org.scalaz" %% "scalaz-deriving" % derivingVersion

proporcionando nuevas typeclasses, mostradas abajo con relación a las typeclasses de Scalaz:

Antes de proceder, aquí tenemos una rápida recapitulación de las typeclasses centrales de Scalaz:

  @typeclass trait InvariantFunctor[F[_]] {
    def xmap[A, B](fa: F[A], f: A => B, g: B => A): F[B]
  }

  @typeclass trait Contravariant[F[_]] extends InvariantFunctor[F] {
    def contramap[A, B](fa: F[A])(f: B => A): F[B]
    def xmap[A, B](fa: F[A], f: A => B, g: B => A): F[B] = contramap(fa)(g)
  }

  @typeclass trait Divisible[F[_]] extends Contravariant[F] {
    def conquer[A]: F[A]
    def divide2[A, B, C](fa: F[A], fb: F[B])(f: C => (A, B)): F[C]
    ...
    def divide22[...] = ...
  }

  @typeclass trait Functor[F[_]] extends InvariantFunctor[F] {
    def map[A, B](fa: F[A])(f: A => B): F[B]
    def xmap[A, B](fa: F[A], f: A => B, g: B => A): F[B] = map(fa)(f)
  }

  @typeclass trait Applicative[F[_]] extends Functor[F] {
    def point[A](a: =>A): F[A]
    def apply2[A,B,C](fa: =>F[A], fb: =>F[B])(f: (A, B) => C): F[C] = ...
    ...
    def apply12[...]
  }

  @typeclass trait Monad[F[_]] extends Functor[F] {
    @op(">>=") def bind[A, B](fa: F[A])(f: A => F[B]): F[B]
  }
  @typeclass trait MonadError[F[_], E] extends Monad[F] {
    def raiseError[A](e: E): F[A]
    def emap[A, B](fa: F[A])(f: A => E \/ B): F[B] = ...
    ...
  }

8.2.1 No repita (DRY)

La forma más simple de derivar una typeclass es reutilizar una que ya exista:

La typeclass Equal tiene una instancia de Contravariant[Equal], proporcionando .contramap:

  object Equal {
    implicit val contravariant = new Contravariant[Equal] {
      def contramap[A, B](fa: Equal[A])(f: B => A): Equal[B] =
        (b1, b2) => fa.equal(f(b1), f(b2))
    }
    ...
  }

Como usuarios de Equal, podemos usar .contramap para nuestros parámetros de tipo únicos. Recuerde que las instancias de typeclasses van en los compañeros de tipos de datos para que estén en el alcance implícito:

  final case class Foo(s: String)
  object Foo {
    implicit val equal: Equal[Foo] = Equal[String].contramap(_.s)
  }

  scala> Foo("hello") === Foo("world")
  false

Sin embargo, no todas las typeclasses tienen una instancia de Contravariant. En particular, las typeclasses con parámetros de tipo en posición covariante podrían más bien tener un Functor.

  object Default {
    def instance[A](d: =>String \/ A) = new Default[A] { def default = d }
    implicit val string: Default[String] = instance("".right)

    implicit val functor: Functor[Default] = new Functor[Default] {
      def map[A, B](fa: Default[A])(f: A => B): Default[B] = instance(fa.default.map(f))
    }
    ...
  }

Ahora podríamos derivar un Default[Foo]

  object Foo {
    implicit val default: Default[Foo] = Default[String].map(Foo(_))
    ...
  }

Si una typeclass tiene parámetros en posiciones tanto covariantes como contravariantes, como es el caso con Semigroup, podría proporcionar un InvariantFunctor

  object Semigroup {
    implicit val invariant = new InvariantFunctor[Semigroup] {
      def xmap[A, B](ma: Semigroup[A], f: A => B, g: B => A) = new Semigroup[B] {
        def append(x: B, y: =>B): B = f(ma.append(g(x), g(y)))
      }
    }
    ...
  }

y podemos invocar .xmap

  object Foo {
    implicit val semigroup: Semigroup[Foo] = Semigroup[String].xmap(Foo(_), _.s)
    ...
  }

Generalmente, es más simple usar .xmap en lugar de .map o .contramap:

  final case class Foo(s: String)
  object Foo {
    implicit val equal: Equal[Foo]         = Equal[String].xmap(Foo(_), _.s)
    implicit val default: Default[Foo]     = Default[String].xmap(Foo(_), _.s)
    implicit val semigroup: Semigroup[Foo] = Semigroup[String].xmap(Foo(_), _.s)
  }

8.2.2 MonadError

Típicamente, las cosas que escriben a partir de un valor polimórfico tienen una instancia de Contravariant, y las cosas que leen a un valor polimórfico tienen un Functor. Sin embargo, es bastante posible que la lectura pueda fallar. Por ejemplo, si tenemos una String por defecto no significa que podamos derivar una String Refined NonEmpty a partir de esta

  import eu.timepit.refined.refineV
  import eu.timepit.refined.api._
  import eu.timepit.refined.collection._

  implicit val nes: Default[String Refined NonEmpty] =
    Default[String].map(refineV[NonEmpty](_))

falla al compilar con

  [error] default.scala:41:32: polymorphic expression cannot be instantiated to expected type;
  [error]  found   : Either[String, String Refined NonEmpty]
  [error]  required: String Refined NonEmpty
  [error]     Default[String].map(refineV[NonEmpty](_))
  [error]                                          ^

Recuerde del Capítulo 4.1 que refineV devuelve un Either, como nos ha recordado el compilador.

Como el autor de la typeclass de Default, podríamos lograr algo mejor que Functor y proporcionar un MonadError[Default, String]:

  implicit val monad = new MonadError[Default, String] {
    def point[A](a: =>A): Default[A] =
      instance(a.right)
    def bind[A, B](fa: Default[A])(f: A => Default[B]): Default[B] =
      instance((fa >>= f).default)
    def handleError[A](fa: Default[A])(f: String => Default[A]): Default[A] =
      instance(fa.default.handleError(e => f(e).default))
    def raiseError[A](e: String): Default[A] =
      instance(e.left)
  }

Ahora tenemos acceso a la sintaxis .emap y podemos derivar nuestro tipo refinado

  implicit val nes: Default[String Refined NonEmpty] =
    Default[String].emap(refineV[NonEmpty](_).disjunction)

De hecho, podemos proporcionar una regla de derivación para todos los tipos refinados

  implicit def refined[A: Default, P](
    implicit V: Validate[A, P]
  ): Default[A Refined P] = Default[A].emap(refineV[P](_).disjunction)

donde Validate es de la librería refined y es requerido por refineV.

De manera similar podemos usar .emap para derivar un decodificador Int a partir de un Long, con protección alrededor del método no total .toInt de la librería estándar.

  implicit val long: Default[Long] = instance(0L.right)
  implicit val int: Default[Int] = Default[Long].emap {
    case n if (Int.MinValue <= n && n <= Int.MaxValue) => n.toInt.right
    case big => s"$big does not fit into 32 bits".left
  }

Como autores de la typeclass Default, podríamos reconsiderar nuestro diseño de la API de modo que nunca pueda fallar, por ejemplo con la siguiente firma de tipo

  @typeclass trait Default[A] {
    def default: A
  }

No seríamos capaces de definir un MonadError, forzándonos a proporcionar instancias que nunca fallen. Esto resultará en más código repetitivo pero ganaremos en seguridad en tiempo de compilación. Sin embargo, continuaremos con String \/ A como el retorno de tipo dado que es un ejemplo más general.

8.2.3 .fromIso

Todas las typeclasses en Scalaz tienen un método en su objeto compañero con una firma similar a la siguiente:

  object Equal {
    def fromIso[F, G: Equal](D: F <=> G): Equal[F] = ...
    ...
  }

  object Monad {
    def fromIso[F[_], G[_]: Monad](D: F <~> G): Monad[F] = ...
    ...
  }

Estas significan que si tenemos un tipo F, y una forma de convertirlo en una G que tenga una instancia, entonces podemos llamar Equal.fromIso para tener una instancia para F.

Por ejemplo, como usuarios de la typeclass, si tenemos un tipo de datos Bar podemos definir un isomorfismo a (String, Int)

  import Isomorphism._

  final case class Bar(s: String, i: Int)
  object Bar {
    val iso: Bar <=> (String, Int) = IsoSet(b => (b.s, b.i), t => Bar(t._1, t._2))
  }

y entonces derivar Equal[Bar] porque ya existe un Equal para todas las tuplas:

  object Bar {
    ...
    implicit val equal: Equal[Bar] = Equal.fromIso(iso)
  }

El mecanismo .fromIso también puede ayudarnos como autores de typeclasses. Considere Default que tiene una firma de tipo Unit => F[A]. Nuestro método default es de hecho isomórfico a Kleisli[F, Unit, A], el transformador de mónadas ReaderT.

Dado que Kleisli ya proporciona un MonadError (si F tiene una), podemos derivar MonadError[Default, String] al crear un isomorfismo entre Default y Kleisli:

  private type Sig[a] = Unit => String \/ a
  private val iso = Kleisli.iso(
    λ[Sig ~> Default](s => instance(s(()))),
    λ[Default ~> Sig](d => _ => d.default)
  )
  implicit val monad: MonadError[Default, String] = MonadError.fromIso(iso)

proporcionándonos .map, .xmap y .emap que hemos estado usando hasta el momento, efectivamente de manera gratuita.

8.2.4 Divisible y Applicative

Para derivar Equal para nuestra case class con dos parámetros, reutilizamos la instancia que Scalaz proporcionó para las tuplas. Pero, ¿de dónde vino la instancia para las tuplas?

Una typeclass más específica que Contravariant es Divisible. Equal tiene una instancia:

  implicit val divisible = new Divisible[Equal] {
    ...
    def divide[A1, A2, Z](a1: =>Equal[A1], a2: =>Equal[A2])(
      f: Z => (A1, A2)
    ): Equal[Z] = { (z1, z2) =>
      val (s1, s2) = f(z1)
      val (t1, t2) = f(z2)
      a1.equal(s1, t1) && a2.equal(s2, t2)
    }
    def conquer[A]: Equal[A] = (_, _) => true
  }

Y a partir de divide2, Divisible es capaz de construir derivaciones hasta divide22. Podemos llamar a estos métodos directamente para nuestros tipos de datos:

  final case class Bar(s: String, i: Int)
  object Bar {
    implicit val equal: Equal[Bar] =
      Divisible[Equal].divide2(Equal[String], Equal[Int])(b => (b.s, b.i))
  }

El equivalente para los parámetros de tipo en posición covariante es Applicative:

  object Bar {
    ...
    implicit val default: Default[Bar] =
      Applicative[Default].apply2(Default[String], Default[Int])(Bar(_, _))
  }

Pero debemos ser cuidadosos para no violar las leyes de las typeclasses cuando implementemos Divisible o Applicative. En particular, es fácil violar la ley de composición que dice que los siguientes dos caminos de código deben resultar exactamente en la misma salida:

  • divide2(divide2(a1, a2)(dupe), a3)(dupe)
  • divide2(a1, divide2(a2, a3)(dupe))(dupe)
  • para cualquier dupe: A => (A, A)

con leyes similares para Applicative.

Considere JsEncoder y la instancia propuesta para Divisible

  new Divisible[JsEncoder] {
    ...
    def divide[A, B, C](fa: JsEncoder[A], fb: JsEncoder[B])(
      f: C => (A, B)
    ): JsEncoder[C] = { c =>
      val (a, b) = f(c)
      JsArray(IList(fa.toJson(a), fb.toJson(b)))
    }

    def conquer[A]: JsEncoder[A] = _ => JsNull
  }

De un lado de las leyes de composición, para una entrada de tipo String, tenemos

  JsArray([JsArray([JsString(hello),JsString(hello)]),JsString(hello)])

y por el otro

  JsArray([JsString(hello),JsArray([JsString(hello),JsString(hello)])])

que son diferentes. Podríamos experimentar con variaciones de la implementación divide, pero nunca satisfará las leyes para todas las entradas.

Por lo tanto no podemos proporcionar un Divisible[JsEncoder] porque violaría las leyes matemáticas e invalidaría todas las suposiciones de las que dependen los usuarios de Divisible.

Para ayudar en la prueba de leyes, las typeclasses de Scalaz contienen versiones codificadas de sus leyes en la typeclass misma. Podemos escribir una prueba automática, verificando que la ley falla, para recordarnos de este hecho:

  val D: Divisible[JsEncoder] = ...
  val S: JsEncoder[String] = JsEncoder[String]
  val E: Equal[JsEncoder[String]] = (p1, p2) => p1.toJson("hello") === p2.toJson("hello")
  assert(!D.divideLaw.composition(S, S, S)(E))

Por otro lado, una prueba similar de JsDecoder cumple las leyes de composición de Applicative

  final case class Comp(a: String, b: Int)
  object Comp {
    implicit val equal: Equal[Comp] = ...
    implicit val decoder: JsDecoder[Comp] = ...
  }

  def composeTest(j: JsValue) = {
    val A: Applicative[JsDecoder] = Applicative[JsDecoder]
    val fa: JsDecoder[Comp] = JsDecoder[Comp]
    val fab: JsDecoder[Comp => (String, Int)] = A.point(c => (c.a, c.b))
    val fbc: JsDecoder[((String, Int)) => (Int, String)] = A.point(_.swap)
    val E: Equal[JsDecoder[(Int, String)]] = (p1, p2) => p1.fromJson(j) === p2.fromJson(j)
    assert(A.applyLaw.composition(fbc, fab, fa)(E))
  }

para un poco de valores de prueba

  composeTest(JsObject(IList("a" -> JsString("hello"), "b" -> JsInteger(1))))
  composeTest(JsNull)
  composeTest(JsObject(IList("a" -> JsString("hello"))))
  composeTest(JsObject(IList("b" -> JsInteger(1))))

Ahora estamos razonablemente seguros de que nuestra MonadError cumple con las leyes.

Sin embargo, simplemente porque tenemos una prueba que pasa para un conjunto pequeño de datos no prueba que las leyes son satisfechas. También debemos razonar durante la implementación para convencernos a nosotros mismos que de debería satisfacer las leyes, e intentar proponer casos especiales donde podría fallar.

Una forma de generar una amplia variedad de datos de prueba es usar la librería scalacheck, que proporciona una typeclass Arbitrary que se integra con la mayoría de los frameworks de prueba para repetir una prueba con datos generados aleatoriamente.

La librería jsonformat proporciona una Arbitrary[JsValue] (¡todos deberían proporcionar una instancia de Arbitrary para sus ADTs!) permitiéndonos hacer uso de la característica forAll de Scalatest:

  forAll(SizeRange(10))((j: JsValue) => composeTest(j))

Esta prueba nos proporciona aún más confianza de que nuestra typeclass cumple con las leyes de composición de Applicative. Al verificar todas las leyes en Divisible y MonadError también tenemos muchas pruebas de humo de manera gratuita.

8.2.5 Decidable y Alt

Donde Divisible y Applicative nos proporcionan derivaciones de typeclasses para productos (construidos a partir de tuplas), Decidable y Alt nos dan coproductos (construidos a partir de disyunciones anidadas):

  @typeclass trait Alt[F[_]] extends Applicative[F] with InvariantAlt[F] {
    def alt[A](a1: =>F[A], a2: =>F[A]): F[A]

    def altly1[Z, A1](a1: =>F[A1])(f: A1 => Z): F[Z] = ...
    def altly2[Z, A1, A2](a1: =>F[A1], a2: =>F[A2])(f: A1 \/ A2 => Z): F[Z] = ...
    def altly3 ...
    def altly4 ...
    ...
  }

  @typeclass trait Decidable[F[_]] extends Divisible[F] with InvariantAlt[F] {
    def choose1[Z, A1](a1: =>F[A1])(f: Z => A1): F[Z] = ...
    def choose2[Z, A1, A2](a1: =>F[A1], a2: =>F[A2])(f: Z => A1 \/ A2): F[Z] = ...
    def choose3 ...
    def choose4 ...
    ...
  }

Las cuatro typeclasses centrales tienen firmas simétricas:

Typeclass método dado firma devuelve
Applicative apply2 F[A1], F[A2] (A1, A2) => Z F[Z]
Alt altly2 F[A1], F[A2] (A1 \/ A2) => Z F[Z]
Divisible divide2 F[A1], F[A2] Z => (A1, A2) F[Z]
Decidable choose2 F[A1], F[A2] Z => (A1 \/ A2) F[Z]

soportando productos covariantes; coproductos covariantes; productos contravariantes; coproductos contravariantes.

Podemos escribir un Decidable[Equal], ¡permitiéndonos derivar Equal para cualquier ADT!

  implicit val decidable = new Decidable[Equal] {
    ...
    def choose2[Z, A1, A2](a1: =>Equal[A1], a2: =>Equal[A2])(
      f: Z => A1 \/ A2
    ): Equal[Z] = { (z1, z2) =>
      (f(z1), f(z2)) match {
        case (-\/(s), -\/(t)) => a1.equal(s, t)
        case (\/-(s), \/-(t)) => a2.equal(s, t)
        case _ => false
      }
    }
  }

Para un ADT

  sealed abstract class Darth { def widen: Darth = this }
  final case class Vader(s: String, i: Int)  extends Darth
  final case class JarJar(i: Int, s: String) extends Darth

donde los productos (Vader y JarJar) tienen un Equal

  object Vader {
    private val g: Vader => (String, Int) = d => (d.s, d.i)
    implicit val equal: Equal[Vader] = Divisible[Equal].divide2(Equal[String], Equal[Int])(g)
  }
  object JarJar {
    private val g: JarJar => (Int, String) = d => (d.i, d.s)
    implicit val equal: Equal[JarJar] = Divisible[Equal].divide2(Equal[Int], Equal[String])(g)
  }

podemos derivar la instancia de Equal para la ADT completa

  object Darth {
    private def g(t: Darth): Vader \/ JarJar = t match {
      case p @ Vader(_, _)  => -\/(p)
      case p @ JarJar(_, _) => \/-(p)
    }
    implicit val equal: Equal[Darth] = Decidable[Equal].choose2(Equal[Vader], Equal[JarJar])(g)
  }

  scala> Vader("hello", 1).widen === JarJar(1, "hello").widen
  false

Las typeclasses que tienen un Applicative pueden ser elegibles para Alt. Si deseamos usar nuestro truco con Klick.iso, tenemos que extender IsomorphismMonadError y hacer un mixin en Alt. Actualice nuestro MonadError[Default, String] para tener un Alt[Default]:

  private type K[a] = Kleisli[String \/ ?, Unit, a]
  implicit val monad = new IsomorphismMonadError[Default, K, String] with Alt[Default] {
    override val G = MonadError[K, String]
    override val iso = ...

    def alt[A](a1: =>Default[A], a2: =>Default[A]): Default[A] = instance(a1.default)
  }

Permitiéndonos derivar nuestro Default[Darth]

object Darth {
    ...
    private def f(e: Vader \/ JarJar): Darth = e.merge
    implicit val default: Default[Darth] =
      Alt[Default].altly2(Default[Vader], Default[JarJar])(f)
  }
  object Vader {
    ...
    private val f: (String, Int) => Vader = Vader(_, _)
    implicit val default: Default[Vader] =
      Alt[Default].apply2(Default[String], Default[Int])(f)
  }
  object JarJar {
    ...
    private val f: (Int, String) => JarJar = JarJar(_, _)
    implicit val default: Default[JarJar] =
      Alt[Default].apply2(Default[Int], Default[String])(f)
  }

  scala> Default[Darth].default
  \/-(Vader())

Regresando a las typeclasses de scalaz-deriving, los padres invariantes de Alt y Decidable son:

  @typeclass trait InvariantApplicative[F[_]] extends InvariantFunctor[F] {
    def xproduct0[Z](f: =>Z): F[Z]
    def xproduct1[Z, A1](a1: =>F[A1])(f: A1 => Z, g: Z => A1): F[Z] = ...
    def xproduct2 ...
    def xproduct3 ...
    def xproduct4 ...
  }

  @typeclass trait InvariantAlt[F[_]] extends InvariantApplicative[F] {
    def xcoproduct1[Z, A1](a1: =>F[A1])(f: A1 => Z, g: Z => A1): F[Z] = ...
    def xcoproduct2 ...
    def xcoproduct3 ...
    def xcoproduct4 ...
  }

soportando typeclasses con un InvariantFunctor como Monoid y Semigroup.

8.2.6 Aridad arbitraria y @deriving

Existen dos problemas con InvariantApplicative e InvariantAlt:

  1. Sólo soportan productos de cuatro campos y coproductos de cuatro entradas.
  2. Existe mucho código repetitivo en el objeto compañero del tipo de datos.

En esta sección resolveremos ambos problemas con clases adicionales introducidas por scalaz-deriving

Efectivamente, nuestras typeclasses centrales Applicative, Divisible, Alt y Decidable todas se pueden extender a aridades arbitrarias usando la librería iotaz, y por lo tanto el postfijo z.

La librería iotaz tiene tres tipos principales:

  • TList que describe cadenas de tipos con longitudes arbitrarias
  • Prod[A <: TList] para productos
  • Cop[A<: TList] para coproductos

A modo de ejemplo, una representación TList de Darth de la sección previa es

  import iotaz._, TList._

  type DarthT  = Vader  :: JarJar :: TNil
  type VaderT  = String :: Int    :: TNil
  type JarJarT = Int    :: String :: TNil

que puede ser instanciada como:

  val vader: Prod[VaderT]    = Prod("hello", 1)
  val jarjar: Prod[JarJarT]  = Prod(1, "hello")

  val VaderI = Cop.Inject[Vader, Cop[DarthT]]
  val darth: Cop[DarthT] = VaderI.inj(Vader("hello", 1))

Para ser capaces de usar la API de scalaz-deriving, necesitamos un Isomorfismo entre nuestras ADTs y la representación genérica de iotaz. Es mucho código repetitivo, y volveremos a esto en un momento:

  object Darth {
    private type Repr   = Vader :: JarJar :: TNil
    private val VaderI  = Cop.Inject[Vader, Cop[Repr]]
    private val JarJarI = Cop.Inject[JarJar, Cop[Repr]]
    private val iso     = IsoSet(
      {
        case d: Vader  => VaderI.inj(d)
        case d: JarJar => JarJarI.inj(d)
      }, {
        case VaderI(d)  => d
        case JarJarI(d) => d
      }
    )
    ...
  }

  object Vader {
    private type Repr = String :: Int :: TNil
    private val iso   = IsoSet(
      d => Prod(d.s, d.i),
      p => Vader(p.head, p.tail.head)
    )
    ...
  }

  object JarJar {
    private type Repr = Int :: String :: TNil
    private val iso   = IsoSet(
      d => Prod(d.i, d.s),
      p => JarJar(p.head, p.tail.head)
    )
    ...
  }

Con esto fuera del camino podemos llamar la API Deriving para Equal, posible porque scalaz-deriving proporciona una instancia optimizada de Deriving[Equal]

  object Darth {
    ...
    implicit val equal: Equal[Darth] = Deriving[Equal].xcoproductz(
      Prod(Need(Equal[Vader]), Need(Equal[JarJar])))(iso.to, iso.from)
  }
  object Vader {
    ...
    implicit val equal: Equal[Vader] = Deriving[Equal].xproductz(
      Prod(Need(Equal[String]), Need(Equal[Int])))(iso.to, iso.from)
  }
  object JarJar {
    ...
    implicit val equal: Equal[JarJar] = Deriving[Equal].xproductz(
      Prod(Need(Equal[Int]), Need(Equal[String])))(iso.to, iso.from)
  }

Para ser capaces de hacer lo mismo para nuestra typeclass Default, necesitamos proporcionar una instancia de Deriving[Default]. Esto es solo un caso de envolver nuestro Alt con un auxiliar:

  object Default {
    ...
    implicit val deriving: Deriving[Default] = ExtendedInvariantAlt(monad)
  }

y entonces invocarlo desde los objetos compañeros

  object Darth {
    ...
    implicit val default: Default[Darth] = Deriving[Default].xcoproductz(
      Prod(Need(Default[Vader]), Need(Default[JarJar])))(iso.to, iso.from)
  }
  object Vader {
    ...
    implicit val default: Default[Vader] = Deriving[Default].xproductz(
      Prod(Need(Default[String]), Need(Default[Int])))(iso.to, iso.from)
  }
  object JarJar {
    ...
    implicit val default: Default[JarJar] = Deriving[Default].xproductz(
      Prod(Need(Default[Int]), Need(Default[String])))(iso.to, iso.from)
  }

Hemos resuelto el problema de aridad arbitraria, pero hemos introducido aún más código repetitivo.

El punto clave es que la anotación @deriving, que viene del deriving-plugin, genera todo este código repetitivo automáticamente y únicamente requiere ser aplicado en el punto más alto de la jerarquía de una ADT:

  @deriving(Equal, Default)
  sealed abstract class Darth { def widen: Darth = this }
  final case class Vader(s: String, i: Int)  extends Darth
  final case class JarJar(i: Int, s: String) extends Darth

También están incluidos en scalaz-deriving instancias para Order, Semigroup y Monoid. Instancias para Show y Arbitrary están disponibles al instalar las librerías extras scalaz-deriving-magnolia y scalaz-deriving-scalacheck.

8.2.7 Ejemplos

Finalizamos nuestro estudio de scalaz-deriving con implementaciones completamente revisadas de todas las typeclasses de ejemplo. Antes de hacer esto necesitamos conocer sobre un nuevo tipo de datos: /~\, es decir, la serpiente en el camino, para contener dos estructuras higher kinded que comparten el mismo parámetro de tipo:

  sealed abstract class /~\[A[_], B[_]] {
    type T
    def a: A[T]
    def b: B[T]
  }
  object /~\ {
    type APair[A[_], B[_]]  = A /~\ B
    def unapply[A[_], B[_]](p: A /~\ B): Some[(A[p.T], B[p.T])] = ...
    def apply[A[_], B[_], Z](az: =>A[Z], bz: =>B[Z]): A /~\ B = ...
  }

Típicamente usamos esto en el contexto de Id /~\ TC donde TC es nuestra typeclass, significando que tenemos un valor, y una instancia de una typeclass para ese valor, sin saber nada más sobre el valor.

Además, todos los métodos en la API Deriving API tienen evidencia implícita de la forma A PairedWith FA, permitiendo que la librería iotaz sea capaz de realizar .zip, .traverse, y otras operaciones sobre Prod y Cop. Podemos ignorar estos parámetros, dado que no los usamos directamente.

8.2.7.1 Equal

Como con Default podríamos definir un Decidable de aridad fija y envolverlo con ExtendedInvariantAlt (la forma más simple), pero escogemos implementar Decidablez directamente por el beneficio de obtener más performance. Hacemos dos optimizaciones adicionales:

  1. Realizar igualdad de instancias .eq antes de aplicar Equal.equal, ejecutando un atajo al verificar la igualdad entre valores idénticos.
  2. Foldable.all permitiendo una salida temprana cuando cualquier comparación es false, por ejemplo si los primeros campos no empatan, ni siquiera pedimos Equal para los valores restantes.
  new Decidablez[Equal] {
    @inline private final def quick(a: Any, b: Any): Boolean =
      a.asInstanceOf[AnyRef].eq(b.asInstanceOf[AnyRef])

    def dividez[Z, A <: TList, FA <: TList](tcs: Prod[FA])(g: Z => Prod[A])(
      implicit ev: A PairedWith FA
    ): Equal[Z] = (z1, z2) => (g(z1), g(z2)).zip(tcs).all {
      case (a1, a2) /~\ fa => quick(a1, a2) || fa.value.equal(a1, a2)
    }

    def choosez[Z, A <: TList, FA <: TList](tcs: Prod[FA])(g: Z => Cop[A])(
      implicit ev: A PairedWith FA
    ): Equal[Z] = (z1, z2) => (g(z1), g(z2)).zip(tcs) match {
      case -\/(_)               => false
      case \/-((a1, a2) /~\ fa) => quick(a1, a2) || fa.value.equal(a1, a2)
    }
  }
8.2.7.2 Default

Tristemente, la API de iotaz para .traverse (y su análogo, .coptraverse) requiere que definamos transformaciones naturales, que tienen una sintaxis torpe, incluso con el plugin kind-projector.

  private type K[a] = Kleisli[String \/ ?, Unit, a]
  new IsomorphismMonadError[Default, K, String] with Altz[Default] {
    type Sig[a] = Unit => String \/ a
    override val G = MonadError[K, String]
    override val iso = Kleisli.iso(
      λ[Sig ~> Default](s => instance(s(()))),
      λ[Default ~> Sig](d => _ => d.default)
    )

    val extract = λ[NameF ~> (String \/ ?)](a => a.value.default)
    def applyz[Z, A <: TList, FA <: TList](tcs: Prod[FA])(f: Prod[A] => Z)(
      implicit ev: A PairedWith FA
    ): Default[Z] = instance(tcs.traverse(extract).map(f))

    val always = λ[NameF ~> Maybe](a => a.value.default.toMaybe)
    def altlyz[Z, A <: TList, FA <: TList](tcs: Prod[FA])(f: Cop[A] => Z)(
      implicit ev: A PairedWith FA
    ): Default[Z] = instance {
      tcs.coptraverse[A, NameF, Id](always).map(f).headMaybe \/> "not found"
    }
  }
8.2.7.3 Semigroup

No es posible definir un Semigroup para coproductos generales, sin embargo es posible definir uno para productos generales. Podemos usar la aridad arbitraria InvariantApplicative:

  new InvariantApplicativez[Semigroup] {
    type L[a] = ((a, a), NameF[a])
    val appender = λ[L ~> Id] { case ((a1, a2), fa) => fa.value.append(a1, a2) }

    def xproductz[Z, A <: TList, FA <: TList](tcs: Prod[FA])
                                             (f: Prod[A] => Z, g: Z => Prod[A])
                                             (implicit ev: A PairedWith FA) =
      new Semigroup[Z] {
        def append(z1: Z, z2: =>Z): Z = f(tcs.ziptraverse2(g(z1), g(z2), appender))
      }
  }
8.2.7.4 JsEncoder y JsDecoder

scalaz-deriving no proporciona acceso a nombres de los campos de modo que no es posible escribir un codificador/decodificador JSON.

8.3 Magnolia

La librería de macros Magnolia proporciona una API limpia para escribir derivaciones de typeclasses. Se instala con la siguiente entrada en build.sbt

  libraryDependencies += "com.propensive" %% "magnolia" % "0.10.1"

Un autor de typeclasses implementaría los siguientes miembros:

  import magnolia._

  object MyDerivation {
    type Typeclass[A]

    def combine[A](ctx: CaseClass[Typeclass, A]): Typeclass[A]
    def dispatch[A](ctx: SealedTrait[Typeclass, A]): Typeclass[A]

    def gen[A]: Typeclass[A] = macro Magnolia.gen[A]
  }

La API de Magnolia es:

  class CaseClass[TC[_], A] {
    def typeName: TypeName
    def construct[B](f: Param[TC, A] => B): A
    def constructMonadic[F[_]: Monadic, B](f: Param[TC, A] => F[B]): F[A]
    def parameters: Seq[Param[TC, A]]
    def annotations: Seq[Any]
  }

  class SealedTrait[TC[_], A] {
    def typeName: TypeName
    def subtypes: Seq[Subtype[TC, A]]
    def dispatch[B](value: A)(handle: Subtype[TC, A] => B): B
    def annotations: Seq[Any]
  }

con auxiliares

  class CaseClass[TC[_], A] {
    def typeName: TypeName
    def construct[B](f: Param[TC, A] => B): A
    def constructMonadic[F[_]: Monadic, B](f: Param[TC, A] => F[B]): F[A]
    def parameters: Seq[Param[TC, A]]
    def annotations: Seq[Any]
  }

  class SealedTrait[TC[_], A] {
    def typeName: TypeName
    def subtypes: Seq[Subtype[TC, A]]
    def dispatch[B](value: A)(handle: Subtype[TC, A] => B): B
    def annotations: Seq[Any]
  }

La typeclass Monadic, usada en constructMonadic, es generada automáticamente si nuestro tipo de datos tiene un .map y un método .flatMap cuando escribimos import mercator._

No tiene sentido usar Magnolia para typeclasses que pueden abstraerse con Divisible, Decidable, Applicative o Alt, dado que estas abstracciones proporcionan mucha estructura extra y pruebas de manera gratuita. Sin embargo, Magnolia ofrece funcionalidad que scalaz-deriving no puede proporcionar: acceso a los nombres de campos, nombres de tipos, anotaciones y valores por defecto.

8.3.1 Ejemplo: JSON

Tenemos ciertas decisiones de diseño que hacer con respecto a la serialización JSON:

  1. ¿Deberíamos incluir campos con valores null?
  2. ¿Deberíamos decodificar de manera distinta valores faltantes vs null?
  3. ¿Cómo codificamos el nombre de un coproducto?
  4. Cómo lidiamos con coproductos que no son JsObject?

Escogemos alternativas por defecto razonables

  • No incluya campos si el valor es un JsNull.
  • Maneje campos faltantes de la misma manera que los valores null.
  • Use un campo especial "type" para desambiguar coproductos usando el nombre del tipo.
  • Ponga valores primitivos en un campo especial "xvalue".

y deje que los usuarios adjunten una anotación a los campos coproductos y productos para personalizar sus formatos:

  sealed class json extends Annotation
  object json {
    final case class nulls()          extends json
    final case class field(f: String) extends json
    final case class hint(f: String)  extends json
  }
  @json.field("TYPE")
  sealed abstract class Cost
  final case class Time(s: String) extends Cost
  final case class Money(@json.field("integer") i: Int) extends Cost

Empiece con un JsEncoder que maneja únicamente nuestras elecciones razonables:

  object JsMagnoliaEncoder {
    type Typeclass[A] = JsEncoder[A]

    def combine[A](ctx: CaseClass[JsEncoder, A]): JsEncoder[A] = { a =>
      val empty = IList.empty[(String, JsValue)]
      val fields = ctx.parameters.foldRight(right) { (p, acc) =>
        p.typeclass.toJson(p.dereference(a)) match {
          case JsNull => acc
          case value  => (p.label -> value) :: acc
        }
      }
      JsObject(fields)
    }

    def dispatch[A](ctx: SealedTrait[JsEncoder, A]): JsEncoder[A] = a =>
      ctx.dispatch(a) { sub =>
        val hint = "type" -> JsString(sub.typeName.short)
        sub.typeclass.toJson(sub.cast(a)) match {
          case JsObject(fields) => JsObject(hint :: fields)
          case other            => JsObject(IList(hint, "xvalue" -> other))
        }
      }

    def gen[A]: JsEncoder[A] = macro Magnolia.gen[A]
  }

Podemos ver cómo la API de Magnolia hace que sea sencillo acceder a nombres de campos y typeclasses para cada parámetro.

Ahora agregue soporte para anotaciones que manejan preferencias del usuario. Para evitar la búsqueda de anotaciones en cada codificación, los guardaremos en un arreglo. Aunque el acceso a los campos en un arreglo no es total, tenemos la garantía de que los índices siempre serán adecuados. El rendimiento es con frecuencia la víctima en la negociación entre especialización y generalización.

  object JsMagnoliaEncoder {
    type Typeclass[A] = JsEncoder[A]

    def combine[A](ctx: CaseClass[JsEncoder, A]): JsEncoder[A] =
      new JsEncoder[A] {
        private val anns = ctx.parameters.map { p =>
          val nulls = p.annotations.collectFirst {
            case json.nulls() => true
          }.getOrElse(false)
          val field = p.annotations.collectFirst {
            case json.field(name) => name
          }.getOrElse(p.label)
          (nulls, field)
        }.toArray

        def toJson(a: A): JsValue = {
          val empty = IList.empty[(String, JsValue)]
          val fields = ctx.parameters.foldRight(empty) { (p, acc) =>
            val (nulls, field) = anns(p.index)
            p.typeclass.toJson(p.dereference(a)) match {
              case JsNull if !nulls => acc
              case value            => (field -> value) :: acc
            }
          }
          JsObject(fields)
        }
      }

    def dispatch[A](ctx: SealedTrait[JsEncoder, A]): JsEncoder[A] =
      new JsEncoder[A] {
        private val field = ctx.annotations.collectFirst {
          case json.field(name) => name
        }.getOrElse("type")
        private val anns = ctx.subtypes.map { s =>
          val hint = s.annotations.collectFirst {
            case json.hint(name) => field -> JsString(name)
          }.getOrElse(field -> JsString(s.typeName.short))
          val xvalue = s.annotations.collectFirst {
            case json.field(name) => name
          }.getOrElse("xvalue")
          (hint, xvalue)
        }.toArray

        def toJson(a: A): JsValue = ctx.dispatch(a) { sub =>
          val (hint, xvalue) = anns(sub.index)
          sub.typeclass.toJson(sub.cast(a)) match {
            case JsObject(fields) => JsObject(hint :: fields)
            case other            => JsObject(hint :: (xvalue -> other) :: IList.empty)
          }
        }
      }

    def gen[A]: JsEncoder[A] = macro Magnolia.gen[A]
  }

Para el decodificador usamos .constructMonadic que tiene una firma de tipo similar a .traverse

  object JsMagnoliaDecoder {
    type Typeclass[A] = JsDecoder[A]

    def combine[A](ctx: CaseClass[JsDecoder, A]): JsDecoder[A] = {
      case obj @ JsObject(_) =>
        ctx.constructMonadic(
          p => p.typeclass.fromJson(obj.get(p.label).getOrElse(JsNull))
        )
      case other => fail("JsObject", other)
    }

    def dispatch[A](ctx: SealedTrait[JsDecoder, A]): JsDecoder[A] = {
      case obj @ JsObject(_) =>
        obj.get("type") match {
          case \/-(JsString(hint)) =>
            ctx.subtypes.find(_.typeName.short == hint) match {
              case None => fail(s"a valid '$hint'", obj)
              case Some(sub) =>
                val value = obj.get("xvalue").getOrElse(obj)
                sub.typeclass.fromJson(value)
            }
          case _ => fail("JsObject with type", obj)
        }
      case other => fail("JsObject", other)
    }

    def gen[A]: JsDecoder[A] = macro Magnolia.gen[A]
  }

De nuevo, agregando soporte para las preferencias del usuario y valores por defecto para los campos, junto con algunas optimizaciones:

  object JsMagnoliaDecoder {
    type Typeclass[A] = JsDecoder[A]

    def combine[A](ctx: CaseClass[JsDecoder, A]): JsDecoder[A] =
      new JsDecoder[A] {
        private val nulls = ctx.parameters.map { p =>
          p.annotations.collectFirst {
            case json.nulls() => true
          }.getOrElse(false)
        }.toArray

        private val fieldnames = ctx.parameters.map { p =>
          p.annotations.collectFirst {
            case json.field(name) => name
          }.getOrElse(p.label)
        }.toArray

        def fromJson(j: JsValue): String \/ A = j match {
          case obj @ JsObject(_) =>
            import mercator._
            val lookup = obj.fields.toMap
            ctx.constructMonadic { p =>
              val field = fieldnames(p.index)
              lookup
                .get(field)
                .into {
                  case Maybe.Just(value) => p.typeclass.fromJson(value)
                  case _ =>
                    p.default match {
                      case Some(default) => \/-(default)
                      case None if nulls(p.index) =>
                        s"missing field '$field'".left
                      case None => p.typeclass.fromJson(JsNull)
                    }
                }
            }
          case other => fail("JsObject", other)
        }
      }

    def dispatch[A](ctx: SealedTrait[JsDecoder, A]): JsDecoder[A] =
      new JsDecoder[A] {
        private val subtype = ctx.subtypes.map { s =>
          s.annotations.collectFirst {
            case json.hint(name) => name
          }.getOrElse(s.typeName.short) -> s
        }.toMap
        private val typehint = ctx.annotations.collectFirst {
          case json.field(name) => name
        }.getOrElse("type")
        private val xvalues = ctx.subtypes.map { sub =>
          sub.annotations.collectFirst {
            case json.field(name) => name
          }.getOrElse("xvalue")
        }.toArray

        def fromJson(j: JsValue): String \/ A = j match {
          case obj @ JsObject(_) =>
            obj.get(typehint) match {
              case \/-(JsString(h)) =>
                subtype.get(h) match {
                  case None => fail(s"a valid '$h'", obj)
                  case Some(sub) =>
                    val xvalue = xvalues(sub.index)
                    val value  = obj.get(xvalue).getOrElse(obj)
                    sub.typeclass.fromJson(value)
                }
              case _ => fail(s"JsObject with '$typehint' field", obj)
            }
          case other => fail("JsObject", other)
        }
      }

    def gen[A]: JsDecoder[A] = macro Magnolia.gen[A]
  }

Llamamos al método JsMagnoliaEncoder.gen o a JsMagnoliaDecoder.gen desde el objeto compañero de nuestro tipo de datos. Por ejemplo, la API de Google Maps

  final case class Value(text: String, value: Int)
  final case class Elements(distance: Value, duration: Value, status: String)
  final case class Rows(elements: List[Elements])
  final case class DistanceMatrix(
    destination_addresses: List[String],
    origin_addresses: List[String],
    rows: List[Rows],
    status: String
  )

  object Value {
    implicit val encoder: JsEncoder[Value] = JsMagnoliaEncoder.gen
    implicit val decoder: JsDecoder[Value] = JsMagnoliaDecoder.gen
  }
  object Elements {
    implicit val encoder: JsEncoder[Elements] = JsMagnoliaEncoder.gen
    implicit val decoder: JsDecoder[Elements] = JsMagnoliaDecoder.gen
  }
  object Rows {
    implicit val encoder: JsEncoder[Rows] = JsMagnoliaEncoder.gen
    implicit val decoder: JsDecoder[Rows] = JsMagnoliaDecoder.gen
  }
  object DistanceMatrix {
    implicit val encoder: JsEncoder[DistanceMatrix] = JsMagnoliaEncoder.gen
    implicit val decoder: JsDecoder[DistanceMatrix] = JsMagnoliaDecoder.gen
  }

Felizmente, ¡la anotación @deriving soporta Magnolia! Si el autor de la typeclass proporciona un archivo deriving.conf junto con su jar, que contenga el siguiente texto

  jsonformat.JsEncoder=jsonformat.JsMagnoliaEncoder.gen
  jsonformat.JsDecoder=jsonformat.JsMagnoliaDecoder.gen

la deriving-macro llamará al método provisto por el usuario:

  @deriving(JsEncoder, JsDecoder)
  final case class Value(text: String, value: Int)
  @deriving(JsEncoder, JsDecoder)
  final case class Elements(distance: Value, duration: Value, status: String)
  @deriving(JsEncoder, JsDecoder)
  final case class Rows(elements: List[Elements])
  @deriving(JsEncoder, JsDecoder)
  final case class DistanceMatrix(
    destination_addresses: List[String],
    origin_addresses: List[String],
    rows: List[Rows],
    status: String
  )

8.3.2 Derivación completamente automática

La generación de instancias implicit en el objeto compañero del tipo de datos se conoce (históricamente) como derivación semiautomática, en contraste con la completamente automática que ocurre cuando el .gen se hace implicit

  object JsMagnoliaEncoder {
    ...
    implicit def gen[A]: JsEncoder[A] = macro Magnolia.gen[A]
  }
  object JsMagnoliaDecoder {
    ...
    implicit def gen[A]: JsDecoder[A] = macro Magnolia.gen[A]
  }

Los usuarios pueden importar estos métodos en su alcance y obtener derivación mágica en el punto de uso

  scala> final case class Value(text: String, value: Int)
  scala> import JsMagnoliaEncoder.gen
  scala> Value("hello", 1).toJson
  res = JsObject([("text","hello"),("value",1)])

Esto puede parecer tentador, dado que envuelve la cantidad mínima de escritura, pero hay dos advertencias a tomar en cuenta:

  1. La macro se llama en cada sitio de uso, es decir cada vez que llamamos .toJson. Esto hace que la compilación sea más lenta y también produce más objetos en tiempo de ejecución, lo que impactará el rendimiento en tiempo de ejecución.
  2. Podrían derivarse cosas inesperadas

La primera advertencia es evidente, pero las derivaciones inesperadas se manifiestan como errores sutiles. Considere lo que podría ocurrir con

  @deriving(JsEncoder)
  final case class Foo(s: Option[String])

si olvidamos proporcionar una derivación implícita para Option. Esperaríamos que Foo(Some("hello") se viera como

  {
    "s":"hello"
  }

Pero en realidad sería la siguiente

  {
    "s": {
      "type":"Some",
      "get":"hello"
    }
  }

porque Magnolia derivó un codificador Option por nosotros.

Esto es confuso, y preferiríamos que el compilador nos indicara que olvidamos algo. Es por esta razón que no se recomienda la derivación completamente automática.

8.4 Shapeless

La librería Shapeless es notablemente la librería más complicada en Scala. La razón de esto es porque toma la característica implicit del lenguaje hasta el extremo: la creación de un lenguaje de programación genérica a nivel de los tipos.

No se trata de un concepto extraño: en Scalaz intentamos limitar nuestro uso de la característica implicit a las typeclasses, pero algunas veces solicitamos que el compilador nos provea evidencia que relacione los tipos. Por ejemplo las relaciones Liskov o Leibniz (<~< y ===), y el inyectar (Inject) una álgebra libre de scalaz.Coproduct de álgebras.

Para instalar Shapeless, agregue lo siguiente a build.sbt

  libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.3"

En el centro de Shapeless están los tipos de datos HList y Coproduct

  package shapeless

  sealed trait HList
  final case class ::[+H, +T <: HList](head: H, tail: T) extends HList
  sealed trait NNil extends HList
  case object HNil extends HNil {
    def ::[H](h: H): H :: HNil = ::(h, this)
  }

  sealed trait Coproduct
  sealed trait :+:[+H, +T <: Coproduct] extends Coproduct
  final case class Inl[+H, +T <: Coproduct](head: H) extends :+:[H, T]
  final case class Inr[+H, +T <: Coproduct](tail: T) extends :+:[H, T]
  sealed trait CNil extends Coproduct // no implementations

que son representaciones genéricas de productos y coproductos. El trait sealed trait HNil se usa por motivos de conveniencia de modo que nunca sea necesario teclear HNil.type.

Shapeless tiene un clon del tipo de datos IsoSet, llamado Generic, que nos permite viajar entre una ADT y su representación genérica:

  trait Generic[T] {
    type Repr
    def to(t: T): Repr
    def from(r: Repr): T
  }
  object Generic {
    type Aux[T, R] = Generic[T] { type Repr = R }
    def apply[T](implicit G: Generic[T]): Aux[T, G.Repr] = G
    implicit def materialize[T, R]: Aux[T, R] = macro ...
  }

Muchos de los tipos en Shapeless tiene un tipo miembro (Repr) y un tipo .Aux alias en su objeto compañero que hace que el segundo tipo sea visible. Esto permite solicitar Generic[Foo] para un tipo Foo sin tener que proporcionar la representación genérica, que se genera por una macro.

  scala> import shapeless._
  scala> final case class Foo(a: String, b: Long)
         Generic[Foo].to(Foo("hello", 13L))
  res: String :: Long :: HNil = hello :: 13 :: HNil

  scala> Generic[Foo].from("hello" :: 13L :: HNil)
  res: Foo = Foo(hello,13)

  scala> sealed abstract class Bar
         case object Irish extends Bar
         case object English extends Bar

  scala> Generic[Bar].to(Irish)
  res: English.type :+: Irish.type :+: CNil.type = Inl(Irish)

  scala> Generic[Bar].from(Inl(Irish))
  res: Bar = Irish

Existe un LabelledGeneric complementario que incluye los nombres de los campos

  scala> import shapeless._, labelled._
  scala> final case class Foo(a: String, b: Long)

  scala> LabelledGeneric[Foo].to(Foo("hello", 13L))
  res: String with KeyTag[Symbol with Tagged[String("a")], String] ::
       Long   with KeyTag[Symbol with Tagged[String("b")],   Long] ::
       HNil =
       hello :: 13 :: HNil

  scala> sealed abstract class Bar
         case object Irish extends Bar
         case object English extends Bar

  scala> LabelledGeneric[Bar].to(Irish)
  res: Irish.type   with KeyTag[Symbol with Tagged[String("Irish")],     Irish.type] :+:
       English.type with KeyTag[Symbol with Tagged[String("English")], English.type] :+:
       CNil.type =
       Inl(Irish)

Note que el valor de una representación LabelledGeneric es la misma que la representación Generic: los nombres de los campos existen únicamente en el tipo y son borrados en tiempo de ejecución.

Nunca necesitamos teclear KeyTag manualmente, y usamos el alias de tipo:

  type FieldType[K, +V] = V with KeyTag[K, V]

Si deseamos acceder al campo de nombre desde un FielType[K, A], pedimos evidencia implícita Witness.Aux[K], que nos permite acceder al valor de K en tiempo de ejecución.

Superficialmente, esto es todo lo que tenemos que saber de Shapeless para ser capaces de derivar un typeclass. Sin embargo, las cosas se ponen cada vez más complejas, de modo que procederemos con ejemplos cada vez más complejos.

8.4.1 Ejemplo: Equal

Un patrón típico es el de extender la typeclass que deseamos derivar, y poner el código de Shapeless en su objeto compañero. Esto nos da un alcance implícito que el compilador puede buscar sin requerir imports complejos.

  trait DerivedEqual[A] extends Equal[A]
  object DerivedEqual {
    ...
  }

El punto de entrada a una derivación de Shapeless es un método, gen, que requiere dos parámetros de tipo, la A que estamos derivando y la R para su representación genérica. Entonces pedimos por la Generic.Aux[A, R], que relaciona A con R, y una instancia de la typeclass Derived para la R. Empezamos con esta firma y una implementación simple:

  import shapeless._

  object DerivedEqual {
    def gen[A, R: DerivedEqual](implicit G: Generic.Aux[A, R]): Equal[A] =
      (a1, a2) => Equal[R].equal(G.to(a1), G.to(a2))
  }

Así hemos reducido el problema a proporcionar un implícito Equal[R] para una R que es la representación Generic de A. Primero considere los productos, donde R <: HList. Esta es la firma que deseamos implementar:

  implicit def hcons[H: Equal, T <: HList: DerivedEqual]: DerivedEqual[H :: T]

porque si podemos implementarlo para una cabeza y una cola, el compilador será capaz de realizar una recursión en este método hasta que alcance el final de la lista. Donde necesitemos proporcionar una instancia para el HNil vacío

  implicit def hnil: DerivedEqual[HNil]

Implementamos estos métodos

  implicit def hcons[H: Equal, T <: HList: DerivedEqual]: DerivedEqual[H :: T] =
    (h1, h2) => Equal[H].equal(h1.head, h2.head) && Equal[T].equal(h1.tail, h2.tail)

  implicit val hnil: DerivedEqual[HNil] = (_, _) => true

y para los coproductos deseamos implementar estas firmas

  implicit def ccons[H: Equal, T <: Coproduct: DerivedEqual]: DerivedEqual[H :+: T]
  implicit def cnil: DerivedEqual[CNil]

.cnil nunca será llamada por una typeclass como Equal con parámetros de tipo únicamente en posición contravariante. Pero el compilador no sabe esto, de modo que tenemos que proporcionar un stub:

  implicit val cnil: DerivedEqual[CNil] = (_, _) => sys.error("impossible")

Para el caso del coproducto sólo podemos comparar dos cosas si están alineadas, que ocurre cuando son tanto In1 o Inr

  implicit def ccons[H: Equal, T <: Coproduct: DerivedEqual]: DerivedEqual[H :+: T] = {
    case (Inl(c1), Inl(c2)) => Equal[H].equal(c1, c2)
    case (Inr(c1), Inr(c2)) => Equal[T].equal(c1, c2)
    case _                  => false
  }

¡Es digno de mención que nuestros métodos se alinean con el concepto de conquer (hnil), divide2 (hlist) y alt2 (coproduct)! Sin embargo, no tenemos ninguna de las ventajas de implementar Decidable, debido a que ahora debemos empezar desde ceros cuando escribimos pruebas para este código.

De modo que probemos esto con una ADT simple

  sealed abstract class Foo
  final case class Bar(s: String)          extends Foo
  final case class Faz(b: Boolean, i: Int) extends Foo
  final case object Baz                    extends Foo

Tenemos que proporcionar instancias en los objetos compañeros:

  object Foo {
    implicit val equal: Equal[Foo] = DerivedEqual.gen
  }
  object Bar {
    implicit val equal: Equal[Bar] = DerivedEqual.gen
  }
  object Faz {
    implicit val equal: Equal[Faz] = DerivedEqual.gen
  }
  final case object Baz extends Foo {
    implicit val equal: Equal[Baz.type] = DerivedEqual.gen
  }

Pero no siempre compila

  [error] shapeless.scala:41:38: ambiguous implicit values:
  [error]  both value hnil in object DerivedEqual of type => DerivedEqual[HNil]
  [error]  and value cnil in object DerivedEqual of type => DerivedEqual[CNil]
  [error]  match expected type DerivedEqual[R]
  [error]     : Equal[Baz.type] = DerivedEqual.gen
  [error]                                      ^

¡Bienvenido a los mensajes de error de compilación de Shapeless!

El problema, que no es evidente en lo absoluto a partir del mensaje de error, es que el compilador es incapaz de averiguar qué es R, y “piensa” que es otra cosa. Requerimos proporcionar los parámetros de tipo explícitamente cuando llamamos a gen, es decir

  implicit val equal: Equal[Baz.type] = DerivedEqual.gen[Baz.type, HNil]

o podemos usar la macro Generic para ayudarnos y dejar que el compilador infiera la representación genérica

  final case object Baz extends Foo {
    implicit val generic                = Generic[Baz.type]
    implicit val equal: Equal[Baz.type] = DerivedEqual.gen[Baz.type, generic.Repr]
  }
  ...

La razón por la que esto soluciona el problema es porque la firma de tipo, después de quitar algunas conveniencias sintácticas (syntactic sugar)

  def gen[A, R: DerivedEqual](implicit G: Generic.Aux[A, R]): Equal[A]

se convierte en

  def gen[A, R](implicit R: DerivedEqual[R], G: Generic.Aux[A, R]): Equal[A]

El compilador de Scala resuelve estas restricciones de tipo de izquierda a derecha, de modo que encuentra muchas soluciones distintas a DerivedEqual[R] antes de aplicar la restricción Generic.Aux[A, R]. Otra forma de resolver esto es no usar límites de contexto.

Con esto en mente, ya no requerimos el implicit val generic o los parámetros de tipo explícitos en la invocación a .gen. Podemos alambrar @deriving al agregar una entrada en deriving.conf (asumiendo que deseamos hacer un override de la implementación scalaz-deriving).

 scalaz.Equal=fommil.DerivedEqual.gen

y escribir

  @deriving(Equal) sealed abstract class Foo
  @deriving(Equal) final case class Bar(s: String)          extends Foo
  @deriving(Equal) final case class Faz(b: Boolean, i: Int) extends Foo
  @deriving(Equal) final case object Baz

Pero, reemplazar scalaz-deriving significa que los tiempos de compilación se vuelven más lentos. Esto se debe a que el compilador está resolviendo N búsquedas implícitas para cada producto de N campos o coproductos de N productos, mientras que scalaz-deriving y Magnolia no.

Note que cuando usamos scalaz-deriving o Magnolia podemos poner la anotación @deriving únicamente en el miembro más alto de una ADT, pero para Shapeless debemos agregarlo a todas sus entradas.

Sin embargo, esta implementación todavía tiene un error: falla para los tipos recursivos en tiempo de ejecución, es decir

  @deriving(Equal) sealed trait ATree
  @deriving(Equal) final case class Leaf(value: String)               extends ATree
  @deriving(Equal) final case class Branch(left: ATree, right: ATree) extends ATree
  scala> val leaf1: Leaf    = Leaf("hello")
         val leaf2: Leaf    = Leaf("goodbye")
         val branch: Branch = Branch(leaf1, leaf2)
         val tree1: ATree   = Branch(leaf1, branch)
         val tree2: ATree   = Branch(leaf2, branch)

  scala> assert(tree1 /== tree2)
  [error] java.lang.NullPointerException
  [error] at DerivedEqual$.shapes$DerivedEqual$$$anonfun$hcons$1(shapeless.scala:16)
          ...

La razón por la que esto pasa es porque Equal[Tree] depende de Equal[Branch], que a su vez depende de Equal[Tree]. ¡Recursión y BANG! Debe cargarse de manera perezosa, y no de manera estricta.

Tanto scalaz-deriving como Magnolia lidian automáticamente con la pereza, pero en Shapeless es la responsabilidad del autor de la typeclass.

Los tipos de macro Cached, Strict y Lazy modifican el comportamiento de la inferencia de tipos del compilador, permitiéndonos alcanzar el nivel de pereza que necesitemos. El patrón que hay que seguir es usar Cached[Strict[_]] en el punto de entrada y Lazy[_] alrededor de las instancias H.

Es mejor apartarse de los límites de contexto y los tipos SAM a partir de este punto:

  sealed trait DerivedEqual[A] extends Equal[A]
  object DerivedEqual {
    def gen[A, R](
      implicit G: Generic.Aux[A, R],
      R: Cached[Strict[DerivedEqual[R]]]
    ): Equal[A] = new Equal[A] {
      def equal(a1: A, a2: A) =
        quick(a1, a2) || R.value.value.equal(G.to(a1), G.to(a2))
    }

    implicit def hcons[H, T <: HList](
      implicit H: Lazy[Equal[H]],
      T: DerivedEqual[T]
    ): DerivedEqual[H :: T] = new DerivedEqual[H :: T] {
      def equal(ht1: H :: T, ht2: H :: T) =
        (quick(ht1.head, ht2.head) || H.value.equal(ht1.head, ht2.head)) &&
          T.equal(ht1.tail, ht2.tail)
    }

    implicit val hnil: DerivedEqual[HNil] = new DerivedEqual[HNil] {
      def equal(@unused h1: HNil, @unused h2: HNil) = true
    }

    implicit def ccons[H, T <: Coproduct](
      implicit H: Lazy[Equal[H]],
      T: DerivedEqual[T]
    ): DerivedEqual[H :+: T] = new DerivedEqual[H :+: T] {
      def equal(ht1: H :+: T, ht2: H :+: T) = (ht1, ht2) match {
        case (Inl(c1), Inl(c2)) => quick(c1, c2) || H.value.equal(c1, c2)
        case (Inr(c1), Inr(c2)) => T.equal(c1, c2)
        case _                  => false
      }
    }

    implicit val cnil: DerivedEqual[CNil] = new DerivedEqual[CNil] {
      def equal(@unused c1: CNil, @unused c2: CNil) = sys.error("impossible")
    }

    @inline private final def quick(a: Any, b: Any): Boolean =
      a.asInstanceOf[AnyRef].eq(b.asInstanceOf[AnyRef])
  }

Mientras estábamos haciendo esto, optimizamos usando el atajo quick de scalaz-deriving.

Ahora podemos llamar

  assert(tree1 /== tree2)

sin tener una excepción en tiempo de ejecución.

8.4.2 Ejemplo: Default

Hay dos trampas en la implementación de una typeclass con un parámetro de tipo en posición covariante. Aquí creamos valores HList y Coproducto, y debemos proporcionar un valor para el caso CNil dado que corresponde al caso donde ningún coproducto es capaz de proporcionar un valor.

  sealed trait DerivedDefault[A] extends Default[A]
  object DerivedDefault {
    def gen[A, R](
      implicit G: Generic.Aux[A, R],
      R: Cached[Strict[DerivedDefault[R]]]
    ): Default[A] = new Default[A] {
      def default = R.value.value.default.map(G.from)
    }

    implicit def hcons[H, T <: HList](
      implicit H: Lazy[Default[H]],
      T: DerivedDefault[T]
    ): DerivedDefault[H :: T] = new DerivedDefault[H :: T] {
      def default =
        for {
          head <- H.value.default
          tail <- T.default
        } yield head :: tail
    }

    implicit val hnil: DerivedDefault[HNil] = new DerivedDefault[HNil] {
      def default = HNil.right
    }

    implicit def ccons[H, T <: Coproduct](
      implicit H: Lazy[Default[H]],
      T: DerivedDefault[T]
    ): DerivedDefault[H :+: T] = new DerivedDefault[H :+: T] {
      def default = H.value.default.map(Inl(_)).orElse(T.default.map(Inr(_)))
    }

    implicit val cnil: DerivedDefault[CNil] = new DerivedDefault[CNil] {
      def default = "not a valid coproduct".left
    }
  }

Así como pudimos establecer una analogía entre Equal y Decidable, podemos ver la relación con Alt en .point (hnil), .apply2 (.hcons) y .altly2 (.ccons).

Hay poco que aprender de un ejemplo como Semigroup, de modo que iremos directamente a estudiar los codificadores y decodificadores.

8.4.3 Ejemplo: JsEncoder

Para ser capaces de reproducir nuestro codificador JSON de Magnolia, debemos ser capaces de acceder a

  1. los nombres de los campos y de las clases
  2. las anotaciones para las preferencias del usuario
  3. los valores por defecto en una case class

Empezaremos al crear un codificador que maneje únicamente los valores por defecto.

Para obtener los nombres de los campos, usamos LabelledGeneric en lugar de Generic, y cuando definimos el tipo del elemento en la cabeza, usamos FieldType[K, H] en lugar de simplemente H. Un Witness.Aux[K] proporciona el valor del nombre del campo en tiempo de ejecución.

Todos nuestros métodos van a devolver JsObject, de modo que en lugar de devolver un JsValue podemos especializar y crear DerivedJsEncoder que tiene una firma de tipo distinta a la de JsEncoder

  import shapeless._, labelled._

  sealed trait DerivedJsEncoder[R] {
    def toJsFields(r: R): IList[(String, JsValue)]
  }
  object DerivedJsEncoder {
    def gen[A, R](
      implicit G: LabelledGeneric.Aux[A, R],
      R: Cached[Strict[DerivedJsEncoder[R]]]
    ): JsEncoder[A] = new JsEncoder[A] {
      def toJson(a: A) = JsObject(R.value.value.toJsFields(G.to(a)))
    }

    implicit def hcons[K <: Symbol, H, T <: HList](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsEncoder[H]],
      T: DerivedJsEncoder[T]
    ): DerivedJsEncoder[FieldType[K, H] :: T] =
      new DerivedJsEncoder[A, FieldType[K, H] :: T] {
        private val field = K.value.name
        def toJsFields(ht: FieldType[K, H] :: T) =
          ht match {
            case head :: tail =>
              val rest = T.toJsFields(tail)
              H.value.toJson(head) match {
                case JsNull => rest
                case value  => (field -> value) :: rest
              }
          }
      }

    implicit val hnil: DerivedJsEncoder[HNil] =
      new DerivedJsEncoder[HNil] {
        def toJsFields(h: HNil) = IList.empty
      }

    implicit def ccons[K <: Symbol, H, T <: Coproduct](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsEncoder[H]],
      T: DerivedJsEncoder[T]
    ): DerivedJsEncoder[FieldType[K, H] :+: T] =
      new DerivedJsEncoder[FieldType[K, H] :+: T] {
        private val hint = ("type" -> JsString(K.value.name))
        def toJsFields(ht: FieldType[K, H] :+: T) = ht match {
          case Inl(head) =>
            H.value.toJson(head) match {
              case JsObject(fields) => hint :: fields
              case v                => IList.single("xvalue" -> v)
            }

          case Inr(tail) => T.toJsFields(tail)
        }
      }

    implicit val cnil: DerivedJsEncoder[CNil] =
      new DerivedJsEncoder[CNil] {
        def toJsFields(c: CNil) = sys.error("impossible")
      }

  }

Shapeless selecciona los caminos de código en tiempo de compilación basándose en la presencia de anotaciones, lo que puede llevarnos a código más optimizado, a expensas de repetición de código. Esto significa que el número de anotaciones con las que estamos lidiando, y sus subtipos debe ser manejable o nos encontraremos escribiendo una cantidad de código unas diez veces más grande. Podemos poner nuestras tres anotaciones en una sola que contenga todos los parámetros de optimización:

 case class json(
    nulls: Boolean,
    field: Option[String],
    hint: Option[String]
  ) extends Annotation

Todos los usuarios de nuestra anotación deben proporcionar los tres valores dado que los valores por defecto y los métodos de conveniencia no están disponibles en los constructores de anotaciones. Podemos escribir extractores personalizados de modo que no tengamos que cambiar nuestro código de Magnolia

  object json {
    object nulls {
      def unapply(j: json): Boolean = j.nulls
    }
    object field {
      def unapply(j: json): Option[String] = j.field
    }
    object hint {
      def unapply(j: json): Option[String] = j.hint
    }
  }

Podemos solicitar Annotation[json, A] para una case class o un sealed trait para obtener acceso a la anotación, pero entonces debemos escribir un hcons y un ccons que lidien con ambos casos debido a que la evidencia no será generada si no está presente la anotación. Por lo tanto tenemos que introducir un alcance implícito de prioridad más baja y poner la evidencia de que no hay anotaciones ahí.

También podríamos solicitar evidencia Annotations.Aux[json, A, J] para obtener un HList de la anotación json para el tipo A. De nuevo, debemos proporcionar hcons y ccons que lidian con los casos donde hay y no hay una anotación.

Para soportar esta anotación, ¡debemos escribir cuatro veces la cantidad de código que antes!

Empezaremos por reescribir el JsEncoder, únicamente manejando código del usuario que no tenga ninguna anotación. Ahora cualquier código que use la anotación @json fallará en compilar, y esta es una buena garantía de seguridad.

Debemos agregar tipos A y J al DerivedJsEncoder y pasarlos a través de las anotaciones en su método .toJsObject. Nuestra evidencia .hcons y .ccons ahora proporciona instancias para DerivedJsEEncoder con una anotación None.type y las movemos a una prioridad más baja de modo que podamos lidiar con Annotation[json, A] en una prioridad más alta.

Note que la evidencia para J está listada antes que la correspondiente a R. Esto es importante, dado que el compilador debe primero fijar el tipo de J antes de que pueda resolver el tipo para R.

  sealed trait DerivedJsEncoder[A, R, J <: HList] {
    def toJsFields(r: R, anns: J): IList[(String, JsValue)]
  }
  object DerivedJsEncoder extends DerivedJsEncoder1 {
    def gen[A, R, J <: HList](
      implicit
      G: LabelledGeneric.Aux[A, R],
      J: Annotations.Aux[json, A, J],
      R: Cached[Strict[DerivedJsEncoder[A, R, J]]]
    ): JsEncoder[A] = new JsEncoder[A] {
      def toJson(a: A) = JsObject(R.value.value.toJsFields(G.to(a), J()))
    }

    implicit def hnil[A]: DerivedJsEncoder[A, HNil, HNil] =
      new DerivedJsEncoder[A, HNil, HNil] {
        def toJsFields(h: HNil, a: HNil) = IList.empty
      }

    implicit def cnil[A]: DerivedJsEncoder[A, CNil, HNil] =
      new DerivedJsEncoder[A, CNil, HNil] {
        def toJsFields(c: CNil, a: HNil) = sys.error("impossible")
      }
  }
  private[jsonformat] trait DerivedJsEncoder1 {
    implicit def hcons[A, K <: Symbol, H, T <: HList, J <: HList](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsEncoder[H]],
      T: DerivedJsEncoder[A, T, J]
    ): DerivedJsEncoder[A, FieldType[K, H] :: T, None.type :: J] =
      new DerivedJsEncoder[A, FieldType[K, H] :: T, None.type :: J] {
        private val field = K.value.name
        def toJsFields(ht: FieldType[K, H] :: T, anns: None.type :: J) =
          ht match {
            case head :: tail =>
              val rest = T.toJsFields(tail, anns.tail)
              H.value.toJson(head) match {
                case JsNull => rest
                case value  => (field -> value) :: rest
              }
          }
      }

    implicit def ccons[A, K <: Symbol, H, T <: Coproduct, J <: HList](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsEncoder[H]],
      T: DerivedJsEncoder[A, T, J]
    ): DerivedJsEncoder[A, FieldType[K, H] :+: T, None.type :: J] =
      new DerivedJsEncoder[A, FieldType[K, H] :+: T, None.type :: J] {
        private val hint = ("type" -> JsString(K.value.name))
        def toJsFields(ht: FieldType[K, H] :+: T, anns: None.type :: J) =
          ht match {
            case Inl(head) =>
              H.value.toJson(head) match {
                case JsObject(fields) => hint :: fields
                case v                => IList.single("xvalue" -> v)
              }
            case Inr(tail) => T.toJsFields(tail, anns.tail)
          }
      }
  }

Ahora podemos agregar las firmas de tipo para los seis métodos nuevos, cubriendo todas las posibilidades para los lugares posibles donde puede ocurrir la anotación. Note que únicamente soportamos una anotación en cada posición. Si el usuario proporciona múltiples anotaciones, cualquiera después de la primera será ignorada silenciosamente.

Ya nos estamos quedando sin nombres para las cosas, de modo que llamaremos arbitrariamente Annotated cuando exista una anotación para la A, y Custom cuando exista una anotación sobre el campo.

  object DerivedJsEncoder extends DerivedJsEncoder1 {
    ...
    implicit def hconsAnnotated[A, K <: Symbol, H, T <: HList, J <: HList](
      implicit
      A: Annotation[json, A],
      K: Witness.Aux[K],
      H: Lazy[JsEncoder[H]],
      T: DerivedJsEncoder[A, T, J]
    ): DerivedJsEncoder[A, FieldType[K, H] :: T, None.type :: J]

    implicit def cconsAnnotated[A, K <: Symbol, H, T <: Coproduct, J <: HList](
      implicit
      A: Annotation[json, A],
      K: Witness.Aux[K],
      H: Lazy[JsEncoder[H]],
      T: DerivedJsEncoder[A, T, J]
    ): DerivedJsEncoder[A, FieldType[K, H] :+: T, None.type :: J]

    implicit def hconsAnnotatedCustom[A, K <: Symbol, H, T <: HList, J <: HList](
      implicit
      A: Annotation[json, A],
      K: Witness.Aux[K],
      H: Lazy[JsEncoder[H]],
      T: DerivedJsEncoder[A, T, J]
    ): DerivedJsEncoder[A, FieldType[K, H] :: T, Some[json] :: J]

    implicit def cconsAnnotatedCustom[A, K <: Symbol, H, T <: Coproduct, J <: HList](
      implicit
      A: Annotation[json, A],
      K: Witness.Aux[K],
      H: Lazy[JsEncoder[H]],
      T: DerivedJsEncoder[A, T, J]
    ): DerivedJsEncoder[A, FieldType[K, H] :+: T, Some[json] :: J]
  }
  private[jsonformat] trait DerivedJsEncoder1 {
    ...
    implicit def hconsCustom[A, K <: Symbol, H, T <: HList, J <: HList](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsEncoder[H]],
      T: DerivedJsEncoder[A, T, J]
    ): DerivedJsEncoder[A, FieldType[K, H] :: T, Some[json] :: J] = ???

    implicit def cconsCustom[A, K <: Symbol, H, T <: Coproduct, J <: HList](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsEncoder[H]],
      T: DerivedJsEncoder[A, T, J]
    ): DerivedJsEncoder[A, FieldType[K, H] :+: T, Some[json] :: J]
  }

En realidad no necesitamos .hconsAnnotated o .hconsAnnotatedCustom para nada, dado que una anotación en una case class no significa nada para la codificación de dicho producto, y sólo se usa en .cconsAnnotated*. Por lo tanto, podemos borrar dos métodos.

.cconsAnnotated y .cconsAnnotatedCustom pueden definirse como

  new DerivedJsEncoder[A, FieldType[K, H] :+: T, None.type :: J] {
    private val hint = A().field.getOrElse("type") -> JsString(K.value.name)
    def toJsFields(ht: FieldType[K, H] :+: T, anns: None.type :: J) = ht match {
      case Inl(head) =>
        H.value.toJson(head) match {
          case JsObject(fields) => hint :: fields
          case v                => IList.single("xvalue" -> v)
        }
      case Inr(tail) => T.toJsFields(tail, anns.tail)
    }
  }

y

  new DerivedJsEncoder[A, FieldType[K, H] :+: T, Some[json] :: J] {
    private val hintfield = A().field.getOrElse("type")
    def toJsFields(ht: FieldType[K, H] :+: T, anns: Some[json] :: J) = ht match {
      case Inl(head) =>
        val ann = anns.head.get
        H.value.toJson(head) match {
          case JsObject(fields) =>
            val hint = (hintfield -> JsString(ann.hint.getOrElse(K.value.name)))
            hint :: fields
          case v =>
            val xvalue = ann.field.getOrElse("xvalue")
            IList.single(xvalue -> v)
        }
      case Inr(tail) => T.toJsFields(tail, anns.tail)
    }
  }

Podría preocupar un poco el uso de .head y .get pero recuerde que los tipos aquí son :: y Some, y por lo tanto estos métodos son totales y seguros de usarse.

.hconsCustom y .cconsCustom se escriben

  new DerivedJsEncoder[A, FieldType[K, H] :: T, Some[json] :: J] {
    def toJsFields(ht: FieldType[K, H] :: T, anns: Some[json] :: J) = ht match {
      case head :: tail =>
        val ann  = anns.head.get
        val next = T.toJsFields(tail, anns.tail)
        H.value.toJson(head) match {
          case JsNull if !ann.nulls => next
          case value =>
            val field = ann.field.getOrElse(K.value.name)
            (field -> value) :: next
        }
    }
  }

y

  new DerivedJsEncoder[A, FieldType[K, H] :+: T, Some[json] :: J] {
    def toJsFields(ht: FieldType[K, H] :+: T, anns: Some[json] :: J) = ht match {
      case Inl(head) =>
        val ann = anns.head.get
        H.value.toJson(head) match {
          case JsObject(fields) =>
            val hint = ("type" -> JsString(ann.hint.getOrElse(K.value.name)))
            hint :: fields
          case v =>
            val xvalue = ann.field.getOrElse("xvalue")
            IList.single(xvalue -> v)
        }
      case Inr(tail) => T.toJsFields(tail, anns.tail)
    }
  }

Obviamente, hay mucho código repetitivo y verboso, pero observando de cerca podemos ver que cada método está implementado tan eficientemente como sea posible con la información que tiene: los caminos del código se seleccionan en tiempo de compilación más bien que en tiempo de ejecución.

Los que sean obsesivos con el rendimiento tal vez deseen refactorizar este código de modo que toda la información de anotaciones esté disponible de antemano, más bien que inyectada por medio del método .toJsFields, con otra capa de indirección. Para un rendimiento máximo, también podríamos tratar cada personalización como una anotación separada, pero eso multiplicaría la cantidad de código que hemos escrito todavía más, con un coste adicional para el tiempo de compilación requerido a nuestros usuarios. Tales optimizaciones están más allá del alcance de este libro, pero es posible y las personas las hacen: la habilidad de mover trabajo de tiempo de ejecución a tiempo de compilación es una de las cosas más atractivas de la programación genérica.

Una advertencia más de la que tenemos que estar conscientes: LabelledGeneric no es compatible con scalaz.@@, pero existe una solución. Digamos que efectivamente deseamos ignorar las etiquetas de modo que agregamos las siguientes reglas de derivación a los objetos compañeros de nuestro codificador y decodificador

  object JsEncoder {
    ...
    implicit def tagged[A: JsEncoder, Z]: JsEncoder[A @@ Z] =
      JsEncoder[A].contramap(Tag.unwrap)
  }
  object JsDecoder {
    ...
    implicit def tagged[A: JsDecoder, Z]: JsDecoder[A @@ Z] =
      JsDecoder[A].map(Tag(_))
  }

Esperaríamos ser capaces de derivar un JsDecoder para algo como nuestra TradeTemplate del Capítulo 5

  final case class TradeTemplate(
    otc: Option[Boolean] @@ Tags.Last
  )
  object TradeTemplate {
    implicit val encoder: JsEncoder[TradeTemplate] = DerivedJsEncoder.gen
  }

Pero obtenemos el siguiente error de compilación

  [error] could not find implicit value for parameter G: LabelledGeneric.Aux[A,R]
  [error]   implicit val encoder: JsEncoder[TradeTemplate] = DerivedJsEncoder.gen
  [error]                                                                     ^

El mensaje de error es tan útil como siempre. La solución es introducir evidencia para H @@ Z en el alcance implícito de menor prioridad, y entonces simplemente invocar el código que el compilador habría encontrado en primer lugar:

  object DerivedJsEncoder extends DerivedJsEncoder1 with DerivedJsEncoder2 {
    ...
  }
  private[jsonformat] trait DerivedJsEncoder2 {
    this: DerivedJsEncoder.type =>

    // WORKAROUND https://github.com/milessabin/shapeless/issues/309
    implicit def hconsTagged[A, K <: Symbol, H, Z, T <: HList, J <: HList](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsEncoder[H @@ Z]],
      T: DerivedJsEncoder[A, T, J]
    ): DerivedJsEncoder[A, FieldType[K, H @@ Z] :: T, None.type :: J] = hcons(K, H, T)

    implicit def hconsCustomTagged[A, K <: Symbol, H, Z, T <: HList, J <: HList](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsEncoder[H @@ Z]],
      T: DerivedJsEncoder[A, T, J]
    ): DerivedJsEncoder[A, FieldType[K, H @@ Z] :: T, Some[json] :: J] = hconsCustom(K, H, T)
  }

Felizmente, sólo es necesario considerar los productos, dado que no es posible agregar etiquetas a los coproductos.

8.4.4 JsDecoder

El lado de la decodificación es tal como lo esperaríamos basándonos en los ejemplos previos. Podemos construir una instancia de un FieldType[K, H con el auxiliar field[K](h: H). Soportando únicamente los razonables valores por defecto significa que escribimos:

  sealed trait DerivedJsDecoder[A] {
    def fromJsObject(j: JsObject): String \/ A
  }
  object DerivedJsDecoder {
    def gen[A, R](
      implicit G: LabelledGeneric.Aux[A, R],
      R: Cached[Strict[DerivedJsDecoder[R]]]
    ): JsDecoder[A] = new JsDecoder[A] {
      def fromJson(j: JsValue) = j match {
        case o @ JsObject(_) => R.value.value.fromJsObject(o).map(G.from)
        case other           => fail("JsObject", other)
      }
    }

    implicit def hcons[K <: Symbol, H, T <: HList](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsDecoder[H]],
      T: DerivedJsDecoder[T]
    ): DerivedJsDecoder[FieldType[K, H] :: T] =
      new DerivedJsDecoder[FieldType[K, H] :: T] {
        private val fieldname = K.value.name
        def fromJsObject(j: JsObject) = {
          val value = j.get(fieldname).getOrElse(JsNull)
          for {
            head  <- H.value.fromJson(value)
            tail  <- T.fromJsObject(j)
          } yield field[K](head) :: tail
        }
      }

    implicit val hnil: DerivedJsDecoder[HNil] = new DerivedJsDecoder[HNil] {
      private val nil               = HNil.right[String]
      def fromJsObject(j: JsObject) = nil
    }

    implicit def ccons[K <: Symbol, H, T <: Coproduct](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsDecoder[H]],
      T: DerivedJsDecoder[T]
    ): DerivedJsDecoder[FieldType[K, H] :+: T] =
      new DerivedJsDecoder[FieldType[K, H] :+: T] {
        private val hint = ("type" -> JsString(K.value.name))
        def fromJsObject(j: JsObject) =
          if (j.fields.element(hint)) {
            j.get("xvalue")
              .into {
                case \/-(xvalue) => H.value.fromJson(xvalue)
                case -\/(_)      => H.value.fromJson(j)
              }
              .map(h => Inl(field[K](h)))
          } else
            T.fromJsObject(j).map(Inr(_))
      }

    implicit val cnil: DerivedJsDecoder[CNil] = new DerivedJsDecoder[CNil] {
      def fromJsObject(j: JsObject) = fail(s"JsObject with 'type' field", j)
    }
  }

Agregar las preferencias del usuario usando anotaciones sigue la misma ruta que en el caso de DerivedJsEncoder y es mecánico, de modo que se deja como un ejercicio al lector.

Algo más falta: valores por defecto para las case class. Podríamos solicitar evidencia, pero el gran problema es que ya no podemos usar el mismo mecanismo de derivación para los productos y coproductos: la evidencia nunca se crea para coproductos.

La solución es bastante drástica. Debemos separar nuestro DerivedJsDecoder en DerivedCoproductJsDecoder y DerivedProductJsDecoder. Nos enfocaremos en el DerivedProductJsDecoder, y mientras es que estamoos en esto usaremos un Map para encontrar más rápidamente los campos:

  sealed trait DerivedProductJsDecoder[A, R, J <: HList, D <: HList] {
    private[jsonformat] def fromJsObject(
      j: Map[String, JsValue],
      anns: J,
      defaults: D
    ): String \/ R
  }

Podemos solicitar evidencia de que existen valores por defecto con Default.Aux[A, D] y duplicar todos los métodos para lidiar con el caso donde se tiene y no se tiene un valor por defecto. Sin embargo, aquí Shapeless tiene piedad y proporciona Default.AsOptions.Aux[A, D] dejándonos manejar los valores por defecto en tiempo de ejecución.

  object DerivedProductJsDecoder {
    def gen[A, R, J <: HList, D <: HList](
      implicit G: LabelledGeneric.Aux[A, R],
      J: Annotations.Aux[json, A, J],
      D: Default.AsOptions.Aux[A, D],
      R: Cached[Strict[DerivedProductJsDecoder[A, R, J, D]]]
    ): JsDecoder[A] = new JsDecoder[A] {
      def fromJson(j: JsValue) = j match {
        case o @ JsObject(_) =>
          R.value.value.fromJsObject(o.fields.toMap, J(), D()).map(G.from)
        case other => fail("JsObject", other)
      }
    }
    ...
  }

Debemos mover los métodos .hcons y .hnil al objeto compañero de la nueva typeclass sellada, que puede manejar valores por defecto

  object DerivedProductJsDecoder {
    ...
      implicit def hnil[A]: DerivedProductJsDecoder[A, HNil, HNil, HNil] =
      new DerivedProductJsDecoder[A, HNil, HNil, HNil] {
        private val nil = HNil.right[String]
        def fromJsObject(j: StringyMap[JsValue], a: HNil, defaults: HNil) = nil
      }

    implicit def hcons[A, K <: Symbol, H, T <: HList, J <: HList, D <: HList](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsDecoder[H]],
      T: DerivedProductJsDecoder[A, T, J, D]
    ): DerivedProductJsDecoder[A, FieldType[K, H] :: T, None.type :: J, Option[H] :: D] =
      new DerivedProductJsDecoder[A, FieldType[K, H] :: T, None.type :: J, Option[H] :: D] {
        private val fieldname = K.value.name
        def fromJsObject(
          j: StringyMap[JsValue],
          anns: None.type :: J,
          defaults: Option[H] :: D
        ) =
          for {
            head <- j.get(fieldname) match {
                     case Maybe.Just(v) => H.value.fromJson(v)
                     case _ =>
                       defaults.head match {
                         case Some(default) => \/-(default)
                         case None          => H.value.fromJson(JsNull)
                       }
                   }
            tail <- T.fromJsObject(j, anns.tail, defaults.tail)
          } yield field[K](head) :: tail
      }
    ...
  }

Ahora ya no podemos usar @deriving para productos y coproductos: sólo puede haber una entrada en el archivo deriving.conf.

¡Oh! Y no olvide agregar soporte para @@

  object DerivedProductJsDecoder extends DerivedProductJsDecoder1 {
    ...
  }
  private[jsonformat] trait DerivedProductJsDecoder2 {
    this: DerivedProductJsDecoder.type =>

    implicit def hconsTagged[
      A, K <: Symbol, H, Z, T <: HList, J <: HList, D <: HList
    ](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsDecoder[H @@ Z]],
      T: DerivedProductJsDecoder[A, T, J, D]
    ): DerivedProductJsDecoder[
      A,
      FieldType[K, H @@ Z] :: T,
      None.type :: J,
      Option[H @@ Z] :: D
    ] = hcons(K, H, T)

    implicit def hconsCustomTagged[
      A, K <: Symbol, H, Z, T <: HList, J <: HList, D <: HList
    ](
      implicit
      K: Witness.Aux[K],
      H: Lazy[JsDecoder[H @@ Z]],
      T: DerivedProductJsDecoder[A, T, J, D]
    ): DerivedProductJsDecoder[
      A,
      FieldType[K, H @@ Z] :: T,
      Some[json] :: J,
      Option[H @@ Z] :: D
    ] = hconsCustomTagged(K, H, T)
  }

8.4.5 Derivaciones complicadas

Shapeless nos permite muchos más tipos de derivaciones de las que son posibles con scalaz-deriving o Magnolia. Como un ejemplo de un codificador/decodificador que es posible con Magnolia, considere este modelo XML de xmlformat

  @deriving(Equal, Show, Arbitrary)
  sealed abstract class XNode

  @deriving(Equal, Show, Arbitrary)
  final case class XTag(
    name: String,
    attrs: IList[XAttr],
    children: IList[XTag],
    body: Maybe[XString]
  )

  @deriving(Equal, Show, Arbitrary)
  final case class XAttr(name: String, value: XString)

  @deriving(Show)
  @xderiving(Equal, Monoid, Arbitrary)
  final case class XChildren(tree: IList[XTag]) extends XNode

  @deriving(Show)
  @xderiving(Equal, Semigroup, Arbitrary)
  final case class XString(text: String) extends XNode

Dada la naturaleza de XML tiene sentido que se tengan pares de codificador/decodificador separados para XChildren y contendido XString. Podríamos proporcionar una derivación para el XChildren con Shapeless pero queremos campos con uso de mayúsculas y minúsculas basado en la clase de typeclass que tengan, así como campos con Option. Podríamos incluso requerir que los campos estén anotados con su nombre codificado. Además, cuando se decodifique desearíamos tener diferentes estrategias para manejar los cuerpos de los elementos XML, que pueden tener múltiples partes, dependiendo de si su tipo tiene un Semigroup, Monoid o ninguno.

8.4.6 Ejemplo: UrlQueryWriter

De manera similar a xmlformat, nuestra aplicación drone-dynamic-agents podría beneficiarse de una derivación de typeclasses de la typeclass UrlQueryWriter, que está construida a partir de instancias de UrlEncodedWriter para cada campo de entrada. No soporta coproductos:

  @typeclass trait UrlQueryWriter[A] {
    def toUrlQuery(a: A): UrlQuery
  }
  trait DerivedUrlQueryWriter[T] extends UrlQueryWriter[T]
  object DerivedUrlQueryWriter {
    def gen[T, Repr](
      implicit
      G: LabelledGeneric.Aux[T, Repr],
      CR: Cached[Strict[DerivedUrlQueryWriter[Repr]]]
    ): UrlQueryWriter[T] = { t =>
      CR.value.value.toUrlQuery(G.to(t))
    }

    implicit val hnil: DerivedUrlQueryWriter[HNil] = { _ =>
      UrlQuery(IList.empty)
    }
    implicit def hcons[Key <: Symbol, A, Remaining <: HList](
      implicit Key: Witness.Aux[Key],
      LV: Lazy[UrlEncodedWriter[A]],
      DR: DerivedUrlQueryWriter[Remaining]
    ): DerivedUrlQueryWriter[FieldType[Key, A] :: Remaining] = {
      case head :: tail =>
        val first =
          Key.value.name -> URLDecoder.decode(LV.value.toUrlEncoded(head).value, "UTF-8")
        val rest = DR.toUrlQuery(tail)
        UrlQuery(first :: rest.params)
    }
  }

Es razonable preguntarse si estas 30 lineas en realidad son una mejora sobre las 8 lineas para las 2 instancias manuales que nuestra aplicación necesita: una decisión que debe tomarse caso por caso.

En aras de la exhaustividad, la derivación UrlEncodedWriter puede escribirse con Magnolia

  object UrlEncodedWriterMagnolia {
    type Typeclass[a] = UrlEncodedWriter[a]
    def combine[A](ctx: CaseClass[UrlEncodedWriter, A]) = a =>
      Refined.unsafeApply(ctx.parameters.map { p =>
        p.label + "=" + p.typeclass.toUrlEncoded(p.dereference(a))
      }.toList.intercalate("&"))
    def gen[A]: UrlEncodedWriter[A] = macro Magnolia.gen[A]
  }

8.4.7 El lado oscuro de la derivación

“Cuidado hay que tener de la derivación automática. Furia, miedo, agresión; el lado oscuro de la derivación son. Fluirán ellos fácilmente, rápidos serán para unirse a ti en pelea. Si alguna vez comienzas el camino oscuro, por siempre dominarán tu compilador, y consumirte logrará.”

  • un antiguo maestro de Shapeless

Además de todas las advertencias sobre la derivación automática que fueron mencionadas para Magnolia, Shapeless es mucho peor. La derivación de Shapeless completamente automática no es únicamente la causa más común de compilación lenta, también es una fuente dolorosa de errores de coherencia de typeclasses.

La derivación completamente automática ocurre cuando el def gen es implícito de modo que una invocación realizará una recursión por todas las entradas de la ADT. Debido a la forma en la que funcionan los alcances implícitos, un implicit def importado tendrá una prioridad más alta que las instancias personalizadas en los objetos compañeros, creando una fuente de incoherencia de typeclasses. Por ejemplo, considere este código si nuestro .gen fuera implícito

  import DerivedJsEncoder._

  @xderiving(JsEncoder)
  final case class Foo(s: String)
  final case class Bar(foo: Foo)

Esperaríamos que la forma codificada derivada de manera completamente automática se viera como

  {
    "foo":"hello"
  }

debido a que hemos usado xderiving para Foo. Pero podría más bien ser

  {
    "foo": {
      "s":"hello"
    }
  }

Es peor aún cuando se agregan métodos al objeto compañero de la typeclass, resultando en que la typeclass siempre se deriva en el punto de uso y los usuarios no pueden optar por no realizarla.

Fundamentalmente, cuando escriba programas genéricos, los implícitos pueden ignorarse por el compilador dependiendo del alcance, ¡resultando en que perdemos la seguridad en tiempo de compilación que era nuestra motivación para programar a nivel de tipos desde el principio!

Todo es mucho más simple en el lado claro, donde implicit se usa para typeclasses coherentes, globalmente únicas. El miedo al código repetitivo es el camino al lado oscuro. El miedo lleva a la furia. La furia lleva al odio. El odio lleva al sufrimiento.

8.5 Rendimiento

No hay bala plateada en lo que respecta a la derivación de typeclasses. Un eje que hay que considerar es el del rendimiento: tanto en tiempo de compilación como en tiempo de ejecución.

8.5.1 Tiempo de compilación

Cuando se trata de tiempos de compilación, Shapeless es un caso extremo. No es poco común ver que un proyecto pequeño expanda su tiempo de compilación de un segundo a un minuto. Para investigar temas de compilación, podemos realizar un perfilado de código con el plugin scalac-profiling

  addCompilerPlugin("ch.epfl.scala" %% "scalac-profiling" % "1.0.0")
  scalacOptions ++= Seq("-Ystatistics:typer", "-P:scalac-profiling:no-profiledb")

Este produce una salida que puede generar un gráfico de llamas.

Para el caso de una derivación de Shapeless, obtenemos una gráfica vívida

casi todo el tiempo de compilación se usa en la resolución implícita. Note que esto también incluye la compilación de las instancias de scalaz-deriving, Magnolia y manuales, pero los cómputos de Shapeless dominan.

Este es el caso cuando la derivación funciona. Si hay un problema con la derivación de Shapeless, el compilador puede quedarse en un ciclo infinito y debe ser terminado.

8.5.2 Rendimiento en tiempo de ejecución

Si ahora consideramos el rendimiento en tiempo de ejecución, la respuesta siempre es: depende.

Suponiendo que la lógica de derivación ha sido escrita de una manera eficiente, sólo es posible saber cuál es más rápida a través de la experimentación.

La librería jsonformat usa el Java Microbenchmark Harness (JMH) sobre modelos que mapean a GeoJSON, Google Maps, y Twitter, contribuidos por Andriy Plokhotnyuk. Hay tres pruebas por modelo:

  • codificar el ADT a un JsValue
  • una decodificación exitosa del mismo valor JsValue al ADT
  • una decodificación fallida de un JsValue con un error de datos

aplicada a las siguientes implementaciones:

  • Magnolia
  • Shapeless
  • manualmente escrita

con las optimizaciones equivalentes en cada una. Los resultados son en operaciones por segundo (más alto es mejor), en una poderosa computadora de escritorio, usando un único hilo:

  > jsonformat/jmh:run -i 5 -wi 5 -f1 -t1 -w1 -r1 .*encode*
  Benchmark                                 Mode  Cnt       Score      Error  Units

  GeoJSONBenchmarks.encodeMagnolia         thrpt    5   70527.223 ±  546.991  ops/s
  GeoJSONBenchmarks.encodeShapeless        thrpt    5   65925.215 ±  309.623  ops/s
  GeoJSONBenchmarks.encodeManual           thrpt    5   96435.691 ±  334.652  ops/s

  GoogleMapsAPIBenchmarks.encodeMagnolia   thrpt    5   73107.747 ±  439.803  ops/s
  GoogleMapsAPIBenchmarks.encodeShapeless  thrpt    5   53867.845 ±  510.888  ops/s
  GoogleMapsAPIBenchmarks.encodeManual     thrpt    5  127608.402 ± 1584.038  ops/s

  TwitterAPIBenchmarks.encodeMagnolia      thrpt    5  133425.164 ± 1281.331  ops/s
  TwitterAPIBenchmarks.encodeShapeless     thrpt    5   84233.065 ±  352.611  ops/s
  TwitterAPIBenchmarks.encodeManual        thrpt    5  281606.574 ± 1975.873  ops/s

Vemos que las implementaciones manuales son las que liderean la tabla, seguidas por Magnolia, y con Shapeless de un 30% a un 70% de rendimiento con respecto a las instancias manuales. Ahora para la decodificación

  > jsonformat/jmh:run -i 5 -wi 5 -f1 -t1 -w1 -r1 .*decode.*Success
  Benchmark                                        Mode  Cnt       Score      Error  Units

  GeoJSONBenchmarks.decodeMagnoliaSuccess         thrpt    5   40850.270 ±  201.457  ops/s
  GeoJSONBenchmarks.decodeShapelessSuccess        thrpt    5   41173.199 ±  373.048  ops/s
  GeoJSONBenchmarks.decodeManualSuccess           thrpt    5  110961.246 ±  468.384  ops/s

  GoogleMapsAPIBenchmarks.decodeMagnoliaSuccess   thrpt    5   44577.796 ±  457.861  ops/s
  GoogleMapsAPIBenchmarks.decodeShapelessSuccess  thrpt    5   31649.792 ±  861.169  ops/s
  GoogleMapsAPIBenchmarks.decodeManualSuccess     thrpt    5   56250.913 ±  394.105  ops/s

  TwitterAPIBenchmarks.decodeMagnoliaSuccess      thrpt    5   55868.832 ± 1106.543  ops/s
  TwitterAPIBenchmarks.decodeShapelessSuccess     thrpt    5   47711.161 ±  356.911  ops/s
  TwitterAPIBenchmarks.decodeManualSuccess        thrpt    5   71962.394 ±  465.752  ops/s

Esta es una carrera más cerrada por el segundo lugar, con Shapeless y Magnolia manteniendo el ritmo. Finalmente, la decodificación a partir de un JsValue que contiene datos inválidos (en una posición intencionalmente torpe)

  > jsonformat/jmh:run -i 5 -wi 5 -f1 -t1 -w1 -r1 .*decode.*Error
  Benchmark                                      Mode  Cnt        Score       Error  Units

  GeoJSONBenchmarks.decodeMagnoliaError         thrpt    5   981094.831 ± 11051.370  ops/s
  GeoJSONBenchmarks.decodeShapelessError        thrpt    5   816704.635 ±  9781.467  ops/s
  GeoJSONBenchmarks.decodeManualError           thrpt    5   586733.762 ±  6389.296  ops/s

  GoogleMapsAPIBenchmarks.decodeMagnoliaError   thrpt    5  1288888.446 ± 11091.080  ops/s
  GoogleMapsAPIBenchmarks.decodeShapelessError  thrpt    5  1010145.363 ±  9448.110  ops/s
  GoogleMapsAPIBenchmarks.decodeManualError     thrpt    5  1417662.720 ±  1197.283  ops/s

  TwitterAPIBenchmarks.decodeMagnoliaError      thrpt    5   128704.299 ±   832.122  ops/s
  TwitterAPIBenchmarks.decodeShapelessError     thrpt    5   109715.865 ±   826.488  ops/s
  TwitterAPIBenchmarks.decodeManualError        thrpt    5   148814.730 ±  1105.316  ops/s

Justo cuando pensamos que habíamos visto un patrón, en donde Magnolia y Shapeless ganaban la carrera cuando decodificaban datos inválidos de GeoJSON, entonces vemos que las instancias manuales ganan los retos de Google Maps y Twitter.

Deseamos incluir a scalaz-deriving en la comparación, de modo que comparamos una implementación equivalente de Equal, probada con dos valores que tienen el mismo contenido (True) y dos valores que tienen contenidos ligeramente distintos (False)

  > jsonformat/jmh:run -i 5 -wi 5 -f1 -t1 -w1 -r1 .*equal*
  Benchmark                                     Mode  Cnt        Score       Error  Units

  GeoJSONBenchmarks.equalScalazTrue            thrpt    5   276851.493 ±  1776.428  ops/s
  GeoJSONBenchmarks.equalMagnoliaTrue          thrpt    5    93106.945 ±  1051.062  ops/s
  GeoJSONBenchmarks.equalShapelessTrue         thrpt    5   266633.522 ±  4972.167  ops/s
  GeoJSONBenchmarks.equalManualTrue            thrpt    5   599219.169 ±  8331.308  ops/s

  GoogleMapsAPIBenchmarks.equalScalazTrue      thrpt    5    35442.577 ±   281.597  ops/s
  GoogleMapsAPIBenchmarks.equalMagnoliaTrue    thrpt    5    91016.557 ±   688.308  ops/s
  GoogleMapsAPIBenchmarks.equalShapelessTrue   thrpt    5   107245.505 ±   468.427  ops/s
  GoogleMapsAPIBenchmarks.equalManualTrue      thrpt    5   302247.760 ±  1927.858  ops/s

  TwitterAPIBenchmarks.equalScalazTrue         thrpt    5    99066.013 ±  1125.422  ops/s
  TwitterAPIBenchmarks.equalMagnoliaTrue       thrpt    5   236289.706 ±  3182.664  ops/s
  TwitterAPIBenchmarks.equalShapelessTrue      thrpt    5   251578.931 ±  2430.738  ops/s
  TwitterAPIBenchmarks.equalManualTrue         thrpt    5   865845.158 ±  6339.379  ops/s

Como es de esperarse, las instancias manuales están bastante lejos del resto, con Shapeless liderando las derivaciones automáticas, scalaz-deriving hace un gran esfuerzo para GeoJSON, pero se queda bastante atrás tanto en la prueba de Google Maps como en la de Twitter. Las pruebas False son más de lo mismo:

  > jsonformat/jmh:run -i 5 -wi 5 -f1 -t1 -w1 -r1 .*equal*
  Benchmark                                     Mode  Cnt        Score       Error  Units

  GeoJSONBenchmarks.equalScalazFalse           thrpt    5    89552.875 ±   821.791  ops/s
  GeoJSONBenchmarks.equalMagnoliaFalse         thrpt    5    86044.021 ±  7790.350  ops/s
  GeoJSONBenchmarks.equalShapelessFalse        thrpt    5   262979.062 ±  3310.750  ops/s
  GeoJSONBenchmarks.equalManualFalse           thrpt    5   599989.203 ± 23727.672  ops/s

  GoogleMapsAPIBenchmarks.equalScalazFalse     thrpt    5    35970.818 ±   288.609  ops/s
  GoogleMapsAPIBenchmarks.equalMagnoliaFalse   thrpt    5    82381.975 ±   625.407  ops/s
  GoogleMapsAPIBenchmarks.equalShapelessFalse  thrpt    5   110721.122 ±   579.331  ops/s
  GoogleMapsAPIBenchmarks.equalManualFalse     thrpt    5   303588.815 ±  2562.747  ops/s

  TwitterAPIBenchmarks.equalScalazFalse        thrpt    5   193930.568 ±  1176.421  ops/s
  TwitterAPIBenchmarks.equalMagnoliaFalse      thrpt    5   429764.654 ± 11944.057  ops/s
  TwitterAPIBenchmarks.equalShapelessFalse     thrpt    5   494510.588 ±  1455.647  ops/s
  TwitterAPIBenchmarks.equalManualFalse        thrpt    5  1631964.531 ± 13110.291  ops/s

El rendimiento en tiempo de ejecución de scalaz-deriving, Magnolia y Shapeless es normalmente lo suficientemente bueno. Debemos ser realistas: no estamos escribiendo aplicaciones que deban ser capaces de codificar más de 130,000 valores a JSON, por segundo, en un único núcleo, en la JVM. Si esto es un problema, probablemente deba usar C++.

Es poco probable que las instancias derivadas sean el cuello de botella de una aplicación. Incluso si lo fueran, existe la posibilidad de usar las soluciones manuales, que son más poderosas y por lo tanto más peligrosas: es fácil introducir errores de dedo, de código, o incluso regresiones de rendimiento por accidente cuando se escriben instancias manuales.

En conclusión: las derivaciones y las macros no son rivales para una instancia manual bien escrita.

8.6 Resumen

Cuando esté decidiendo en una tecnología para usar con derivación de typeclasses, la siguiente gráfica puede ayudar

Feature Scalaz Magnolia Shapeless Manual
@deriving  
Leyes      
Compilación rápida  
Nombres de campos    
Anotaciones   parcialmente  
Valores por defecto   con advertencias  
Complicada     dolorosamente  
Rendimiento       no tengo rival

Prefiera scalaz-deriving de ser posible, usando Magnolia para codificadores/decodificadores o si el rendimiento es algo que deba considerar, usando Shapeless únicamente si los tiempos de compilación no son una preocupación.

Las instancias manuales siempre son una salida de emergencia para casos especiales y para conseguir el mayor rendimiento. Evite introducir errores de dedo al usar una herramienta de generación de código.

9. Alambrando la aplicación

Para finalizar, aplicaremos lo que hemos aprendido para alambrar la aplicación de ejemplo, e implementaremos un cliente y servidor HTTP usando la librería de PF pura http4s.

El código fuente de la aplicación drone-dynamic-agents está disponible junto con el código fuente del libro en https://github.com/fommil/fpmortals/ bajo el folder examples. No es necesario estar en una computadora para leer este capítulo, pero muchos lectores preferirán explorar el código fuente además de leer este texto.

Algunas partes de la aplicación se han dejado sin implementar, como ejercicios para el lector. Vea el README para más instrucciones.

9.1 Visión general

Nuestra aplicación principal únicamente requiere de una implementación del álgebra DynAgents.

  trait DynAgents[F[_]] {
    def initial: F[WorldView]
    def update(old: WorldView): F[WorldView]
    def act(world: WorldView): F[WorldView]
  }

Ya tenemos una implementación, DynAgentsModule, que requiere implementaciones de las álgebras Drone y Machines, que requieren álgebras JsonClient, LocalClock y OAuth2, etc., etc.

Es valioso tener una visión completa de todas las álgebras, módulos e intérpretes de una aplicación. Esta es la estructura del código fuente:

  ├── dda
  │   ├── algebra.scala
  │   ├── DynAgents.scala
  │   ├── main.scala
  │   └── interpreters
  │       ├── DroneModule.scala
  │       └── GoogleMachinesModule.scala
  ├── http
  │   ├── JsonClient.scala
  │   ├── OAuth2JsonClient.scala
  │   ├── encoding
  │   │   ├── UrlEncoded.scala
  │   │   ├── UrlEncodedWriter.scala
  │   │   ├── UrlQuery.scala
  │   │   └── UrlQueryWriter.scala
  │   ├── oauth2
  │   │   ├── Access.scala
  │   │   ├── Auth.scala
  │   │   ├── Refresh.scala
  │   │   └── interpreters
  │   │       └── BlazeUserInteraction.scala
  │   └── interpreters
  │       └── BlazeJsonClient.scala
  ├── os
  │   └── Browser.scala
  └── time
      ├── Epoch.scala
      ├── LocalClock.scala
      └── Sleep.scala

Las firmas de todas las álgebras pueden resumirse como

  trait Sleep[F[_]] {
    def sleep(time: FiniteDuration): F[Unit]
  }

  trait LocalClock[F[_]] {
    def now: F[Epoch]
  }

  trait JsonClient[F[_]] {
    def get[A: JsDecoder](
      uri: String Refined Url,
      headers: IList[(String, String)]
    ): F[A]

    def post[P: UrlEncodedWriter, A: JsDecoder](
      uri: String Refined Url,
      payload: P,
      headers: IList[(String, String)]
    ): F[A]
  }

  trait Auth[F[_]] {
    def authenticate: F[CodeToken]
  }
  trait Access[F[_]] {
    def access(code: CodeToken): F[(RefreshToken, BearerToken)]
  }
  trait Refresh[F[_]] {
    def bearer(refresh: RefreshToken): F[BearerToken]
  }
  trait OAuth2JsonClient[F[_]] {
    // same methods as JsonClient, but doing OAuth2 transparently
  }

  trait UserInteraction[F[_]] {
    def start: F[String Refined Url]
    def open(uri: String Refined Url): F[Unit]
    def stop: F[CodeToken]
  }

  trait Drone[F[_]] {
    def getBacklog: F[Int]
    def getAgents: F[Int]
  }

  trait Machines[F[_]] {
    def getTime: F[Epoch]
    def getManaged: F[NonEmptyList[MachineNode]]
    def getAlive: F[MachineNode ==>> Epoch]
    def start(node: MachineNode): F[Unit]
    def stop(node: MachineNode): F[Unit]
  }

Note que algunas firmas de capítulos previos han sido refactorizadas para usar los tipos de datos de Scalaz, ahora que sabemos porqué son superiores a las alternativas en la librería estándar.

Los tipos de datos son:

  @xderiving(Order, Arbitrary)
  final case class Epoch(millis: Long) extends AnyVal

  @deriving(Order, Show)
  final case class MachineNode(id: String)

  @deriving(Equal, Show)
  final case class CodeToken(token: String, redirect_uri: String Refined Url)

  @xderiving(Equal, Show, ConfigReader)
  final case class RefreshToken(token: String) extends AnyVal

  @deriving(Equal, Show, ConfigReader)
  final case class BearerToken(token: String, expires: Epoch)

  @deriving(ConfigReader)
  final case class OAuth2Config(token: RefreshToken, server: ServerConfig)

  @deriving(ConfigReader)
  final case class AppConfig(drone: BearerToken, machines: OAuth2Config)

  @xderiving(UrlEncodedWriter)
  final case class UrlQuery(params: IList[(String, String)]) extends AnyVal

y las typeclasses son

  @typeclass trait UrlEncodedWriter[A] {
    def toUrlEncoded(a: A): String Refined UrlEncoded
  }
  @typeclass trait UrlQueryWriter[A] {
    def toUrlQuery(a: A): UrlQuery
  }

Derivamos typeclasses útiles usando scalaz-deriving y Magnolia. La typeclass ConfigReader es de la librería pureconfig y se usa para leer la configuración en tiempo de ejecución a partir de archivos de propiedades HOCON.

Y sin entrar en detalles sobre cómo implementar las álgebras, necesitamos conocer el grafo de dependencias de nuestro DynAgentsModule.

  final class DynAgentsModule[F[_]: Applicative](
    D: Drone[F],
    M: Machines[F]
  ) extends DynAgents[F] { ... }

  final class DroneModule[F[_]](
    H: OAuth2JsonClient[F]
  ) extends Drone[F] { ... }

  final class GoogleMachinesModule[F[_]](
    H: OAuth2JsonClient[F]
  ) extends Machines[F] { ... }

Existen dos módulos que implementan OAuth2JsonClient, uno que usará el álgebra OAuth2 Refresh (para Google) y otra que reutilice un BearerToken que no expira (para Drone).

  final class OAuth2JsonClientModule[F[_]](
    token: RefreshToken
  )(
    H: JsonClient[F],
    T: LocalClock[F],
    A: Refresh[F]
  )(
    implicit F: MonadState[F, BearerToken]
  ) extends OAuth2JsonClient[F] { ... }

  final class BearerJsonClientModule[F[_]: Monad](
    bearer: BearerToken
  )(
    H: JsonClient[F]
  ) extends OAuth2JsonClient[F] { ... }

Hasta el momento hemos visto requerimientos para F que tienen un Applicative[F], Monad[F] y MonadState[F, BearerToken]. Todos estos requerimientos pueden satisfacerse usando StateT[Task, BearerToken, ?] como nuestro contexto de la aplicación.

Sin embargo, algunas de nuestras álgebras tienen solo un intérprete, usando Task

  final class LocalClockTask extends LocalClock[Task] { ... }
  final class SleepTask extends Sleep[Task] { ... }

Pero recuerde que nuestras álgebras pueden proporcionar un liftM en su objeto compañero, vea el Capítulo 7.4 sobre la Librería de Transformadores de Mónadas, permitiéndonos elevar un LocalClock[Task] en nuestro contexto deseado StateT[Task, BearerToken, ?], y todo es consistente.

Tristemente, ese no es el final de la historia. Las cosas se vuelven más complicadas cuando vamos a la siguiente capa. Nuestro JsonClient tiene un intérprete que usa un contexto distinto

  final class BlazeJsonClient[F[_]](H: Client[Task])(
    implicit
    F: MonadError[F, JsonClient.Error],
    I: MonadIO[F, Throwable]
  ) extends JsonClient[F] { ... }
  object BlazeJsonClient {
    def apply[F[_]](
      implicit
      F: MonadError[F, JsonClient.Error],
      I: MonadIO[F, Throwable]
    ): Task[JsonClient[F]] = ...
  }

Note que el constructor BlazeJsonClient devuelve un Task[JsonClient[F]], no un JsonClient[F]. Esto es porque el acto de creación del cliente es un efecto: se crean pools de conexiones mutables y se manejan internamente por https.

No debemos olvidar que tenemos que proporcionar un RefreshToken para GoogleMachinesModule. Podríamos solicitar al usuario hacer todo el trabajo mecánico, pero somos buenas personas y proveemos una aplicación que usa las álgebras Auth y Access. LAs implementaciones AuthModule y AccessModule traen consigo dependencias adicionales, pero felizmente no cambian al contexto de la aplicación F[_].

  final class AuthModule[F[_]: Monad](
    config: ServerConfig
  )(
    I: UserInteraction[F]
  ) extends Auth[F] { ... }

  final class AccessModule[F[_]: Monad](
    config: ServerConfig
  )(
    H: JsonClient[F],
    T: LocalClock[F]
  ) extends Access[F] { ... }

  final class BlazeUserInteraction private (
    pserver: Promise[Void, Server[Task]],
    ptoken: Promise[Void, String]
  ) extends UserInteraction[Task] { ... }
  object BlazeUserInteraction {
    def apply(): Task[BlazeUserInteraction] = ...
  }

El intérprete para UserInteraction es la parte más compleja de nuestro código: inicia un servidor HTTP, envía al usuario a visitar una página web en su navegador, captura un callback en el servidor, y entonces devuelve el resultado mientras que apaga el servidor web de manera segura.

Más bien que usar un StateT para administrar este estado, usaremos la primitiva Promise (de ioeffect). Siempre usaremos Promise (o IORef) en lugar de un StateT cuando estemos escribiendo un intérprete IO dado que nos permite contener la abstracción. Si fuéramos a usar un StateT, no sólo tendría un impacto en el rendimiento de la aplicación completa, sino que también habría una fuga de manejo de stado interno en la aplicación principal, que sería responsable de proporcionar el valor inicial. Tampoco podríamos usar StateT en este escenario debido a que requerimos semántica de “espera” que sólo es proporcionada por Promise.

9.2 Main

La parte más fea de la PF es asegurarse de que las mónadas estén alineadas y eso tiende a pasar en el punto de entrada Main.

Nuestro ciclo principal es

  state = initial()
  while True:
    state = update(state)
    state = act(state)

y las buenas noticias es que el código real se verá como

  for {
    old     <- F.get
    updated <- A.update(old)
    changed <- A.act(updated)
    _       <- F.put(changed)
    _       <- S.sleep(10.seconds)
  } yield ()

donde F mantiene el estado del mundo en una MonadState[F, WorldView]. Podemos poner esto en un método que se llame .step y repetirlo por siempre al invocar .step[F].forever[Unit].

Hay dos enfoques que podemos tomar, y exploraremos ambos. El primero, y el más simple, es construir una pila de mónadas con la que todas las mónadas sean compatibles. A todo se le agregaría un .liftM para elevarlo a la pila más general.

El código que deseamos escribir para el modo de autenticación única es

  def auth(name: String): Task[Unit] = {
    for {
      config    <- readConfig[ServerConfig](name + ".server")
      ui        <- BlazeUserInteraction()
      auth      = new AuthModule(config)(ui)
      codetoken <- auth.authenticate
      client    <- BlazeJsonClient
      clock     = new LocalClockTask
      access    = new AccessModule(config)(client, clock)
      token     <- access.access(codetoken)
      _         <- putStrLn(s"got token: $token")
    } yield ()
  }.run

donde .readConfig y .putStrLn son invocaciones a librerías. Podemos pensar de ellas como intérpretes Task de álgebras que leen la configuración de tiempo de ejecución de la aplicación e imprimen una cadena a la pantalla.

Pero este código no compila, por dos razones. Primero, necesitamos considerar lo que será nuestra pila de mónadas. El constructor BlazeJsonClient devuelve un Task pero los métodos JsonClient requieren un MonadError[..., JsonClient.Error]. Este puede ser provisto por EitherT. Por lo tanto podemos construir la pila de mónadas co,unes para la entera comprensión for`como

  type H[a] = EitherT[Task, JsonClient.Error, a]

Tristemente esto significa que debemos llamar .liftM sobre todo lo que devuelva un Task, lo que agrega mucho código repetitivo y verboso. Además, el método .liftM no toma un tipo de la forma H[_], sino un tipo de la forma H[_[_], _], de modo que tenemos que crear un alias de tipo para ayudar al compilador:

  type HT[f[_], a] = EitherT[f, JsonClient.Error, a]
  type H[a]        = HT[Task, a]

ahora podemos llamar .liftM[HT] cuando recibimos un Task

  for {
    config    <- readConfig[ServerConfig](name + ".server").liftM[HT]
    ui        <- BlazeUserInteraction().liftM[HT]
    auth      = new AuthModule(config)(ui)
    codetoken <- auth.authenticate.liftM[HT]
    client    <- BlazeJsonClient[H].liftM[HT]
    clock     = new LocalClockTask
    access    = new AccessModule(config)(client, clock)
    token     <- access.access(codetoken)
    _         <- putStrLn(s"got token: $token").liftM[HT]
  } yield ()

Pero esto todavía no compila, debido a que clock es un LocalClock[Task] y AccessModule requiere un LocalClock[H]. Entonces agregamos el código verboso .liftM necesario al objeto compañero de LocalClock y entonces podemos elevar el álgebra completa

  clock     = LocalClock.liftM[Task, HT](new LocalClockTask)

¡y ahora todo compila!

El segundo enfoque para alambrar la aplicación completa es más compleja, pero es necesaria cuando existen conflictos en la pila de la mónada, tales como necesitamos en nuestro ciclo principal. Si realizamos un análisis encontramos que son necesarias las siguientes:

  • MonadError[F, JsonClient.Error] para usos del JsonClient
  • MonadState[F, BearerToken] para usos del OAuth2JsonClient
  • MonadState[F, WorldView] para nuestro ciclo principal

Tristemente, los dos requerimientos de MonadState están en conflicto. Podríamos construir un tipo de datos que capture todo el estado del programa, pero entonces tendríamos una abstracción defectuosa. En lugar de esto, anidamos nuestras comprensiones for y proporcionamos estado donde sea necesario.

Ahora tenemos que pensar sobre tres capas, que llamaremos F, G, H

  type HT[f[_], a] = EitherT[f, JsonClient.Error, a]
  type GT[f[_], a] = StateT[f, BearerToken, a]
  type FT[f[_], a] = StateT[f, WorldView, a]

  type H[a]        = HT[Task, a]
  type G[a]        = GT[H, a]
  type F[a]        = FT[G, a]

Ahora tenemos algunas malas noticias sobre .liftM… funciona solamente una capa a la vez. Si tenemos un Task[A] y deseamos un F[A], debemos ir a través de cada paso y teclear ta.liftM[HT].liftM[GT].liftM[FT]. De manera similar, cuando elevamos álgebras tenemos que invocar liftM múltiples veces. Para tener un Sleep[F], tenemos que teclear

  val S: Sleep[F] = {
    import Sleep.liftM
    liftM(liftM(liftM(new SleepTask)))
  }

y para obtener un LocalClock[G] tenemos que hacer dos elevaciones

  val T: LocalClock[G] = {
    import LocalClock.liftM
    liftM(liftM(new LocalClockTask))
  }

La aplicación principal se vuelve”

  def agents(bearer: BearerToken): Task[Unit] = {
    ...
    for {
      config <- readConfig[AppConfig]
      blaze  <- BlazeJsonClient[G]
      _ <- {
        val bearerClient = new BearerJsonClientModule(bearer)(blaze)
        val drone        = new DroneModule(bearerClient)
        val refresh      = new RefreshModule(config.machines.server)(blaze, T)
        val oauthClient =
          new OAuth2JsonClientModule(config.machines.token)(blaze, T, refresh)
        val machines = new GoogleMachinesModule(oauthClient)
        val agents   = new DynAgentsModule(drone, machines)
        for {
          start <- agents.initial
          _ <- {
            val fagents = DynAgents.liftM[G, FT](agents)
            step(fagents, S).forever[Unit]
          }.run(start)
        } yield ()
      }.eval(bearer).run
    } yield ()
  }

donde el ciclo exterior está usando Task, el ciclo de enmedio está usando G, y el ciclo interno está usando F.

Las llamadas a .run(start) y .eval(bearer) son donde proporcionamos el estado para las partes StateT de nuestra aplicación. El .run es para revelar el error EitherT.

Podemos invocar estos dos puntos de entrada para nuestra SafeApp

  object Main extends SafeApp {
    def run(args: List[String]): IO[Void, ExitStatus] = {
      if (args.contains("--machines")) auth("machines")
      else agents(BearerToken("<invalid>", Epoch(0)))
    }.attempt[Void].map {
      case \/-(_)   => ExitStatus.ExitNow(0)
      case -\/(err) => ExitStatus.ExitNow(1)
    }
  }

¡y entonces ejecutarla!

  > runMain fommil.dda.Main --machines
  [info] Running (fork) fommil.dda.Main --machines
  ...
  [info] Service bound to address /127.0.0.1:46687
  ...
  [info] Created new window in existing browser session.
  ...
  [info] Headers(Host: localhost:46687, Connection: keep-alive, User-Agent: Mozilla/5.0 ...)
  ...
  [info] POST https://www.googleapis.com/oauth2/v4/token
  ...
  [info] got token: "<elided>"

¡Yay!

9.3 Blaze

Implementaremos el cliente y el servidor HTTP con la librería de terceros http4s. Los intérpretes para sus álgebras de cliente y servidor se llaman Blaze.

Necesitamos las siguientes dependencias

  val http4sVersion = "0.18.16"
  libraryDependencies ++= Seq(
    "org.http4s"            %% "http4s-dsl"          % http4sVersion,
    "org.http4s"            %% "http4s-blaze-server" % http4sVersion,
    "org.http4s"            %% "http4s-blaze-client" % http4sVersion
  )

9.3.1 BlazeJsonClient

Necesitaremos algunos imports

  import org.http4s
  import org.http4s.{ EntityEncoder, MediaType }
  import org.http4s.headers.`Content-Type`
  import org.http4s.client.Client
  import org.http4s.client.blaze.{ BlazeClientConfig, Http1Client }

El módulo Client puede resumirse como

  final class Client[F[_]](
    val shutdown: F[Unit]
  )(implicit F: MonadError[F, Throwable]) {
    def fetch[A](req: Request[F])(f: Response[F] => F[A]): F[A] = ...
    ...
  }

donde Request y Responseson los tipos de datos:

  final case class Request[F[_]](
    method: Method
    uri: Uri,
    headers: Headers,
    body: EntityBody[F]
  ) {
    def withBody[A](a: A)
                   (implicit F: Monad[F], A: EntityEncoder[F, A]): F[Request[F]] = ...
    ...
  }

  final case class Response[F[_]](
    status: Status,
    headers: Headers,
    body: EntityBody[F]
  )

construidos a partir de

  final case class Headers(headers: List[Header])
  final case class Header(name: String, value: String)

  final case class Uri( ... )
  object Uri {
    // not total, only use if `s` is guaranteed to be a URL
    def unsafeFromString(s: String): Uri = ...
    ...
  }

  final case class Status(code: Int) {
    def isSuccess: Boolean = ...
    ...
  }

  type EntityBody[F[_]] = fs2.Stream[F, Byte]

El tipo EntityBody es un alias para Stream de la librería fs2(https://github.com/functional-streams-for-scala/fs2). El tipo de datos Stream puede pensarse como un torrente de datos con efectos, perezoso, y basado en tracción (pull-based). Está implementado como una mónada Free con capacidades para atrapar excepciones e interrupciones. Stream toma dos parámetros de tipo: un tipo de efecto y un tipo de contenido, y tiene una representación interna eficiente para hacer un procesamiento por lotes. Por ejemplo, aunque estemos usando Stream[F, Byte], en realidad está envolviendo un Array[Byte] desnudo que llega sobre la red.

Tenemos que convertir nuestras representaciones para el encabezado y URL en las versiones requeridas por https4s:

  def convert(headers: IList[(String, String)]): http4s.Headers =
    http4s.Headers(
      headers.foldRight(List[http4s.Header]()) {
        case ((key, value), acc) => http4s.Header(key, value) :: acc
      }
    )

  def convert(uri: String Refined Url): http4s.Uri =
    http4s.Uri.unsafeFromString(uri.value) // we already validated our String

Tanto nuestro método .get como .post requieren una conversión del tipo Response de http4s a una A. Podemos refactorizar esto en una única función, .handler

  import JsonClient.Error

  final class BlazeJsonClient[F[_]] private (H: Client[Task])(
    implicit
    F: MonadError[F, Error],
    I: MonadIO[F, Throwable]
  ) extends JsonClient[F] {
    ...
    def handler[A: JsDecoder](resp: http4s.Response[Task]): Task[Error \/ A] = {
      if (!resp.status.isSuccess)
        Task.now(JsonClient.ServerError(resp.status.code).left)
      else
        for {
          text <- resp.body.through(fs2.text.utf8Decode).compile.foldMonoid
          res = JsParser(text)
            .flatMap(_.as[A])
            .leftMap(JsonClient.DecodingError(_))
        } yield res
    }
  }

El .through(fs2.text.utf8decode) es para convertir un Stream[Task, Byte] en un Stream[Task, String], con .compile.foldMonoid interpretándolo con nuestra Task y combinando todas las partes usando el Monoid[String], dándonos un Task[String].

Entonces analizamos gramaticalmente la cadena como JSON y usamos el JsDecoder[A] para crear la salida requerida.

Esta es nuestra implementación de .get

  def get[A: JsDecoder](
    uri: String Refined Url,
    headers: IList[(String, String)]
  ): F[A] =
    I.liftIO(
        H.fetch(
          http4s.Request[Task](
            uri = convert(uri),
            headers = convert(headers)
          )
        )(handler[A])
      )
      .emap(identity)

.get es totalmente código para conectar partes: convertimos nuestros tipos de entrada en http4s.Request, entonces invocamos .fetch sobre el Client con nuestro handler. Esto nos da un Task[Error \/ A], pero tenemos que devolver un F[A]. Por lo tanto usamos el método MonadIO.liftIO para crear un F[Error \/ A] y entonces usamos .emap para poner el error dentro de la F.

Tristemente, la compilación de este código fallaría si lo intentamos. El error se vería como

  [error] BlazeJsonClient.scala:95:64: could not find implicit value for parameter
  [error]  F: cats.effect.Sync[scalaz.ioeffect.Task]

Básicamente, es porque faltan dependencias del ecosistema de cats.

La razón de esta falla es que la librería http4s está usando una librería de programación FP diferente, no Scalaz. Felizmente, scalaz-ioeffect proporciona una capa de compatibilidad y el proyecto shims proporciona conversiones implícitas transparentes (hasta que no lo son). Podemos lograr que nuestro código compile con las siguientes dependencias:

  libraryDependencies ++= Seq(
    "com.codecommit" %% "shims"                % "1.4.0",
    "org.scalaz"     %% "scalaz-ioeffect-cats" % "2.10.1"
  )

y estos imports

  import shims._
  import scalaz.ioeffect.catz._

La implementación de .post es similar pero también debemos proporcionar una instancia de

  EntityEncoder[Task, String Refined UrlEncoded]

Felizmente, la typeclass EntityEncoder proporciona conveniencias que nos permiten derivar una a partir de un codificador existente String

  implicit val encoder: EntityEncoder[Task, String Refined UrlEncoded] =
    EntityEncoder[Task, String]
      .contramap[String Refined UrlEncoded](_.value)
      .withContentType(
        `Content-Type`(MediaType.`application/x-www-form-urlencoded`)
      )

La única diferencia entre .get y .post es la forma en que construimos http4s.Request

  http4s.Request[Task](
    method = http4s.Method.POST,
    uri = convert(uri),
    headers = convert(headers)
  )
  .withBody(payload.toUrlEncoded)

y la pieza final es el constructor, que es un caso de invocar HttpClient con un objeto de configuración

  object BlazeJsonClient {
    def apply[F[_]](
      implicit
      F: MonadError[F, JsonClient.Error],
      I: MonadIO[F, Throwable]
    ): Task[JsonClient[F]] =
      Http1Client(BlazeClientConfig.defaultConfig).map(new BlazeJsonClient(_))
  }

9.3.2 BlazeUserInteraction

Necesitamos iniciar un servidor HTTP, que es mucho más sencillo de lo que suena. Primero, los imports

  import org.http4s._
  import org.http4s.dsl._
  import org.http4s.server.Server
  import org.http4s.server.blaze._

Necesitamos crear una dsl para nuestro tipo de efecto, que entonces importaremos

  private val dsl = new Http4sDsl[Task] {}
  import dsl._

Ahora podemos usar la dsl de http4s para crear endpoints HTTP. Más bien que describir todo lo que puede hacerse, simplemente implementaremos el endpoint que sea similar a cualquier otro DLS de HTTP

  private object Code extends QueryParamDecoderMatcher[String]("code")
  private val service: HttpService[Task] = HttpService[Task] {
    case GET -> Root :? Code(code) => ...
  }

El tipo de retorno de cada empate de patrones es un Task[Response[Task]. En nuestra implementación deseamos tomar el code y ponerlo en una promesa ptoken:

  final class BlazeUserInteraction private (
    pserver: Promise[Throwable, Server[Task]],
    ptoken: Promise[Throwable, String]
  ) extends UserInteraction[Task] {
    ...
    private val service: HttpService[Task] = HttpService[Task] {
      case GET -> Root :? Code(code) =>
        ptoken.complete(code) >> Ok(
          "That seems to have worked, go back to the console."
        )
    }
    ...
  }

pero la definición de nuestras rutas de servicios no es suficiente, requerimos arrancar un servidor, lo que hacemos con BlazeBuilder

  private val launch: Task[Server[Task]] =
    BlazeBuilder[Task].bindHttp(0, "localhost").mountService(service, "/").start

Haciendo un enlace con el puerto 0 hace que el sistema operativo asigne un puerto efímero. Podemos descubrir en qué puerto está ejecutándose en realidad al consultar el campo server.address.

Nuestra implementación de los métodos .start y .stop ahora es directa

  def start: Task[String Refined Url] =
    for {
      server  <- launch
      updated <- pserver.complete(server)
      _ <- if (updated) Task.unit
           else server.shutdown *> fail("server was already running")
    } yield mkUrl(server)

  def stop: Task[CodeToken] =
    for {
      server <- pserver.get
      token  <- ptoken.get
      _      <- IO.sleep(1.second) *> server.shutdown
    } yield CodeToken(token, mkUrl(server))

  private def mkUrl(s: Server[Task]): String Refined Url = {
    val port = s.address.getPort
    Refined.unsafeApply(s"http://localhost:${port}/")
  }
  private def fail[A](s: String): String =
    Task.fail(new IOException(s) with NoStackTrace)

El tiempo de 1.second ocupado en dormir es necesario para evitar apagar el servidor antes de que la respuesta se envío de regreso al navegador. ¡La IO no pierde el tiempo cuando se trata de rendimiento concurrente!

Finalmente, para crear un BlazeUserInteraction, únicamente requerimos las dos promesas sin inicializar

  object BlazeUserInteraction {
    def apply(): Task[BlazeUserInteraction] = {
      for {
        p1 <- Promise.make[Void, Server[Task]].widenError[Throwable]
        p2 <- Promise.make[Void, String].widenError[Throwable]
      } yield new BlazeUserInteraction(p1, p2)
    }
  }

Podríamos haber usado IO[Void, ?] en vez de esto, pero dado que el resto de nuestra aplicación está usando Task (es decir, IO[Throwable, ?]), invocamos .widenErrorpara evitar introducir cualquier cantidad de código verboso que nos podría distraer.

9.4 Gracias

¡Y eso es todo! Felicidades por llegar al final.

Si usted aprendió algo de este libro, por favor diga a sus amigos. Este libro no tiene un departamento de marketing, de modo que sus palabras son necesarias para dar a conocerlo.

Involúcrese en Scalaz al unirse al cuarto de charla de gitter. Ahí puede solicitar consejo, ayudar a los nuevos (ahora usted es un experto), y contribuir al siguiente release.

Tabla de typeclasses

Typeclass Método Desde Dado Hacia
InvariantFunctor xmap F[A] A => B, B => A F[B]
Contravariant contramap F[A] B => A F[B]
Functor map F[A] A => B F[B]
Apply ap / <*> F[A] F[A => B] F[B]
  apply2 F[A], F[B] (A, B) => C F[C]
Alt altly2 F[A], F[B] (A \/ B) => C F[C]
Divide divide2 F[A], F[B] C => (A, B) F[C]
Decidable choose2 F[A], F[B] C => (A \/ B) F[C]
Bind bind / >>= F[A] A => F[B] F[B]
  join F[F[A]]   F[A]
Cobind cobind F[A] F[A] => B F[B]
  cojoin F[A]   F[F[A]]
Applicative point A   F[A]
Divisible conquer     F[A]
Comonad copoint F[A]   A
Semigroup append A, A   A
Plus plus / <+> F[A], F[A]   F[A]
MonadPlus withFilter F[A] A => Boolean F[A]
Align align F[A], F[B]   F[A \&/ B]
  merge F[A], F[A]   F[A]
Zip zip F[A], F[B]   F[(A, B)]
Unzip unzip F[(A, B)]   (F[A], F[B])
Cozip cozip F[A \/ B]   F[A] \/ F[B]
Foldable foldMap F[A] A => B B
  foldMapM F[A] A => G[B] G[B]
Traverse traverse F[A] A => G[B] G[F[B]]
  sequence F[G[A]]   G[F[A]]
Equal equal / === A, A   Boolean
Show shows A   String
Bifunctor bimap F[A, B] A => C, B => D F[C, D]
  leftMap F[A, B] A => C F[C, B]
  rightMap F[A, B] B => C F[A, C]
Bifoldable bifoldMap F[A, B] A => C, B => C C
(con MonadPlus) separate F[G[A, B]]   (F[A], F[B])
Bitraverse bitraverse F[A, B] A => G[C], B => G[D] G[F[C, D]]
  bisequence F[G[A], G[B]]   G[F[A, B]]

Haskell

La documentación de Scalaz con frecuencia cita de librerías o artículos escritos para el lenguaje de programación Haskell. En este breve capítulo, aprenderemos suficiente Haskell para ser capaces de entender el material original, y para asistir a charlas de Haskell en conferencias de programación funcional.

Data

Haskell tiene una sintaxis muy limpia para las ADTs. Esta es una estructura de lista ligada.

  data List a = Nil | Cons a (List a)

List es un constructor de tipos, a es el parámetro de tipo, | separa los constructores de datos, que son: Nil la lista vacía y una celda Cons. Cons toma dos parámetros, que están separados por espacios en blanco: sin comas ni paréntesis para los parámetros.

No existen las subclases en Haskell, de modo que no hay tal cosa como el tipo Nil o el tipo Cons: ambos construyen una List.

Una (posible) traducción a Scala sería:

  sealed abstract class List[A]
  object Nil {
    def apply[A]: List[A] = ...
    def unapply[A](as: List[A]): Option[Unit] = ...
  }
  object Cons {
    def apply[A](head: A, tail: List[A]): List[A] = ...
    def unapply[A](as: List[A]): Option[(A, List[A])] = ...
  }

Es decir, el constructor de tipo es algo como sealed abstract class, y cada constructor de datos es .apply / .unapply. Note que Scala no realiza un emparejamiento de patrones exhaustivo sobre esta codificación, por lo que Scalaz no lo usa.

Podemos usar notación infija: una definición más agradable podría usar el símbolo :. en lugar de Cons

  data List t = Nil | t :. List t
  infixr 5 :.

donde especificamos una fijeza (fixity), que puede ser

  • infix: sin asociatividad
  • infixl: asociatividad a la izquierda
  • infixr: asociatividad a la derecha

Un número de 0 (baja) a 9 (alta) especifica la precedencia. Ahora podemos crear una lista de enteros al teclear

  1 :. 2 :. Nil

Haskell ya tiene una estructura de lista ligada, que tan fundamental en la programación funcional que se le ha asignado una sintaxis con corchetes cuadrados [a]

  data [] a = [] | a : [a]
  infixr 5 :

y un constructor conveniente de valores multi-argumentos: [1, 2, 3] en lugar de 1 : 2 : 3 : []

Al final nuestras ADTs tendrán que almacenar valores primitivos. Los tipos de datos primitivos más comunes son:

  • Char: un caracter unicode
  • Text: para bloques de texto unicode
  • Int: un entero con signo de precisión fija, dependiente de la máquina
  • Word: un Int sin signo, y de tamaños fijos Word8 / Word16 / Word32 / Word64
  • Float / Double: Números de precisión IEEE sencilla y doble
  • Integer / Natural: enteros de precisión arbitraria con y sin signo, respectivamente
  • (,): tuplas, desde 0 (también conocido como unit) hasta 62 campos
  • IO inspiración para la IO de Scalaz, implementada para el entorno de ejecución

con menciones honoríficas para

  data Bool       = True | False
  data Maybe a    = Nothing | Just a
  data Either a b = Left a  | Right b
  data Ordering   = LT | EQ | GT

Como Scala, Haskell tiene aliases de tipo: un alias o su forma expandida pueden ser usados de forma intercambiable. Por razones históricas, String está definido como una lista ligada de Char

type String = [Char]

que es muy ineficiente y siempre desearemos usar Text más bien.

Finalmente, es posible definir nombres de campos en las ADTs usando sintaxis de registros, lo que significa que podemos crear los constructores de datos con llaves y usar anotaciones de tipo con dos puntos dobles para indicar los tipos

  -- ADT desnuda
  data Resource = Human Int String
  data Company  = Company String [Resource]

  -- Usando la sintaxis de registros
  data Resource = Human
                  { serial    :: Int
                  , humanName :: String
                  }
  data Company  = Company
                  { companyName :: String
                  , employees   :: [Resource]
                  }

Note que el constructor de datos Human y el tipo Resource no tienen el mismo nombre. La sintaxis de registro genera el equivalente a un método para acceder a los campos y un método para hacer copias

  -- construct
  adam = Human 0 Adam
  -- field access
  serial adam
  -- copy
  eve = adam { humanName = "Eve" }

Una alternativa más eficiente que las definiciones data de un único campo es usar un newtype, que no tiene costo adicional en tiempo de ejecución:

  newtype Alpha = Alpha { underlying :: Double }

equivalente a extends AnyVal pero sin sus problemas.

Funciones

Aunque no es necesario, es una buena práctica escribir explícitamente la firma de una función: su nombre seguido de su tipo. Por ejemplo fold especializada para una lista ligada

  foldl :: (b -> a -> b) -> b -> [a] -> b

Todas las funciones usan currying en Haskell, cada parámetro está separado por una -> y el tipo final es el tipo de retorno. La firma anterior es equivalente a la siguiente firma de Scala:

  def foldLeft[A, B](f: (B, A) => B)(b: B)(as: List[A]): B

Algunas observaciones:

  • no se ocupan palabras reservadas
  • no hay necesidad de declarar los tipos que se introducen
  • no hay necesidad de nombrar los parámetros

lo que resulta en código conciso.

Las funciones infijas están definidas en paréntesis y requieren de una definición de fijeza:

  (++) :: [a] -> [a] -> [a]
  infixr 5 ++

Las funciones normales pueden invocarse usando notación infija al rodear su nombre con comillas. Las dos formas siguientes son equivalentes:

  a `foo` b
  foo a b

Una función infija puede invocarse como una función normal si ponemos su nombre entre paréntesis, y puede aplicarse currying ya sea por la izquierda o por la derecha, con frecuencia resultando en semántica distinta:

  invert = (1.0 /)
  half   = (/ 2.0)

Las funciones con frecuencia se escriben con el parámetro más general al principio, para habilitar un máximo reutilización de las formas que usan currying.

La definición de una función puede usar emparejamiento de patrones, con una linea por caso. Ahí es posible nombrar los parámetros usando los constructores de datos para extraer los parámetros, de manera muy similar a una cláusula case de Scala:

  fmap :: (a -> b) -> Maybe a -> Maybe b
  fmap f (Just a) = Just (f a)
  fmap _ Nothing  = Nothing

Los guiones bajos sirven para indicar parámetros que son ignorados y los nombres de las funciones pueden estar en posición infija:

  (<+>) :: Maybe a -> Maybe a -> Maybe a
  Just a <+> _      = Just a
  Empty  <+> Just a = Just a
  Empty  <+> Empty  = Empty

Podemos definir funciones lambda anónimas con una diagonal invertida, que se parece a la letra griega λ. Las siguientes expresiones son equivalentes:

  (*)
  (\a1 -> \a2 -> a1 * a2)
  (\a1 a2     -> a1 * a2)

Las funciones de Haskell sobre las que se hace un emparejamiento de patrones son conveniencias sintácticas para funciones lambda anidadas. Considere una función simple que crea una tupla dadas tres entradas:

  tuple :: a -> b -> c -> (a, b, c)

La implementación

  tuple a b c = (a, b, c)

es equivalente a

  tuple = \a -> \b -> \c -> (a, b, c)

En el cuerpo de una función podemos crear asignaciones locales de valores con cláusulas let o where. Las siguientes son definiciones equivalentes de map para una lista ligada (un apóstrofe es un caracter válido en los identificadores de nombre):

  map :: (a -> b) -> [a] -> [b]

  -- explicit
  map f as = foldr map' [] as
             where map' a bs = f a : bs

  -- terser, making use of currying
  map f    = foldr map' []
             where map' a = (f a :)

  -- let binding
  map f    = let map' a = (f a :)
             in foldr map' []

  -- actual implementation
  map _ []       = []
  map f (x : xs) = f x : map f xs

if / then / else son palabras reservadas para sentencias condicionales:

  filter :: (a -> Bool) -> [a] -> [a]
  filter _ [] = []
  filter f (head : tail) = if f head
                           then head : filter f tail
                           else filter f tail

Un estilo alternativo es el uso de guardas de casos

  filter f (head : tail) | f head    = head : filter f tail
                         | otherwise = filter f tail

El emparejamiento de patrones sobre cualquier término se hace con case ... of

  unfoldr :: (a -> Maybe (b, a)) -> a -> [b]
  unfoldr f b = case f b of
                  Just (b', a') -> b' : unfoldr f a'
                  Nothing       -> []

Las guardas pueden usarse dentro de los emparejamientos. Por ejemplo, digamos que deseamos tratar a los ceros como un caso especial:

  unfoldrInt :: (a -> Maybe (Int, a)) -> a -> [Int]
  unfoldrInt f b = case f b of
                     Just (i, a') | i == 0    -> unfoldrInt f a'
                                  | otherwise -> i : unfoldrInt f a'
                     Nothing                  -> []

Finalmente, dos funciones que vale la pena mencionar son ($) y (.)

  -- operador de aplicación
  ($) :: (a -> b) -> a -> b
  infixr 0

  -- composición de funciones
  (.) :: (b -> c) -> (a -> b) -> a -> c
  infixr 9

Ambas funciones son alternativas de estilo a los paréntesis anidados.

Las siguientes funciones son equivalentes:

  Just (f a)
  Just $ f a

así como lo son

  putStrLn (show (1 + 1))
  putStrLn $ show $ 1 + 1

Existe una tendencia a preferir la composición de funciones con . en lugar de múltiples $

  (putStrLn . show) $ 1 + 1

Typeclasses

Para definir una typeclass usamos la palabra reservada class, seguida del nombre de la typeclass, su parámetro de tipo, y entonces los miembros requeridos un una cláusula where.

Si existen dependencias entre las typeclasses, por ejemplo, el hecho de que un Applicative requiere la existencia de un Functor, llamamos a esta una restricción y usamos la notación =>:

  class Functor f where
    (<$>) :: (a -> b) -> f a -> f b
    infixl 4 <$>

  class Functor f => Applicative f where
    pure  :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b
    infixl 4 <*>

  class Applicative f => Monad f where
    (=<<) :: (a -> f b) -> f a -> f b
    infixr 1 =<<

Proporcionamos una implementación de una typeclass con la palabra reservada instance. Si deseamos repetir la firma en las funciones instancias, útiles por claridad, debemos habilitar la extensión del lenguaje InstanceSigs.

  {-# LANGUAGE InstanceSigs #-}

  data List a = Nil | a :. List a

  -- definidos en otra parte
  (++) :: List a -> List a -> List a
  map :: (a -> b) -> List a -> List b
  flatMap :: (a -> List b) -> List a -> List b
  foldLeft :: (b -> a -> b) -> b -> List a -> b

  instance Functor List where
    (<$>) :: (a -> b) -> List a -> List b
    f <$> as = map f as

  instance Applicative List where
    pure a = a :. Nil

    Nil <*> _  = Nil
    fs  <*> as = foldLeft (++) Nil $ (<$> as) <$> fs

  instance Monad List where
    f =<< list = flatMap f list

Si tenemos una restricción de typeclass en una función, usamos la misma notación =>. Por ejemplo podemos definir algo similar al Apply.apply2 de Scalaz

  apply2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
  apply2 f fa fb = f <$> fa <*> fb

Dado que hemos introducido Monad, es un punto para introducir la notación do, que fue la inspiración de las comprensiones for de Scala:

  do
    a <- f
    b <- g
    c <- h
    pure (a, b, c)

que es equivalente a

  f >>= \a ->
    g >>= \b ->
      h >>= \c ->
        pure (a, b, c)

donde >>= es =<< con los parámetros en orden contrario

  (>>=) :: Monad f => f a -> (a -> f b) -> f b
  (>>=) = flip (=<<)
  infixl 1 >>=

  -- from the stdlib
  flip :: (a -> b -> c) -> b -> a -> c

A diferencia de Scala, no es necesario crear variables para los valores unit, o proporcionar un yield si estamos devolviendo (). Por ejemplo

  for {
    _ <- putStr("hello")
    _ <- putStr(" world")
  } yield ()

se traduce como

  do putStr "hello"
     putStr " world"

Los valores que no son monádicos pueden crearse con la palabra reservada let:

  nameReturn :: IO String
  nameReturn = do putStr "What is your first name? "
                  first <- getLine
                  putStr "And your last name? "
                  last  <- getLine
                  let full = first ++ " " ++ last
                  putStrLn ("Pleased to meet you, " ++ full ++ "!")
                  pure full

Finalmente, Haskell tiene derivación de typeclasses con la palabra reservada deriving, la inspiración para @scalaz.deriving. Definir las reglas de derivación es un tema avanzado, pero es fácil derivar una typeclass para una ADT:

  data List a = Nil | a :. List a
                deriving (Eq, Ord)

Algebras

En Scala, las typeclasses y las álgebras se definen como una interfaz trait, Las typeclasses son inyectadas con la característica implicit y las álgebras se pasan como parámetros explícitos. No hay soporte a nivel de Haskell para las álgebras: ¡son simplemente datos!

Considere el álgebra simple Console de la introducción. Podemos reescribirla en Haskell como un registro de funciones:

  data Console m = Console
                    { println :: Text -> m ()
                    , readln  :: m Text
                    }

con la lógica de negocios usando una restricción monádica

  echo :: (Monad m) => Console m -> m ()
  echo c = do line <- readln c
              println c line

Una implementación de producción para Console probablemente tendría tipo Console IO. La función liftIO de Scalaz está inspirada en una función de Haskell del mismo nombre y puede elevar Console IO a cualquier pila de Mónadas Avanzadas.

Dos extensiones adicionales del lenguaje hacen que la lógica de negocio sea aún más limpia. Por ejemplo, RecordWildCards permite importar todos los campos de un tipo mediante el uso de {..}:

  echo :: (Monad m) => Console m -> m ()
  echo Console{..} = do line <- readln
                        println line

NamedFieldPuns requiere que cada campo importado sea listado explícitamente, lo que es más código verboso pero hace que el código sea más fácil de leer:

  echo :: (Monad m) => Console m -> m ()
  echo Console{readln, println} = do line <- readln
                                     println line

Mientras en Scala esta codificación podría llamarse Finalmente sin etiquetas, en Haskell es conocida como estilo MTL. Sin entrar en detalles, algunos desarrolladores de Scala no comprendieron un artículo de investigación sobre los beneficios de rendimiento de las ADTs generalizadas en Haskell.

Una alternativa al estilo MTL son los Efectos extensibles, también conocido como estilo de Mónada libre.

Módulos

El código fuente de Haskell está organizado en módulos jerárquicos con la restricción de que todo el contenido de un module debe vivir en un único archivo. En la parte superior de un archivo se declara el nombre del module

  module Silly.Tree where

Una convención es usar directorios en el disco para organizar el código, de modo que este archivo iría en Silly/Tree.hs.

Por defecto todos los símbolos en el archivo son exportados pero podemos escoger exportar miembros específicos, por ejemplo el tipo Tree y los constructores de datos, y una función fringe, omitiendo sapling:

  module Silly.Tree (Tree(..), fringe) where

  data Tree a = Leaf a | Branch (Tree a) (Tree a)

  fringe :: Tree a -> [a]
  fringe (Leaf x)            = [x]
  fringe (Branch left right) = fringe left ++ fringe right

  sapling :: Tree String
  sapling = Leaf ""

De manera interesante, podemos exportar símbolos que son importados al módulo, permitiendo que los autores de librerías empaquen su API completa en un único módulo, sin importar cómo fue implementada.

En un archivo distinto podemos importar todos los miembros exportados desde Silly.Tree

  import Silly.Tree

que es aproximadamente equivalente a la sintaxis de Scala import silly.tree._. Si deseamos restringir los símbolos que importamos podemos proveer una lista explícita entre paréntesis después del import

  import Silly.Tree (Tree, fringe)

Aquí únicamente importamos el constructor de tipo Tree (no los constructores de datos) y la función fringe. Si desamos importar todos los constructores de datos (y los emparejadores de patrones) podemos usar Tree(...). Si únicamente deseamos importar el constructor Branch podemos listarlo explícitamente:

  import Silly.Tree (Tree(Branch), fringe)

Si tenemos colisión de nombres sobre un símbolo podemos usar un import qualified, con una lista opcional de símbolos a importar

  import qualified Silly.Tree as T

Ahora podemos acceder a la función fringe con T.fringe.

De manera alternativa, más bien que seleccionar, podemos escoger que no importar

  import Silly.Tree hiding (fringe)

Por defecto el módulo Prelude es importado implícitamente pero si agregamos un import explícito del módulo Prelude, únicamente nuestra versión es usada. Podemos usar esta técnica para esconder funciones antiguas inseguras

  import Prelude hiding ((!!), head)

o usar un preludio personalizado y deshabilitar el preludio por defecto con la extensión del lenguaje NoImplicitPrelude.

Evaluación

Haskell se compila a código nativo, no hay una máquina virtual, pero existe un recolector de basura. Un aspecto fundamental del ambiente de ejecución es que todos los parámetros se evalúan de manera perezosa por default. Haskell trata todos los términos como una promesa de proporcionar un valor cuando sea necesario, llamado un thunk. Los thunks se reducen tanto como sea necesario para proceder, y no más.

¡Una enorme ventaja de la evaluación perezosa es que es mucho más complicado ocasionar un sobreflujo de la pila! Una desventaja es que existe un costo adicional comparado con la evaluación estricta, por lo que Haskell nos permite optar por la evaluación estricta parámetro por parámetro.

Haskell también está matizado por lo que significa evaluación estricta: se dice que un término está en su forma normal de cabeza débil (WHNF, por sus siglas en inglés) si los bloques de código más externos no pueden reducirse más, y en su forma normal si el término está completamente evaluado. La estrategia de evaluación por defecto de Scala corresponde aproximadamente a la forma normal.

Por ejemplo, estos términos están en forma normal:

  42
  (2, "foo")
  \x -> x + 1

mientras que los siguientes no están en forma normal (todavía pueden reducirse más):

  1 + 2            -- reduces to 3
  (\x -> x + 1) 2  -- reduces to 3
  "foo" ++ "bar"   -- reduces to "foobar"
  (1 + 1, "foo")   -- reduces to (2, "foo")

Los siguientes términos están en WHNF debido a que el código más externo no puede reducirse más (aunque las partes internas puedan):

  (1 + 1, "foo")
  \x -> 2 + 2
  'f' : ("oo" ++ "bar")

La estrategia de evaluación por defecto es no realizar reducción alguna cuando se pasa un término como parámetro. El soporte a nivel de lenguaje nos permite solicitar WHNF para cualquier término con ($!)

  -- evalúa `a` a WHNF, entonces invoca la función con dicho valor
  ($!) :: (a -> b) -> a -> b
  infixr 0

Podemos usar un signo de admiración ! en los parámetros data:

  data StrictList t = StrictNil | !t :. !(StrictList t)

  data Employee = Employee
                    { name :: !Text
                    , age :: !Int
                    }

La extensión StrictData permite parámetros estrictos para todos los datos en un módulo.

Otra extensión, BangPatterns, permite que ! sea usado en los argumentos de las funciones. La extensión Strict hace todas las funciones y los parámetros de datos en el módulo estrictos por defecto.

Yendo al extremo podemos usar ($!!) y la typeclass NFData para evaluación en su forma normal.

  class NFData a where
    rnf :: a -> ()

  ($!!) :: (NFData a) => (a -> b) -> a -> b

que está sujeto a la disponibilidad de una instancia de NFData.

El costo de optar por ser estricto es que Haskell se comporta como cualquier otro lenguaje estricto y puede realizar trabajo innecesario. Optar por la evaluación estricta debe hacerse con mucho cuidado, y únicamente para mejoras de rendimiento medibles. Si está en duda, sea perezoso y acepte las opciones por defecto.

Siguientes pasos

Haskell es un lenguaje más rápido, seguro y simple que Scala y ha sido probado en la industria. Considere tomar el curso de data61 sobre programación funcional, y pregunte en el cuarto de charla #qfpl en freenode.net.

Algunos materiales de aprendizaje adicionales son:

Si usted disfruta usar Haskell y entiende el valor que traería a su negocio, ¡dígale a sus managers! De esa manera, el pequeño porcentaje de managers que lideran proyectos de Haskell serán capaces de atraer talento de programación funcional de los muchos equipos que no, y todos serán felices.

Licencias de terceros

Algo del código fuente de este libro ha sido copiado de proyectos de software libre. La licencia de estos proyectos requieren que los siguientes textos se distribuyan con el código que se presenta en este libro.

Scala License

  Copyright (c) 2002-2017 EPFL
  Copyright (c) 2011-2017 Lightbend, Inc.

  All rights reserved.

  Redistribution and use in source and binary forms, with or without modification,
  are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    * Neither the name of the EPFL nor the names of its contributors
      may be used to endorse or promote products derived from this software
      without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Scalaz License

  Copyright (c) 2009-2014 Tony Morris, Runar Bjarnason, Tom Adams,
                          Kristian Domagala, Brad Clow, Ricky Clarkson,
                          Paul Chiusano, Trygve Laugstøl, Nick Partridge,
                          Jason Zaugg
  All rights reserved.

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions
  are met:

  1. Redistributions of source code must retain the above copyright
     notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.
  3. Neither the name of the copyright holder nor the names of
     its contributors may be used to endorse or promote products derived from
     this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.