Примеры скалад государственной монады - PullRequest
75 голосов
/ 12 октября 2011

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

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

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

Ответы [ 3 ]

82 голосов
/ 12 октября 2011

Полагаю, 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]()))
15 голосов
/ 16 октября 2011

Я наткнулся на интересную запись в блоге 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))
}
13 голосов
/ 22 декабря 2015

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

Итак, мы едва ударили босса, и один из юнитов погиб, РИП.

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

...