Scalaz状态单子例子

77

我还没有看到很多Scalaz State Monad的示例。有这个例子,但很难理解,并且似乎只有一个其他问题在Stack Overflow上。

我将发布我玩过的几个示例,但欢迎提供更多示例。此外,如果有人可以提供关于为什么使用initmodifyputgets的示例,那就太好了。

编辑:这里是一份精彩的2小时演示关于状态单子。

3个回答

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)]]

使用 sequence 来获取一个 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)
}

现在使用 traverse 对 tenDoubleThrows 应用 freqSum。traverse 等同于 map(freqSum).sequence。请保留 html 标签。
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,也许其他人可以展示如何增强StateFreqStateRandom以执行组合计算。我发现两个状态变换器的组合可以像这样结合:
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]()))

我没有写“monad transformer”,而是“state transformer”。State[S, x]对象不持有状态,而是后者的转换。只是我认为名称可以选择得更清晰一些。这与你的回答无关,而是关于Scalaz的问题。 - ziggystar
在 def dice() 中,"state" 应该是 "State"。 - Tvaroh
@Tvaroh,在提问/回答时我使用的是scalaz 6.x。你现在评论时是在scalaz 7.x的上下文中吗? - huynhjl
eval() 函数中使用 ! 语法是否仍然有效?对我来说,在 scalaz 7 中并没有找到 ! - David B.
1
@DavidB.,操作符式的语法似乎已经消失了,被名称所取代。!现在是eval~>现在是exec - huynhjl
显示剩余3条评论

15
我发现了一篇有趣的博客文章Grok Haskell Monad Transformers来自sigfp,该博客提供了一个通过单子变换器应用两个状态单子的例子。这里是一个Scalaz翻译。 第一个例子展示了一个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)

所以我这里有一个使用initmodify的例子。经过一番尝试,init[S]非常方便生成一个State[S,S]值,但它允许的另一件事是在for循环中访问状态。modify[S]是在for循环中转换状态的方便方法。因此,上面的示例可以理解为:

  • a <- init[Int]: 从一个Int状态开始,并将其设置为State[Int, _]单子包装的值,然后将其绑定到a
  • _ <- modify[Int](_ + 1): 增加Int状态
  • b <- init[Int]: 取出Int状态并将其绑定到b(与a相同,但现在状态已增加)
  • 使用ab产生一个State[Int, (Int, Int)]值。

State[S,A]中,for循环语法已经使得在A端上操作变得微不足道。 initmodifyputgets提供了一些工具来在State[S,A]中操作S端。

博客文章的第二个例子被翻译为:

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 允许我们在任意单子上添加状态转换。 在这种情况下,状态是一个递增的 Int。 如果我们调用 stTrans ! 0,我们最终会得到 M[String]。在我们的例子中,MStateString,所以最终我们会得到 StateString[String],它是 State[String, String]
关键部分在于我们想要从 stTrans 中提取出Int 状态值。这就是 initT 的作用。 它只是创建一个对象,以一种可以与 stTrans 进行 flatMap 的方式访问状态。
编辑:事实证明,如果真正地重用 test1test2,就可以避免所有这些笨拙。它们方便地存储了返回元组的 _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的非常简单的示例:

让我们定义一个小的“游戏”,其中一些游戏单位正在与BOSS(也是游戏单位)战斗。

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)))
}

当游戏进行时,我们希望跟踪游戏状态,因此让我们定义我们的“动作”与状态单子相关:

让我们狠狠地攻击boss,使他失去10点生命值:health

def strike : State[Game, Unit] = modify[Game] { s =>
  s.copy(
    boss = s.boss.copy(health = s.boss.health - 10)
  )
}

而且BOSS也可以反击!当BOSS反击时,每个队友会失去5点生命值

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”:

现在我们可以将这些动作组合成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. 有关getputmodify之间的区别:

modify可以被看作是getput的结合:

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时,您在概念上使用getput,或者您可以仅使用它们中的一个。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接