Скаладская государственная монада, примеры
Я не видел много примеров монады состояния скалаза. Этот пример есть, но его трудно понять, и, кажется, есть только один вопрос о переполнении стека.
Я собираюсь опубликовать несколько примеров, с которыми я играл, но я бы приветствовал дополнительные. Также, если кто-то может привести пример того, почему init
, modify
, put
а также gets
используются для этого было бы здорово.
Редактировать: вот потрясающая 2-часовая презентация о государственной монаде.
3 ответа
Я предполагаю, что scalaz 7.0.x и следующий импорт (посмотрите историю ответов для scalaz 6.x):
import scalaz._
import Scalaz._
Тип состояния определяется как State[S, A]
где S
это тип государства и A
тип значения, которое оформляется. Основной синтаксис для создания значения состояния использует State[S, A]
функция:
// Create a state computation incrementing the state and returning the "str" value
val s = State[Int, String](i => (i + 1, "str"))
Чтобы выполнить вычисление состояния по начальному значению:
// 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")
Состояние может быть пропущено через вызовы функций. Сделать это вместо Function[A, B]
определить Function[A, State[S, B]]]
, Использовать State
функция...
import java.util.Random
def dice() = State[Random, Int](r => (r, r.nextInt(6) + 1))
Тогда for/yield
синтаксис может использоваться для составления функций:
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)
Вот еще один пример. Заполните список с помощью TwoDice()
расчеты состояния.
val list = List.fill(10)(TwoDice())
// List[scalaz.IndexedStateT[scalaz.Id.Id,Random,Random,(Int, Int)]]
Используйте последовательность, чтобы получить State[Random, List[(Int,Int)]]
, Мы можем предоставить псевдоним типа.
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))
Или мы можем использовать sequenceU
который выведет типы:
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))
Еще один пример с State[Map[Int, Int], Int]
вычислить частоту сумм в списке выше. freqSum
вычисляет сумму бросков и подсчитывает частоты.
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)
}
Теперь используйте траверс, чтобы применить freqSum
над tenDoubleThrows
, traverse
эквивалентно 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]]
Или более кратко, используя traverseU
выводить типы:
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]]
Обратите внимание, что, потому что State[S, A]
это псевдоним типа для StateT[Id, S, A]
, tenDoubleThrows2 в конечном итоге набирается как Id
, я использую copoint
превратить его обратно в List
тип.
Короче говоря, похоже, что ключом к использованию состояния является наличие функций, возвращающих функцию, изменяющую состояние и требуемое фактическое значение результата... Отказ от ответственности: я никогда не использовал state
в производственном коде, просто пытаюсь почувствовать это.
Дополнительная информация о комментарии @ziggystar
Я перестал пытаться использовать stateT
может быть кто-то еще может показать, если StateFreq
или же StateRandom
может быть дополнен для выполнения комбинированных вычислений. Вместо этого я обнаружил, что состав двух преобразователей состояния можно объединить так:
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
}
Это основано на g
будучи однопараметрической функцией, получающей результат первого преобразователя состояния и возвращающей преобразователь состояния. Тогда будет работать следующее:
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]()))
Я наткнулся на интересное сообщение в блоге Grok Haskell Monad Transformers от sigfp, в котором есть пример применения двух монад состояния через преобразователь монад. Вот перевод скаляза.
Первый пример показывает 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)
Итак, у меня есть пример использования init
а также modify
, После игры с этим немного, init[S]
оказывается действительно удобно генерировать State[S,S]
значение, но другая вещь, которую он позволяет, это получить доступ к состоянию внутри для понимания. modify[S]
это удобный способ преобразовать состояние внутри для понимания. Таким образом, приведенный выше пример можно прочитать как:
a <- init[Int]
: начать сInt
состояние, установите его как значение, заключенное вState[Int, _]
монада и привязать его кa
_ <- modify[Int](_ + 1)
: увеличитьInt
государствоb <- init[Int]
: взятьInt
заявить и связатьb
(так же, как дляa
но сейчас состояние увеличивается)- дать
State[Int, (Int, Int)]
значение с использованиемa
а такжеb
,
Синтаксис для понимания уже упрощает работу над A
сторона в State[S, A]
, init
, modify
, put
а также gets
предоставить некоторые инструменты для работы на S
сторона в State[S, A]
,
Второй пример в сообщении блога переводится как:
val test2 = for {
a <- init[String]
_ <- modify[String](_ + "1")
b <- init[String]
} yield (a, b)
val go2 = test2 ! "0"
// (String, String) = ("0","01")
Очень то же самое объяснение, что и test1
,
Третий пример более хитрый, и я надеюсь, что есть что-то более простое, что мне еще предстоит открыть.
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")
В этом коде stTrans
заботится о преобразовании обоих состояний (инкремент и суффикс с "1"
а также вытаскивая String
государство. stateT
позволяет добавить преобразование состояния в произвольную монаду M
, В этом случае государство является Int
это увеличивается. Если бы мы позвонили stTrans ! 0
мы бы в конечном итоге M[String]
, В нашем примере M
является StateString
так что мы в конечном итоге StateString[String]
который State[String, String]
,
Самое сложное в том, что мы хотим вытащить Int
государственное значение из stTrans
, Это то, что initT
для. Он просто создает объект, который дает доступ к состоянию так, как мы можем использовать FlatMap stTrans
,
Редактировать: Оказывается, всей этой неловкости можно избежать, если мы действительно повторно используем test1
а также test2
которые удобно хранить требуемые состояния в _2
Элемент их возвращаемых кортежей:
// same as test3:
val test31 = stateT[StateString, Int, (Int, String)]{ i =>
val (_, a) = test1 ! i
for (t <- test2) yield (a, (a, t._2))
}
Вот очень маленький пример того, как State
может быть использован:
Давайте определим небольшую "игру", в которой некоторые игровые юниты сражаются с боссом (который также является игровым юнитом).
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)))
}
Когда игра продолжается, мы хотим отслеживать состояние игры, поэтому давайте определим наши "действия" в терминах монады состояния:
Давайте сильно ударить босса, чтобы он потерял 10 из его health
:
def strike : State[Game, Unit] = modify[Game] { s =>
s.copy(
boss = s.boss.copy(health = s.boss.health - 10)
)
}
И босс может нанести ответный удар! Когда он все в партии теряет 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)
}
Теперь мы можем составить эти действия в play
:
def play = for {
_ <- strike
_ <- fireBreath
_ <- fireBreath
_ <- strike
} yield ()
Конечно, в реальной жизни игра будет более динамичной, но этого достаточно для моего маленького примера:)
Мы можем запустить его сейчас, чтобы увидеть окончательное состояние игры:
val res = play.exec(Game.init)
println(res)
>> Game(0,GameUnit(80),List(GameUnit(10)))
Таким образом, мы едва ударили по боссу, и одно из подразделений погибло, RIP.
Дело здесь в композиции. State
(это просто функция S => (A, S)
) позволяет определять действия, которые дают результаты, а также манипулировать некоторым состоянием, не зная слишком много, откуда оно приходит. Monad
часть дает вам композицию, чтобы ваши действия могли быть составлены:
A => State[S, B]
B => State[S, C]
------------------
A => State[S, C]
и так далее.
PS Что касается различий между get
, put
а также modify
:
modify
можно рассматривать как get
а также put
все вместе:
def modify[S](f: S => S) : State[S, Unit] = for {
s <- get
_ <- put(f(s))
} yield ()
или просто
def modify[S](f: S => S) : State[S, Unit] = get[S].flatMap(s => put(f(s)))
Поэтому, когда вы используете modify
вы концептуально используете get
а также put
Или вы можете просто использовать их в одиночку.