Скаладская государственная монада, примеры

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

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

Другие вопросы по тегам