web-dev-qa-db-fra.com

Exemples de monade d'état de Scalaz

Je n'ai pas vu beaucoup d'exemples de la monade d'état scalaz. Il y a cet exemple mais il est difficile à comprendre et il n'y a qu'une seule autre question sur débordement de pile semble-t-il.

Je vais poster quelques exemples avec lesquels j'ai joué mais j'en souhaiterais d'autres. Aussi, si quelqu'un peut fournir un exemple sur la raison pour laquelle init, modify, put et gets sont utilisés, ce serait formidable.

Edit: ici est une présentation impressionnante de 2 heures sur la monade d'état.

76
huynhjl

Je suppose, scalaz 7.0.x et les importations suivantes (regardez l'historique des réponses pour scalaz 6.x ):

import scalaz._
import Scalaz._

Le type d'état est défini comme State[S, A]S est le type de l'état et A est le type de la valeur en cours de décoration. La syntaxe de base pour créer une valeur d'état utilise la fonction State[S, A]:

// Create a state computation incrementing the state and returning the "str" value
val s = State[Int, String](i => (i + 1, "str")) 

Pour exécuter le calcul d'état sur une valeur initiale:

// 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")

L'état peut être transmis via des appels de fonction. Pour ce faire au lieu de Function[A, B], Définissez Function[A, State[S, B]]]. Utilisez la fonction State ...

import Java.util.Random
def dice() = State[Random, Int](r => (r, r.nextInt(6) + 1))

Ensuite, la syntaxe for/yield Peut être utilisée pour composer des fonctions:

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)

Voici un autre exemple. Remplissez une liste avec TwoDice() calculs d'état.

val list = List.fill(10)(TwoDice())
// List[scalaz.IndexedStateT[scalaz.Id.Id,Random,Random,(Int, Int)]]

Utilisez la séquence pour obtenir une State[Random, List[(Int,Int)]]. Nous pouvons fournir un alias de type.

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

Ou nous pouvons utiliser sequenceU qui déduira les types:

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

Un autre exemple avec State[Map[Int, Int], Int] Pour calculer la fréquence des sommes dans la liste ci-dessus. freqSum calcule la somme des fréquences des lancers et des décomptes.

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)
}

Utilisez maintenant traverse pour appliquer freqSum sur tenDoubleThrows. traverse est équivalent à 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]]

Ou plus succinctement en utilisant traverseU pour déduire les types:

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

Notez que parce que State[S, A] Est un alias de type pour StateT[Id, S, A], TenDoubleThrows2 finit par être tapé comme Id. J'utilise copoint pour le reconvertir en un type List.

En bref, il semble que la clé pour utiliser l'état est d'avoir des fonctions renvoyant une fonction modifiant l'état et la valeur de résultat réelle souhaitée ... Avertissement: je n'ai jamais utilisé state dans le code de production, essayant juste de se faire une idée.

Informations supplémentaires sur le commentaire @ziggystar

J'ai renoncé à essayer d'utiliser stateT peut être quelqu'un d'autre peut montrer si StateFreq ou StateRandom peut être augmenté pour effectuer le calcul combiné. Ce que j'ai trouvé à la place, c'est que la composition des deux transformateurs d'état peut être combinée comme ceci:

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
}

Il repose sur le fait que g est une fonction à un paramètre prenant le résultat du premier transformateur d'état et renvoyant un transformateur d'état. Ensuite, les éléments suivants fonctionneraient:

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

Je suis tombé sur un article de blog intéressant Grok Haskell Transformers Monad de sigfp qui a un exemple d'application de deux monades d'état via un transformateur de monade. Voici une traduction scalaz.

Le premier exemple montre une monade 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)

J'ai donc ici un exemple d'utilisation de init et modify. Après avoir joué un peu avec, init[S] S'avère très pratique pour générer une valeur State[S,S], Mais l'autre chose qu'il permet est d'accéder à l'état à l'intérieur du pour la compréhension. modify[S] Est un moyen pratique de transformer l'état à l'intérieur du pour la compréhension. Ainsi, l'exemple ci-dessus peut être lu comme suit:

  • a <- init[Int]: Commencez par un état Int, définissez-le comme valeur enveloppée par la monade State[Int, _] Et liez-le à a
  • _ <- modify[Int](_ + 1): incrémente l'état Int
  • b <- init[Int]: Prenez l'état Int et liez-le à b (comme pour a mais maintenant l'état est incrémenté)
  • donne une valeur State[Int, (Int, Int)] en utilisant a et b.

