Примеры монад состояния Scalaz

Я не видел много примеров монады состояния scalaz. Есть это пример, но его сложно понять, и в стеке есть только один другой вопрос переполнение кажется.

Я собираюсь опубликовать несколько примеров, с которыми я играл, но я бы приветствовал дополнительные. Также было бы здорово, если бы кто-нибудь мог предоставить пример того, почему для этого используются init, modify, put и gets.

Изменить: здесь - это потрясающая двухчасовая презентация монады состояния.


person huynhjl    schedule 12.10.2011    source источник


Ответы (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]()))
person huynhjl    schedule 12.10.2011
comment
Разве монада State не преобразователь состояний на самом деле? И второй вопрос: есть ли лучший способ объединить броски костей и суммирование в одну монаду состояния? Как бы вы это сделали, учитывая две монады? - person ziggystar; 12.10.2011
comment
@ziggystar, технически StateFreq и StateRandom - монады. Я не думаю, что State[S, x] является преобразователем монад, поскольку S не обязательно должен быть монадой. Мне тоже интересно, как лучше комбинировать. Я не вижу ничего явно доступного. Может быть stateT может помочь, но я еще не понял этого. - person huynhjl; 13.10.2011
comment
Я писал не преобразователь монад, а преобразователь состояний. Объекты State[S, x]' содержат не состояние, а его преобразование. Просто я думаю, что название можно было бы выбрать менее запутанным. Это не о вашем ответе, а о Скалазе. - person ziggystar; 13.10.2011
comment
@ziggystar, я понял, как использовать stateT, чтобы объединить прокатку и суммирование в единую StateT монаду! См. stackoverflow.com/q/7782589/257449. Застрял ближе к концу, потом я понял traverse. - person huynhjl; 16.10.2011
comment
состояние в def dice () ... должно быть состоянием. - person Tvaroh; 26.09.2013
comment
@Tvaroh, я использовал scalaz 6.x во время этого вопроса / ответа. Вы комментируете в контексте scalaz 7.x? - person huynhjl; 18.10.2013
comment
Синтаксис ! для eval() все еще действителен? ! у меня в скалазе 7 не встречается. - person David B.; 22.12.2013
comment
@DavidB., Похоже, операторский синтаксис исчез и заменен именами. ! теперь eval; ~> теперь exec. - person huynhjl; 24.12.2013

Я наткнулся на интересную запись в блоге 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))
}
person huynhjl    schedule 16.10.2011

Вот очень небольшой пример того, как можно использовать 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]

и так далее.

P.S. Что касается различий между 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 или можете просто использовать их по отдельности.

person Alexey Raga    schedule 22.12.2015