Ejemplos de mónada del estado de Scalaz
state-monad (3)
No he visto muchos ejemplos de la mónada de estado scalaz. Existe este ejemplo, pero es difícil de entender y solo parece haber otra pregunta sobre el desbordamiento de la pila.
Voy a publicar algunos ejemplos con los que he jugado, pero agradecería otros. Además, si alguien puede dar un ejemplo sobre por qué se usan init
, modify
, put
y gets
, sería genial.
Editar: here hay una impresionante presentación de 2 horas sobre la mónada estatal.
Aquí hay un pequeño ejemplo de cómo se puede usar State
:
Definamos un pequeño "juego" donde algunas unidades de juego luchan contra el jefe (que también es una unidad de juego).
case class GameUnit(health: Int)
case class Game(score: Int, boss: GameUnit, party: List[GameUnit])
object Game {
val init = Game(0, GameUnit(100), List(GameUnit(20), GameUnit(10)))
}
Cuando la obra está activa, queremos hacer un seguimiento del estado del juego, así que vamos a definir nuestras "acciones" en términos de una mónada de estado:
Golpeemos al jefe con fuerza para que pierda 10 de su health
:
def strike : State[Game, Unit] = modify[Game] { s =>
s.copy(
boss = s.boss.copy(health = s.boss.health - 10)
)
}
¡Y el jefe puede devolver el golpe! Cuando lo hace, todos en una fiesta pierden 5 health
.
def fireBreath : State[Game, Unit] = modify[Game] { s =>
val us = s.party
.map(u => u.copy(health = u.health - 5))
.filter(_.health > 0)
s.copy(party = us)
}
Ahora podemos componer estas acciones en play
:
def play = for {
_ <- strike
_ <- fireBreath
_ <- fireBreath
_ <- strike
} yield ()
Por supuesto, en la vida real, la obra será más dinámica, pero es suficiente para mi pequeño ejemplo :)
Podemos ejecutarlo ahora para ver el estado final del juego:
val res = play.exec(Game.init)
println(res)
>> Game(0,GameUnit(80),List(GameUnit(10)))
Así que apenas golpeamos al jefe y una de las unidades murió, RIP.
El punto aquí es la composición . State
(que es solo una función S => (A, S)
) le permite definir acciones que producen resultados y también manipular algún estado sin saber demasiado de dónde viene el estado. La parte de Monad
te da composición para que tus acciones se puedan componer:
A => State[S, B]
B => State[S, C]
------------------
A => State[S, C]
y así.
PS En cuanto a las diferencias entre get
, put
y modify
:
modify
se puede ver como get
y armar:
def modify[S](f: S => S) : State[S, Unit] = for {
s <- get
_ <- put(f(s))
} yield ()
o simplemente
def modify[S](f: S => S) : State[S, Unit] = get[S].flatMap(s => put(f(s)))
Entonces, cuando use modify
, conceptualmente use get
y put
, o puede usarlas solo.
Me encontré con una publicación de blog interesante Grok Haskell Monad Transformers de sigfp que tiene un ejemplo de aplicación de dos mónadas de estado a través de un transformador de mónada. Aquí hay una traducción scalaz.
El primer ejemplo muestra una mónada de State[Int, _]
:
val test1 = for {
a <- init[Int]
_ <- modify[Int](_ + 1)
b <- init[Int]
} yield (a, b)
val go1 = test1 ! 0
// (Int, Int) = (0,1)
Así que tengo aquí un ejemplo de usar init
y modify
. Después de jugar con él un poco, init[S]
resulta ser muy conveniente para generar un valor de State[S,S]
, pero lo otro que permite es acceder al estado dentro de para la comprensión. modify[S]
es una forma conveniente de transformar el estado dentro de para la comprensión. Entonces, el ejemplo anterior se puede leer como:
-
a <- init[Int]
: comience con un estadoInt
, configúrelo como el valor envuelto por la mónadaState[Int, _]
y conéctelo aa
-
_ <- modify[Int](_ + 1)
: incrementa el estadoInt
-
b <- init[Int]
: tomar el estadoInt
y vincularlo ab
(lo mismo que paraa
pero ahora el estado se incrementa) - produce un valor de
State[Int, (Int, Int)]
usandob
.
La sintaxis de comprensión ya hace que sea trivial trabajar en el lado A
en el State[S, A]
. init
, modify
, put
y gets
proporciona algunas herramientas para trabajar en el lado S
en State[S, A]
.
El segundo ejemplo en la publicación del blog se traduce a:
val test2 = for {
a <- init[String]
_ <- modify[String](_ + "1")
b <- init[String]
} yield (a, b)
val go2 = test2 ! "0"
// (String, String) = ("0","01")
Casi la misma explicación que test1
.
El tercer ejemplo es más complicado y espero que haya algo más simple que aún tengo que descubrir.
type StateString[x] = State[String, x]
val test3 = {
val stTrans = stateT[StateString, Int, String]{ i =>
for {
_ <- init[String]
_ <- modify[String](_ + "1")
s <- init[String]
} yield (i+1, s)
}
val initT = stateT[StateString, Int, Int]{ s => (s,s).pure[StateString] }
for {
b <- stTrans
a <- initT
} yield (a, b)
}
val go3 = test3 ! 0 ! "0"
// (Int, String) = (1,"01")
En ese código, stTrans
se encarga de la transformación de ambos estados (incremento y sufijo con "1"
) y de extraer el estado de String
. stateT
nos permite agregar transformación de estado en una mónada arbitraria M
En este caso, el estado es un Int
que se incrementa. Si llamamos a stTrans ! 0
stTrans ! 0
terminaríamos con M[String]
. En nuestro ejemplo, M
es StateString
, por lo que terminaremos con StateString[String]
que es State[String, String]
.
La parte difícil aquí es que queremos sacar el valor del estado Int
de stTrans
. Esto es para lo que initT
es. Simplemente crea un objeto que da acceso al estado de una manera que podemos flatMap con stTrans
.
Editar: Resulta que toda esa torpeza se puede evitar si realmente reutilizamos test1
y test2
que almacenan convenientemente los estados deseados en el elemento _2
de sus tuplas devueltas:
// same as test3:
val test31 = stateT[StateString, Int, (Int, String)]{ i =>
val (_, a) = test1 ! i
for (t <- test2) yield (a, (a, t._2))
}
Supongo que scalaz 7.0.x y las siguientes importaciones (mira el historial de respuestas para scalaz 6.x ):
import scalaz._
import Scalaz._
El tipo de estado se define como State[S, A]
donde S
es el tipo del estado y A
es el tipo del valor que se está decorando. La sintaxis básica para crear un valor de estado hace uso de la función State[S, A]
:
// Create a state computation incrementing the state and returning the "str" value
val s = State[Int, String](i => (i + 1, "str"))
Para ejecutar el cálculo de estado en un valor inicial:
// start with state of 1, pass it to s
s.eval(1)
// returns result value "str"
// same but only retrieve the state
s.exec(1)
// 2
// get both state and value
s(1) // or s.run(1)
// (2, "str")
El estado se puede enhebrar mediante llamadas a funciones. Para hacer esto en lugar de la Function[A, B]
, defina la Function[A, State[S, B]]]
. Usa la función de State
...
import java.util.Random
def dice() = State[Random, Int](r => (r, r.nextInt(6) + 1))
Entonces, la sintaxis for/yield
se puede usar para componer funciones:
def TwoDice() = for {
r1 <- dice()
r2 <- dice()
} yield (r1, r2)
// start with a known seed
TwoDice().eval(new Random(1L))
// resulting value is (Int, Int) = (4,5)
Aquí hay otro ejemplo. Complete una lista con TwoDice()
estado de TwoDice()
.
val list = List.fill(10)(TwoDice())
// List[scalaz.IndexedStateT[scalaz.Id.Id,Random,Random,(Int, Int)]]
Utilice la secuencia para obtener un State[Random, List[(Int,Int)]]
. Podemos proporcionar un alias tipo.
type StateRandom[x] = State[Random,x]
val list2 = list.sequence[StateRandom, (Int,Int)]
// list2: StateRandom[List[(Int, Int)]] = ...
// run this computation starting with state new Random(1L)
val tenDoubleThrows2 = list2.eval(new Random(1L))
// tenDoubleThrows2 : scalaz.Id.Id[List[(Int, Int)]] =
// List((4,5), (2,4), (3,5), (3,5), (5,5), (2,2), (2,4), (1,5), (3,1), (1,6))
O podemos usar sequenceU
que inferirá los tipos:
val list3 = list.sequenceU
val tenDoubleThrows3 = list3.eval(new Random(1L))
// tenDoubleThrows3 : scalaz.Id.Id[List[(Int, Int)]] =
// List((4,5), (2,4), (3,5), (3,5), (5,5), (2,2), (2,4), (1,5), (3,1), (1,6))
Otro ejemplo con State[Map[Int, Int], Int]
para calcular la frecuencia de las sumas en la lista anterior. freqSum
calcula la suma de los lanzamientos y las frecuencias de conteos.
def freqSum(dice: (Int, Int)) = State[Map[Int,Int], Int]{ freq =>
val s = dice._1 + dice._2
val tuple = s -> (freq.getOrElse(s, 0) + 1)
(freq + tuple, s)
}
Ahora usa poligonal para aplicar freqSum
durante tenDoubleThrows
. traverse
es equivalente a la map(freqSum).sequence
.
type StateFreq[x] = State[Map[Int,Int],x]
// only get the state
tenDoubleThrows2.copoint.traverse[StateFreq, Int](freqSum).exec(Map[Int,Int]())
// Map(10 -> 1, 6 -> 3, 9 -> 1, 7 -> 1, 8 -> 2, 4 -> 2) : scalaz.Id.Id[Map[Int,Int]]
O más sucintamente mediante el uso de traverseU
para inferir los tipos:
tenDoubleThrows2.copoint.traverseU(freqSum).exec(Map[Int,Int]())
// Map(10 -> 1, 6 -> 3, 9 -> 1, 7 -> 1, 8 -> 2, 4 -> 2) : scalaz.Id.Id[Map[Int,Int]]
Tenga en cuenta que debido a que State[S, A]
es un alias de tipo para StateT[Id, S, A]
, tenDoubleThrows2 termina tipeado como Id
. Uso el copoint
de copoint
para volver a convertirlo en un tipo de List
.
En resumen, parece que la clave para usar el estado es tener funciones que devuelven una función que modifique el estado y el valor de resultado real deseado ... Descargo de responsabilidad: nunca he usado el state
en el código de producción, simplemente tratando de obtener una sensación.
Información adicional sobre @ziggystar comment
Dejé de intentar usar stateT
, es posible que alguien más muestre si StateFreq
o StateRandom
pueden aumentarse para realizar el cálculo combinado. Lo que encontré en su lugar es que la composición de los dos transformadores de estado se puede combinar así:
def stateBicompose[S, T, A, B](
f: State[S, A],
g: (A) => State[T, B]) = State[(S,T), B]{ case (s, t) =>
val (newS, a) = f(s)
val (newT, b) = g(a) apply t
(newS, newT) -> b
}
Se basa en que g
es una función de un solo parámetro que toma el resultado del primer transformador de estado y devuelve un transformador de estado. Entonces, lo siguiente funcionaría:
def diceAndFreqSum = stateBicompose(TwoDice, freqSum)
type St2[x] = State[(Random, Map[Int,Int]), x]
List.fill(10)(diceAndFreqSum).sequence[St2, Int].exec((new Random(1L), Map[Int,Int]()))