在Haskell中维护复杂的状态

假设你正在Haskell中构建一个相当大的模拟。 有许多不同类型的实体,其属性随着模拟的进展而更新。 比方说,为了举例,你的实体被称为猴子,大象,熊等。

您维护这些实体的状态的首选方法是什么?

我想到的第一个也是最明显的方法是这样的:

mainLoop :: [Monkey] -> [Elephant] -> [Bear] -> String
mainLoop monkeys elephants bears =
  let monkeys'   = updateMonkeys   monkeys
      elephants' = updateElephants elephants
      bears'     = updateBears     bears
  in
    if shouldExit monkeys elephants bears then "Done" else
      mainLoop monkeys' elephants' bears'

mainLoop函数签名中明确提到每种类型的实体已经很难mainLoop 。 你可以想象如果你有20种类型的实体,它会变得非常糟糕。 (20对于复杂的模拟并不是不合理的。)所以我认为这是一个不可接受的方法。 但是它的updateMonkeys在于像updateMonkeys这样的函数在它们的功能上非常明确:它们取得一个Monkeys列表并返回一个新列表。

那么接下来的想法就是把所有的状态都放到一个大数据结构中,这样就清理了mainLoop的签名:

mainLoop :: GameState -> String
mainLoop gs0 =
  let gs1 = updateMonkeys   gs0
      gs2 = updateElephants gs1
      gs3 = updateBears     gs2
  in
    if shouldExit gs0 then "Done" else
      mainLoop gs3

有人会建议我们将GameState封装在State Monad中,并在do调用updateMonkeys等。 没关系。 有些人宁愿建议我们用功能组合清理它。 我觉得还好。 (顺便说一下,我是Haskell的新手,所以也许我错了一些。)

但问题是,像updateMonkeys这样的函数不会从它们的类型签名中为您提供有用的信息。 你不能确定他们做了什么。 当然, updateMonkeys是一个描述性的名字,但这没什么安慰。 当我传入一个神物并说“请更新我的全球状态”时,我觉得我们已经回到了当务之急的世界。 它的感觉就像全局变量的另一个名字:你有一个函数可以对全局状态做一些事情,你称之为,并且你希望最好。 (我想你仍然可以避免一些并发性问题,这些问题可能会在全局变量中出现在一个命令式程序中,但是,并发并不仅仅是全局变量的唯一问题。)

还有一个问题是:假设对象需要交互。 例如,我们有一个这样的功能:

stomp :: Elephant -> Monkey -> (Elephant, Monkey)
stomp elephant monkey =
  (elongateEvilGrin elephant, decrementHealth monkey)

说这在updateElephants被调用,因为这是我们检查是否有任何大象正在跺脚任何猴子的范围。 在这种情况下,你如何优雅地将变化传播给猴子和大象? 在我们的第二个例子中, updateElephants接受并返回一个god对象,所以它可以影响两个变化。 但是这只是进一步混淆了水域并强化了我的观点:用上帝的对象,你实际上只是在改变全局变量。 如果你不使用神物,我不确定你将如何传播这些类型的变化。

该怎么办? 当然,很多程序需要管理复杂的状态,所以我猜测这个问题有一些众所周知的方法。

只是为了比较,这里是我如何解决OOP世界中的问题。 会有MonkeyElephant等物体。 我可能有类方法在所有活动物中进行查找。 也许你可以通过位置,ID,查找任何内容。 由于查找函数的基础数据结构,它们将保持分配在堆上。 (我假定GC或引用计数。)他们的成员变量会一直变化。 任何类别的任何方法都可以改变任何其他类别的任何活体动物。 例如,一头Elephant可以采用一种可以减少传入的Monkey物体的健康的stomp方法,并且不需要通过

同样,在Erlang或其他面向演员的设计中,您可以相当优雅地解决这些问题:每个actor都维护自己的循环,从而维护自己的状态,因此您永远不需要上帝对象。 消息传递允许一个对象的活动触发其他对象中的更改,而无需传递大量的东西来备份调用堆栈。 然而我听说它说Haskell中的演员们都皱起了眉头。


答案是功能性反应式编程(FRP)。 它是两种编码风格的混合体:组件状态管理和时间依赖值。 由于FRP实际上是一整套设计模式,我希望更具体:我推荐Netwire。

其基本思想非常简单:您可以编写许多小型自包含组件,每个组件都有自己的本地状态。 这实际上与时间相关的值相当,因为每次查询这样的组件时,您可能会得到不同的答案并导致本地状态更新。 然后你将这些组件组合起来形成你的实际程序。

虽然这听起来很复杂且效率低下,但它实际上只是一个非常薄的常规功能层。 Netwire实施的设计模式受到AFRP(Arrowized Functional Reactive Programming)的启发。 它可能有不同的值得拥有自己的名字(WFRP?)。 你可能想阅读教程。

无论如何,一个小演示如下。 你的积木是电线:

myWire :: WireP A B

把它看作是一个组件。 它是B类型的时变值,取决于类型A的时变值,例如模拟器中的粒子:

particle :: WireP [Particle] Particle

它取决于粒子列表(例如所有当前存在的粒子),本身就是一个粒子。 让我们使用预定义的线(使用简化类型):

time :: WireP a Time

这是时间类型的时间变化值(= Double)。 那么,现在是时间本身了(每当有线网络启动时,从0开始计数)。 由于它不依赖于其他随时间变化的值,因此您可以随意输入它,因此可以输入多态输入类型。 也有恒定的线(随时间变化的时变值):

pure 15 :: Wire a Integer

-- or even:
15 :: Wire a Integer

要连接两根导线,只需使用分类组合:

integral_ 3 . 15

这为您提供15倍实时速度(从15开始的时间积分)从3开始(积分常数)的时钟。 感谢各种类别的例子,电线非常方便组合。 您可以使用常规操作员以及应用样式或箭头样式。 想要一个从10开始的时钟,是实时速度的两倍?

10 + 2*time

想要一个以(0,0)速度开始和(0,0)并以每秒每秒(2,1)加速的粒子?

integral_ (0, 0) . integral_ (0, 0) . pure (2, 1)

想要在用户按空格键时显示统计信息?

stats . keyDown Spacebar <|> "stats currently disabled"

这只是Netwire能为你做的一小部分。


我知道这是老话题。 但是我现在面临同样的问题,试图通过exerciseism.io来实施铁栅栏密码练习。 在Haskell看到这样一个普遍问题如此糟糕的关注令人非常失望。 我不认为要做一些简单的事情来维持我需要学习玻璃钢的状态。 所以,我继续搜索,发现解决方案看起来更直接 - 状态monad:https://en.wikibooks.org/wiki/Haskell/Understanding_monads/State

链接地址: http://www.djcxy.com/p/58757.html

上一篇: Maintaining complex state in Haskell

下一篇: choosing a monad at runtime