it-roy-ru.com

Примеры скалад государственной монады

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

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

Редактировать: здесь это потрясающая 2-часовая презентация о государственной монаде.

75
huynhjl

Я предполагаю, 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]()))
82
huynhjl

Я наткнулся на интересное сообщение в блоге 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))
}
15
huynhjl

Вот очень маленький пример того, как можно использовать 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, или вы можете просто использовать их в одиночку.

13
Alexey Raga