Приклади монад штату Скалаз


77

Я не бачив багато прикладів монади штату Скалаз. Існує цей приклад, але його важко зрозуміти, і, здається, є лише одне питання щодо переповнення стека.

Я збираюся опублікувати кілька прикладів, з якими я бавився, але я би вітав додаткові. Крім того, якщо хто - небудь може дати приклад про те, чому init, modify, putі getsвикористовуються для цього було б здорово.

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

Відповіді:


83

Я припускаю, що 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)]]

Використовуйте послідовність, щоб отримати a 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]()))

Хіба Stateмонада не є "державним перетворювачем" у реальності? І як друге запитання: чи є якийсь більш приємний спосіб поєднати кістки та підсумки в одній державній монаді? Як би ви це зробили з огляду на дві монади?
ziggystar

@ziggystar, технічно StateFreqі StateRandomє монадами. Я не думаю State[S, x], що це монада-трансформатор, оскільки Sне повинна бути монадою. Щодо більш приємного способу поєднання, мені теж цікаво. Я не бачу нічого, очевидно, легкодоступного. Можливо, це stateTможе допомогти, але я ще не зрозумів цього.
huynhjl

Я писав не "монадний трансформатор", а "державний трансформатор". Ці State[S, x]'об'єкти не тримають державу , але і перетворення останнього. Просто я думаю, що назву можна було б обрати менш заплутаною. Це не ваша відповідь, а лише Скалаз.
ziggystar

@ziggystar, я придумав, як використати stateTкомбінацію зсуву та підсумовування в єдину StateTмонаду! Див. Stackoverflow.com/q/7782589/257449 . Застряг до кінця, тоді я traverseврешті зрозумів .
huynhjl

1
@DavidB., Схожий на оператор синтаксис, схоже, зник і його замінили імена. !є зараз eval; ~>є зараз exec.
huynhjl

15

Я натрапив на цікавий допис у блозі 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))
}

14

Ось дуже маленький приклад того, як 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, або ви можете просто використовувати їх поодинці.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.