Translate

miércoles, 6 de abril de 2022

Funtores parte 2

Seguimos con Funtores 

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:

import scala.concurrent.{Future, Await}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._

val future: Future[String] =
    Future(123).
    map(n => n + 1).
    map(n => n * 2).
    map(n => s"${n}!")

Await.result(future, 1.second)
// res2: String = "248!"

Tenga en cuenta que los futuros de Scala no son un gran ejemplo de programación funcional pura porque no son referencialmente transparentes. Future siempre calcula y almacena en caché un resultado y no hay forma de que modifiquemos este comportamiento. Esto significa que podemos obtener resultados impredecibles cuando usamos Future para envolver cálculos de efectos secundarios. Por ejemplo:

import scala.util.Random
val future1 = {
  // Initialize Random with a fixed seed:
  val r = new Random(0L)
  // nextInt has the side-effect of moving to
  // the next random number in the sequence:
  val x = Future(r.nextInt)

  for {
    a <- x
    b <- x
  } yield (a, b)
}

val future2 = {
  val r = new Random(0L)
  for {
    a <- Future(r.nextInt)
    b <- Future(r.nextInt)
  } yield (a, b)
}

val result1 = Await.result(future1, 1.second)
// result1: (Int, Int) = (-1155484576, -1155484576)
val result2 = Await.result(future2, 1.second)
// result2: (Int, Int) = (-1155484576, -723955400)

Idealmente, nos gustaría que resultado1 y resultado2 contuvieran el mismo valor. Sin embargo, el cálculo de future1 llama a nextInt una vez y el cálculo de future2 lo llama dos veces. Porque nextInt devuelve un resultado diferente cada vez que obtenemos un resultado diferente en cada caso.

Este tipo de discrepancia hace que sea difícil razonar acerca de los programas que involucran Futuros y efectos secundarios. También hay otros aspectos problemáticos del comportamiento de Future, como la forma en que siempre inicia los cálculos inmediatamente en lugar de permitir que el usuario dicte cuándo debe ejecutarse el programa. 

Cuando miramos Cats Effect, veremos que el tipo IO resuelve estos problemas. Si Future no es referencialmente transparente, tal vez deberíamos buscar otro tipo de datos similar que lo sea. Pero de él hablaremos el proximo post.