Hay situaciones en las que la eliminación de izquierda a derecha no es la opción correcta. Un ejemplo es el tipo O en Scalactic, que es un equivalente convencionalmente sesgado a la izquierda de O bien:
type PossibleResult = ActualResult Or Error
Hay situaciones en las que la eliminación de izquierda a derecha no es la opción correcta. Un ejemplo es el tipo O en Scalactic, que es un equivalente convencionalmente sesgado a la izquierda de O bien:
type PossibleResult = ActualResult Or Error
Vimos una instancia de functor para Function1.
import cats.Functor
import cats.instances.function._ // for Functor
import cats.syntax.functor._
// for map
val func1 = (x: Int) => x.toDouble
val func2 = (y: Double) => y * 2
val func3 = func1.map(func2)
// func3: Int => Double = scala.Function1$$Lambda$7919/0
Entre otros tipos, Cats proporciona una instancia de Invariant para Monoid.
Imagina que queremos producir un monoide para el tipo símbolo de Scala. Cats no proporciona un Monoide para Símbolo, pero sí proporciona un Monoide para un tipo similar: Cadena o String. Podemos escribir nuestro nuevo semigrupo con un método vacío que se basa en la cadena vacía y un método de combinación que funciona de la siguiente manera:
Podemos implementar combine usando imap, pasando funciones de tipo String =>Symbol y Symbol => String como parámetros. Aquí está el código, escrito usando el método de extensión imap proporcionado por cats.syntax.invariant:
Veamos la implementación de funtores contravariantes e invariantes en Cats, proporcionados por las clases de tipos cats.Contravariant y cats.Invariant.
trait Contravariant[F[_]] {
def contramap[A, B](fa: F[A])(f: B => A): F[B]
}
trait Invariant[F[_]] {
def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B]
}
Podemos pensar en el método map de Functor como "agregar" una transformación a una cadena. Ahora vamos a ver otras dos type class, una que representa anteponer operaciones a una cadena y otra que representa la construcción de una cadena bidireccional de operaciones. Estos se denominan funtores contravariantes e invariantes respectivamente.
La primera de nuestras clases de tipos, el funtor contravariante, proporciona una operación llamada contramapa que representa "anteponer" una operación a una cadena.
El método contramap solo tiene sentido para tipos de datos que representan transformaciones. Por ejemplo, no podemos definir un contramapa para una opción porque no hay forma de retroalimentar un valor en un Option[B] a través de una función A => B. Sin embargo, podemos definir un contramapa para la clase de tipo Imprimible :
trait Printable[A] {
def format(value: A): String
}
Podemos definir un funtor simplemente definiendo su método map. Aquí hay un ejemplo de un Functor para Option, aunque tal cosa ya existe en cats.instances. La implementación es trivial: simplemente llamamos al método de mapa de Option:
implicit val optionFunctor: Functor[Option] = new Functor[Option] {
def map[A, B](value: Option[A])(func: A => B): Option[B] = value.map(func)
}
El método principal proporcionado por la sintaxis de Functor es map. Es difícil demostrar esto con Opciones y Listas, ya que tienen sus propios métodos de mapa integrados y el compilador de Scala siempre preferirá un método integrado a un método de extensión. Resolveremos esto con dos ejemplos. Primero veamos el map sobre funciones. El tipo Function1 de Scala no tiene un método de map (se llama andThen en su lugar), por lo que no hay conflictos de nombres:
import cats.instances.function._ // for Functor
import cats.syntax.functor._ // for map
val func1 = (a: Int) => a + 1
val func2 = (a: Int) => a * 2
val func3 = (a: Int) => s"${a}!"
val func4 = func1.map(func2).map(func3)
func4(123)
// res3: String = "248!"
La clase de tipo de funtor es cats.Functor. Obtenemos instancias usando el método Functor.apply estándar de un objeto complementario. Como es habitual, las instancias predeterminadas se organizan por tipo en el paquete cats.instances:
import cats.Functor
import cats.instances.list._
// for Functor
import cats.instances.option._ // for Functor
val list1 = List(1, 2, 3)
// list1: List[Int] = List(1, 2, 3)
val list2 = Functor[List].map(list1)(_ * 2)
// list2: List[Int] = List(2, 4, 6)
val option1 = Option(123)
// option1: Option[Int] = Some(123)
val option2 = Functor[Option].map(option1)(_.toString)
// option2: Option[String] = Some("123")
Formalmente, un funtor es un tipo F[A] con un map de tipo (A => B) => F[B].
Cats codifica Functor como una clase de tipos, cats.Functor, por lo que el método se ve un poco diferente. Acepta la F[A] inicial como parámetro junto con la función de transformación. Aquí hay una versión simplificada de la definición:
package cats
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
Los métodos de map de List, Option y Either aplican funciones de forma eager (es decir no lazy). Sin embargo, la idea de secuenciar cálculos es más general que esto. Investiguemos el comportamiento de algunos otros funtores que aplican el patrón de diferentes maneras.
Futures : Future es un funtor que secuencia cálculos asincrónicos poniéndolos en cola y aplicándolos a medida que se completan sus predecesores. La firma de tipo de su método de map, tiene la misma forma que las firmas anteriores. Sin embargo, el comportamiento es muy diferente. Cuando trabajamos con un Futuro no tenemos garantías sobre su estado interno. El cálculo envuelto puede estar en curso, completo o rechazado. Si el futuro está completo, nuestra función de map se puede llamar inmediatamente. De lo contrario, algún grupo de subprocesos subyacente pone en cola la llamada de función y vuelve a ella más tarde. No sabemos cuándo se llamará a nuestras funciones, pero sí sabemos en qué orden se llamarán. De esta manera, Future proporciona el mismo comportamiento de secuencia que se ve en List, Option y Either:
Los funtores son una abstracción que nos permite representar secuencias de operaciones dentro de un contexto como una Lista, un Option o cualquiera de miles de otras posibilidades. Los funtores por sí solos no son tan útiles, pero los casos especiales de funtores, como las mónadas y los funtores aplicativos, son algunas de las abstracciones más utilizadas.
Informalmente, un funtor es cualquier cosa con un método map. Probablemente conozca muchos tipos que tienen esto: Option, List, y Either, por nombrar algunos. Por lo general, nos encontramos primero con el map cuando iteramos sobre Listas. Sin embargo, para entender los funtores necesitamos pensar en el método de otra manera. En lugar de recorrer la lista, deberíamos pensar en ella como transformar todos los valores dentro de una sola vez. Especificamos la función que se va a aplicar y el map se asegura de que se aplique a cada elemento. Los valores cambian pero la estructura de la lista (el número de elementos y su orden) permanece igual:
List(1, 2, 3).map(n => n + 1)
// res0: List[Int] = List(2, 3, 4)
De manera similar, cuando asignamos una opción, transformamos los contenidos pero dejamos el contexto Some o None sin cambios. El mismo principio se aplica a Either con sus contextos Left y Right. Esta noción general de transformación, junto con el patrón común de firmas de tipo, es lo que conecta el comportamiento del mapa en diferentes tipos de datos.