Beispiele für Scalaz-Staatsmonaden

77

Ich habe nicht viele Beispiele für die Scalaz-Staatsmonade gesehen. Es gibt dieses Beispiel, aber es ist schwer zu verstehen und es scheint nur eine andere Frage zum Stapelüberlauf zu geben.

Ich werde ein paar Beispiele veröffentlichen, mit denen ich gespielt habe, aber ich würde zusätzliche begrüßen. Auch wenn jemand beispielsweise auf Grund zur Verfügung stellen kann init, modify, putund getswerden dafür verwendet , wäre toll.

Bearbeiten: Hier ist eine tolle 2-stündige Präsentation über die Staatsmonade.

huynhjl
quelle

Antworten:

83

Ich gehe davon aus, scalaz 7.0.x und die folgenden Importe (siehe Antwortverlauf für scalaz 6.x ):

import scalaz._
import Scalaz._

Der Zustandstyp ist definiert als State[S, A]wo Sist der Typ des Zustands und Aist der Typ des Wertes, der dekoriert wird. Die grundlegende Syntax zum Erstellen eines Statuswerts verwendet die folgende State[S, A]Funktion:

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

So führen Sie die Statusberechnung für einen Anfangswert aus:

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

Der Status kann durch Funktionsaufrufe gefädelt werden. Um dies zu tun Function[A, B], definieren Sie Function[A, State[S, B]]]. Verwenden Sie die StateFunktion ...

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

Dann kann die for/yieldSyntax verwendet werden, um Funktionen zu erstellen:

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)

Hier ist ein weiteres Beispiel. Füllen Sie eine Liste mit TwoDice()Statusberechnungen.

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

Verwenden Sie die Sequenz, um eine zu erhalten State[Random, List[(Int,Int)]]. Wir können einen Typalias bereitstellen.

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

Oder wir können verwenden sequenceU, um die Typen abzuleiten:

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

Ein weiteres Beispiel State[Map[Int, Int], Int]für die Berechnung der Häufigkeit von Summen in der obigen Liste. freqSumberechnet die Summe der Würfe und zählt die Frequenzen.

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

Verwenden Sie nun die Traverse, um freqSumsie aufzutragen tenDoubleThrows. traverseist äquivalent zu 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]]

Oder genauer gesagt, indem Sie traverseUdie Typen ableiten:

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

Beachten Sie, dass tenDoubleThrows2 , da State[S, A]es sich um einen StateT[Id, S, A]Typalias für handelt , als typisiert wird Id. Ich copointverwandle es wieder in einen ListTyp.

Kurz gesagt, es scheint, dass der Schlüssel zur Verwendung von state darin besteht, dass Funktionen eine Funktion zurückgeben, die den Status und den gewünschten tatsächlichen Ergebniswert ändert ... Haftungsausschluss: Ich habe noch nie stateim Produktionscode verwendet, nur um ein Gefühl dafür zu bekommen.

Zusätzliche Infos zu @ziggystar Kommentar

Ich habe es aufgegeben, es zu versuchen, stateTkann jemand anderes zeigen, ob StateFreqoder StateRandomkann erweitert werden, um die kombinierte Berechnung durchzuführen. Stattdessen habe ich festgestellt, dass die Zusammensetzung der beiden Zustandstransformatoren folgendermaßen kombiniert werden kann:

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
}

Es basiert auf geiner Ein-Parameter-Funktion, die das Ergebnis des ersten Zustandstransformators aufnimmt und einen Zustandstransformator zurückgibt. Dann würde folgendes funktionieren:

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]()))
huynhjl
quelle
Ist die StateMonade nicht wirklich ein "Staatstransformator"? Und als zweite Frage: Gibt es eine schönere Möglichkeit, das Würfeln und das Summieren in einer einzigen Staatsmonade zu kombinieren? Wie würden Sie das angesichts der beiden Monaden machen?
Ziggystar
@ziggystar, technisch StateFreqund StateRandomsind Monaden. Ich denke nicht, dass State[S, x]es sich um einen Monadentransformator handelt, da Ses sich nicht um eine Monade handeln muss. Für eine schönere Art zu kombinieren, frage ich mich auch. Ich sehe nichts offensichtlich sofort verfügbar. Vielleicht stateTkönnte es helfen, aber ich habe es noch nicht herausgefunden.
Huynhjl
Ich habe nicht "Monadentransformator" geschrieben, sondern "Zustandstransformator". Die State[S, x]'Objekte halten keinen Zustand, sondern eine Transformation des letzteren. Es ist nur so, dass ich denke, der Name könnte weniger verwirrend gewählt werden. Das ist nichts über deine Antwort, sondern über Scalaz.
Ziggystar
@ziggystar, ich habe herausgefunden, wie ich stateTdas Rollen und Summieren in einer einzigen StateTMonade kombinieren kann ! Siehe stackoverflow.com/q/7782589/257449 . Ich bin gegen Ende festgefahren, dann habe ich es traverseirgendwann herausgefunden.
Huynhjl
1
@DavidB., Die Operator-ähnliche Syntax scheint verschwunden zu sein und durch Namen ersetzt worden zu sein. !ist jetzt eval; ~>ist jetzt exec.
Huynhjl
15

