scala scalaz state-monad

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 estado Int , configúrelo como el valor envuelto por la mónada State[Int, _] y conéctelo a a
  • _ <- modify[Int](_ + 1) : incrementa el estado Int
  • b <- init[Int] : tomar el estado Int y vincularlo a b (lo mismo que para a pero ahora el estado se incrementa)
  • produce un valor de State[Int, (Int, Int)] usando b .

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]()))