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")
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 State 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 unit 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)