Ich bin auf einen interessanten Blog-Beitrag von Grok Haskell Monad Transformers von sigfp gestoßen, der ein Beispiel für die Anwendung von zwei Zustandsmonaden über einen Monadentransformator enthält. Hier ist eine Scalaz-Übersetzung.

Das erste Beispiel zeigt eine State[Int, _]Monade:

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

val go1 = test1 ! 0
// (Int, Int) = (0,1)

Ich habe hier also ein Beispiel für die Verwendung von initund modify. Nachdem Sie ein wenig damit gespielt haben, init[S]stellt sich heraus, dass es sehr praktisch ist, einen State[S,S]Wert zu generieren , aber das andere, was es erlaubt, ist, zum Verständnis auf den Status innerhalb des zuzugreifen. modify[S]ist eine bequeme Möglichkeit, den Zustand innerhalb des Verständnisses zu transformieren. Das obige Beispiel kann also wie folgt gelesen werden:

  • a <- init[Int]: Beginnen Sie mit einem IntStatus, legen Sie ihn als den von der State[Int, _]Monade umschlossenen Wert fest und binden Sie ihn ana
  • _ <- modify[Int](_ + 1): Erhöhen Sie den IntStatus
  • b <- init[Int]: nimm den IntStatus und binde ihn an b(wie für, aaber jetzt wird der Status erhöht)
  • einen State[Int, (Int, Int)]Wert mit aund ergeben b.

Der für das Verständnis Syntax schon macht es trivial zu Arbeiten an der ASeite State[S, A]. init, modify, putUnd getseinige Werkzeuge zum Arbeiten an der bereitzustellen SSeite State[S, A].

Das zweite Beispiel im Blog-Beitrag lautet:

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

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

Sehr die gleiche Erklärung wie test1.

Das dritte Beispiel ist schwieriger und ich hoffe, es gibt etwas Einfacheres, das ich noch nicht entdeckt habe.

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

Kümmert sich in diesem Code stTransum die Transformation beider Zustände (Inkrementieren und Suffixieren mit "1") sowie um das Herausziehen des StringZustands. stateTermöglicht es uns, eine Zustandstransformation für eine beliebige Monade hinzuzufügen M. In diesem Fall ist der Status Intinkrementiert. Wenn wir stTrans ! 0anrufen würden, würden wir am Ende mit M[String]. In unserem Beispiel Mist StateString, also werden wir mit StateString[String]dem enden, was ist State[String, String].

Der schwierige Teil hier ist, dass wir den IntZustandswert herausziehen wollen stTrans. Dafür ist da initT. Es wird lediglich ein Objekt erstellt, das den Zugriff auf den Status auf eine Weise ermöglicht, mit der wir flatMap verwenden können stTrans.

Bearbeiten: Es stellt sich heraus, dass all diese Unbeholfenheit vermieden werden kann, wenn wir sie wirklich wiederverwenden test1und test2die gewünschten Zustände bequem im _2Element ihrer zurückgegebenen Tupel speichern :

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

Hier ist ein sehr kleines Beispiel, wie Stateverwendet werden kann:

Definieren wir ein kleines "Spiel", in dem einige Spieleinheiten gegen den Boss kämpfen (der auch eine Spieleinheit ist).

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

Wenn das Spiel läuft, möchten wir den Spielstatus verfolgen. Definieren wir also unsere "Aktionen" in Form einer Statusmonade:

Lassen Sie uns den Boss hart schlagen, damit er 10 von seinem verliert health:

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

Und der Chef kann zurückschlagen! Wenn er es tut, verliert jeder in einer Gruppe 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)
}

Jetzt können wir komponieren diese Aktionen in play:

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

Natürlich wird das Spiel im wirklichen Leben dynamischer sein, aber es ist Essen genug für mein kleines Beispiel :)

Wir können es jetzt ausführen, um den endgültigen Status des Spiels zu sehen:

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

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

Also haben wir den Boss kaum getroffen und eine der Einheiten ist gestorben, RIP.

Der Punkt hier ist die Zusammensetzung . State(was nur eine Funktion ist S => (A, S)) ermöglicht es Ihnen, Aktionen zu definieren, die zu Ergebnissen führen, und auch einen Zustand zu manipulieren, ohne zu viel zu wissen, woher der Zustand kommt. Der MonadTeil gibt Ihnen Komposition, damit Ihre Aktionen komponiert werden können:

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

und so weiter.

PS Was Unterschiede zwischen get, putund modify:

modifykann als getund putzusammen gesehen werden:

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

oder einfach

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

Wenn Sie also verwenden, verwenden modifySie konzeptionell getund put, oder Sie können sie einfach alleine verwenden.

Alexey Raga
quelle