Haskell中复杂状态的维护

Haskell中复杂状态的维护,haskell,functional-programming,state,Haskell,Functional Programming,State,假设您正在Haskell中构建一个相当大的模拟。有许多不同类型的实体,其属性会随着模拟的进行而更新。比如说,为了举例,你的实体被称为猴子、大象、熊等等 维护这些实体状态的首选方法是什么 我想到的第一个也是最明显的方法是: mainLoop :: [Monkey] -> [Elephant] -> [Bear] -> String mainLoop monkeys elephants bears = let monkeys' = updateMonkeys monke

假设您正在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
函数签名中明确提到每种类型的实体已经很难看了。你可以想象,如果你有,比如说,20种类型的实体,情况会变得多么糟糕。(20对于复杂的模拟来说并非不合理。)因此我认为这是一种不可接受的方法。但它的可取之处在于,像
updateMonkey
这样的函数非常明确:它们获取猴子列表并返回一个新的

因此,下一个想法是将所有内容整合到一个包含所有状态的大数据结构中,从而清理
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
有些人会建议我们将游戏状态包装成一个状态单子,并调用
do
中的
updatemonkey
等。那很好。有些人更愿意建议我们用函数组合来清理它。我想也不错。(顺便说一句,我是Haskell的新手,所以我可能在某些方面错了。)

但是问题是,像
updateMonkey
这样的函数不能从它们的类型签名中提供有用的信息。你不能确定他们是干什么的。当然,
updateMonkeys
是一个描述性的名称,但这并不是什么安慰。当我经过一家公司,说“请更新我的全球状态”时,我感觉我们回到了命令式的世界。它的另一个名字感觉像是全局变量:你有一个函数,它对全局状态做了一些事情,你调用它,你希望它是最好的。(我想您仍然可以避免命令式程序中全局变量出现的一些并发问题。但是,并发性并不是全局变量的唯一错误。)

另一个问题是:假设对象需要交互。例如,我们有这样一个函数:

stomp :: Elephant -> Monkey -> (Elephant, Monkey)
stomp elephant monkey =
  (elongateEvilGrin elephant, decrementHealth monkey)
假设这在
updateElephants
中被调用,因为那是我们检查大象是否在任何猴子的踩踏范围内的地方。在这个场景中,您如何优雅地将这些变化传播给猴子和大象?在我们的第二个示例中,
updateElephants
获取并返回一个god对象,因此它可以影响这两个更改。但这只是进一步搅乱了局势,强化了我的观点:对于上帝对象,你实际上只是在改变全局变量。如果你没有使用god对象,我不确定你会如何传播这些类型的变化

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

为了比较起见,下面是我在OOP世界中解决这个问题的方法。将有
猴子
大象
等对象。我可能会有类方法来查找所有活体动物的集合。也许你可以按位置,ID,随便什么查找。由于查找函数背后的数据结构,它们将保持在堆上的分配状态。(我假设GC或引用计数)它们的成员变量会一直发生变异。任何种类的任何方法都能使任何其他种类的活动物发生变异。例如,
Elephant
可以有一个
stomp
方法,该方法会降低传入的
Monkey
对象的运行状况,而无需传递该对象

类似地,在Erlang或其他面向参与者的设计中,您可以相当优雅地解决这些问题:每个参与者维护自己的循环,从而维护自己的状态,因此您永远不需要god对象。消息传递允许一个对象的活动触发其他对象中的更改,而无需一路将一堆内容传递回调用堆栈。然而,我听说哈斯克尔的演员们不受欢迎。

答案是(FRP)。它是两种编码风格的混合:组件状态管理和时间相关值。由于FRP实际上是一个完整的设计模式系列,我想更具体一点:我推荐

其基本思想非常简单:您编写了许多小的、自包含的组件,每个组件都有自己的局部状态。这实际上相当于依赖于时间的值,因为每次查询此类组件时,您可能会得到不同的答案并导致局部状态更新。然后将这些组件组合成实际的程序

虽然这听起来复杂且低效,但实际上它只是围绕常规函数的一个非常薄的层。Netwire实现的设计模式受AFRP(箭头化功能反应式编程)的启发。它可能有足够的不同,值得它自己的名字(WFRP?)。你可能想看看这本书

在任何情况下,一个小演示如下。您的构建块是:

myWire :: WireP A B
将其视为一个组件。它是类型B的时变值,取决于类型a的时变值,例如模拟器中的粒子:

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

这是时间(=双精度)类型的时变值。好的,时间本身(从0开始,从有线网络启动时开始计算)。因为它不依赖于另一个时变值,所以您可以根据需要为它提供任何信息,因此是多态输入类型。还有恒定导线(随时间变化的值不会随时间变化):

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

integral_ 3 . 15
这将为您提供一个以15倍实时速度(15随时间的积分)从3(积分常数)开始的时钟
pure 15 :: Wire a Integer

-- or even:
15 :: Wire a Integer
integral_ 3 . 15
10 + 2*time
integral_ (0, 0) . integral_ (0, 0) . pure (2, 1)
stats . keyDown Spacebar <|> "stats currently disabled"