Translate

miércoles, 17 de agosto de 2022

Mónadas en Cats parte 22

Como hemos visto con Reader y Writer, el poder de la mónada State proviene de la combinación de instancias. Los métodos map y flatMap enhebran el estado de una instancia a otra. Cada instancia individual representa una transformación de estado atómico, y su combinación representa una secuencia completa de cambios:

val step1 = State[Int, String]{ num =>

    val ans = num + 1

    (ans, s"Result of step1: $ans")

}

val step2 = State[Int, String]{ num =>

    val ans = num * 2

    (ans, s"Result of step2: $ans")

}

val both = for {

    a <- step1

    b <- step2

} yield (a, b)

val (state, result) = both.run(20).value

// state: Int = 42

// result: (String, String) = ("Result of step1: 21", "Result of step2: 42")


Como puedes ver, en este ejemplo el estado final es el resultado de aplicar ambas transformaciones en secuencia. El estado se entrelaza paso a paso, aunque no interactuamos con él. El modelo general para usar la mónada de estado es representar cada paso de un cálculo como una instancia y componer los pasos usando los operadores de mónada estándar. Cats proporciona varios constructores de conveniencia para crear pasos primitivos:

  • get extrae el estado como resultado;
  • set actualiza el estado y devuelve la unidad como resultado;
  • pure ignora el estado y devuelve un resultado proporcionado;
  • inspect extrae el estado a través de una función de transformación;
  • modify actualiza el estado utilizando una función de actualización.

val getDemo = State.get[Int]
// getDemo: State[Int, Int] = cats.data.IndexedStateT@796af713
getDemo.run(10).value
// res1: (Int, Int) = (10, 10)
val setDemo = State.set[Int](30)
// setDemo: State[Int, Unit] = cats.data.IndexedStateT@f9e66fa
setDemo.run(10).value
// res2: (Int, Unit) = (30, ())
val pureDemo = State.pure[Int, String]("Result")
// pureDemo: State[Int, String] = cats.data.IndexedStateT@439e3ee4
pureDemo.run(10).value
// res3: (Int, String) = (10, "Result")
val inspectDemo = State.inspect[Int, String](x => s"${x}!")
// inspectDemo: State[Int, String] = cats.data.IndexedStateT@77263be4
inspectDemo.run(10).value
// res4: (Int, String) = (10, "10!")
val modifyDemo = State.modify[Int](_ + 1)
// modifyDemo: State[Int, Unit] = cats.data.IndexedStateT@44ddcbfc
modifyDemo.run(10).value
// res5: (Int, Unit) = (11, ())

Podemos ensamblar estos bloques de construcción usando una comprensión. Por lo general, ignoramos el resultado de las etapas intermedias que solo representan transformaciones en el estado:

import cats.data.State
import State._
val program: State[Int, (Int, Int, Int)] = for {
a <- get[Int]
_ <- set[Int](a + 1)
b <- get[Int]
_ <- modify[Int](_ + 1)
c <- inspect[Int, Int](_ * 1000)
} yield (a, b, c)
// program: State[Int, (Int, Int, Int)] = cats.data.
IndexedStateT@42c9d44a
val (state, result) = program.run(1).value
// state: Int = 3
// result: (Int, Int, Int) = (1, 2, 3000)