La syntaxe for comprehension rend déjà trivial de travailler du côté A dans State[S, A]. init, modify, put et gets fournissent des outils pour travailler du côté S dans State[S, A].

Le deuxième exemple du billet de blog se traduit par:

val test2 = for {
  a <- init[String]
  _ <- modify[String](_ + "1")
  b <- init[String]
} yield (a, b)

val go2 = test2 ! "0"
// (String, String) = ("0","01")

Vraiment la même explication que test1.

Le troisième exemple est plus délicat et j'espère qu'il y a quelque chose de plus simple que je n'ai pas encore découvert.

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

Dans ce code, stTrans s'occupe de la transformation des deux états (incrémenter et suffixer avec "1") Ainsi que d'extraire l'état String. stateT nous permet d'ajouter une transformation d'état sur une monade arbitraire M. Dans ce cas, l'état est un Int incrémenté. Si nous appelions stTrans ! 0 Nous finirions avec M[String]. Dans notre exemple, M est StateString, nous allons donc nous retrouver avec StateString[String] Qui est State[String, String].

La partie délicate ici est que nous voulons extraire la valeur d'état Int de stTrans. C'est à cela que sert initT. Il crée simplement un objet qui donne accès à l'état d'une manière que nous pouvons flatMap avec stTrans.

Edit: Il s'avère que toute cette maladresse peut être évitée si nous réutilisons vraiment test1 Et test2 Qui stockent commodément les états souhaités dans l'élément _2 De leurs tuples retournés:

// same as test3:
val test31 = stateT[StateString, Int, (Int, String)]{ i => 
  val (_, a) = test1 ! i
  for (t <- test2) yield (a, (a, t._2))
}
15
huynhjl

Voici un tout petit exemple sur la façon dont State peut être utilisé:

Définissons un petit "jeu" où certaines unités de jeu combattent le boss (qui est également une unité de jeu).

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)))
}

Lorsque le jeu est en cours, nous voulons garder une trace de l'état du jeu, alors définissons nos "actions" en termes de monade d'état:

Frappons fort le boss pour qu'il en perde 10 de son health:

def strike : State[Game, Unit] = modify[Game] { s =>
  s.copy(
    boss = s.boss.copy(health = s.boss.health - 10)
  )
}

Et le patron peut riposter! Quand il fait tout le monde dans un parti perd 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)
}

Maintenant, nous pouvons composer ces actions dans play:

def play = for {
  _ <- strike
  _ <- fireBreath
  _ <- fireBreath
  _ <- strike
} yield ()

Bien sûr, dans la vraie vie, le jeu sera plus dynamique, mais c'est assez de nourriture pour mon petit exemple :)

Nous pouvons l'exécuter maintenant pour voir l'état final du jeu:

val res = play.exec(Game.init)
println(res)

>> Game(0,GameUnit(80),List(GameUnit(10)))

Nous avons donc à peine frappé le boss et l'une des unités est morte, RIP.

Le point ici est le composition. State (qui n'est qu'une fonction S => (A, S)) vous permet de définir des actions qui produisent des résultats et aussi de manipuler un état sans trop savoir d'où il vient. La partie Monad vous donne la composition pour que vos actions puissent être composées:

 A => State[S, B] 
 B => State[S, C]
------------------
 A => State[S, C]

etc.

P.S. Comme pour les différences entre get, put et modify:

modify peut être vu comme get et put ensemble:

def modify[S](f: S => S) : State[S, Unit] = for {
  s <- get
  _ <- put(f(s))
} yield ()

ou simplement

def modify[S](f: S => S) : State[S, Unit] = get[S].flatMap(s => put(f(s)))

Ainsi, lorsque vous utilisez modify, vous utilisez conceptuellement get et put, ou vous pouvez simplement les utiliser seuls.

13
Alexey Raga