gpt4 book ai didi

haskell - 在 Haskell 中维护复杂状态

转载 作者:行者123 更新时间:2023-12-02 02:33:34 24 4
gpt4 key购买 nike

假设您正在 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 对于复杂的模拟来说并非不合理。)所以我认为这是一种 Not Acceptable 方法。但它的优点是函数像 updateMonkeys他们的工作非常明确:他们获取猴子列表并返回一个新的。

那么接下来的想法是将所有内容滚动到一个包含所有状态的大数据结构中,从而清理 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进入状态单子(monad)并调用 updateMonkeys等在 do .没关系。有些人宁愿建议我们用函数组合来清理它。也很好,我想。 (顺便说一句,我是 Haskell 的新手,所以也许我对其中的一些错误。)

但问题是,像 updateMonkeys 这样的函数不要从他们的类型签名中给你有用的信息。你不能确定他们在做什么。当然, updateMonkeys是一个描述性的名字,但这是一个小小的安慰。当我传入 god object并说“请更新我的全局状态”,我觉得我们回到了命令世界。感觉就像是另一个名字的全局变量:你有一个对全局状态做某事的函数,你调用它,你希望它是最好的。 (我想你仍然避免了一些在命令式程序中会出现在全局变量中的并发问题。但是,并发几乎不是全局变量唯一的错误。)

另一个问题是:假设对象需要交互。例如,我们有一个这样的函数:
stomp :: Elephant -> Monkey -> (Elephant, Monkey)
stomp elephant monkey =
(elongateEvilGrin elephant, decrementHealth monkey)

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

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

只是为了比较,这就是我可能如何解决 OOP 世界中的问题。会有 Monkey , Elephant等对象。我可能有类方法可以在所有活体动物的集合中进行查找。也许您可以按位置、ID 等进行查找。由于查找函数底层的数据结构,它们将保持在堆上分配。 (我假设 GC 或引用计数。)它们的成员变量会一直发生变异。任何类的任何方法都可以使任何其他类的任何活体动物发生变异。例如。一个 Elephant可能有 stomp会降低传入的健康状况的方法 Monkey对象,并且没有必要通过它

同样,在 Erlang 或其他面向角色的设计中,您可以相当优雅地解决这些问题:每个角色都维护自己的循环,从而维护自己的状态,因此您永远不需要上帝对象。消息传递允许一个对象的事件触发其他对象的更改,而无需将一堆东西一直传递到调用堆栈。然而,我听说 Haskell 中的 Actor 不受欢迎。

最佳答案

答案是functional reactive programming (玻璃钢)。它混合了两种编码风格:组件状态管理和时间相关值。由于FRP实际上是一整套设计模式,所以我想更具体一点:我推荐Netwire .

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

虽然这听起来很复杂且效率低下,但实际上它只是围绕常规函数的一个非常薄的层。 Netwire 实现的设计模式受到 AFRP(箭头函数响应式编程)的启发。它可能足够不同,值得拥有自己的名字(WFRP?)。您可能想阅读 tutorial .

无论如何,下面是一个小演示。你的构建 block 是电线:

myWire :: WireP A B

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

它依赖于粒子列表(例如所有当前存在的粒子)并且本身就是一个粒子。让我们使用预定义的线(具有简化类型):
time :: WireP a Time

这是 Time (= Double) 类型的时变值。好吧,是时候了(从 0 开始,从有线网络启动时算起)。由于它不依赖于另一个随时间变化的值,因此您可以随心所欲地提供它,因此是多态输入类型。还有一些固定线(不随时间变化的时变值):
pure 15 :: Wire a Integer

-- or even:
15 :: Wire a Integer

要连接两条线,您只需使用分类组合:
integral_ 3 . 15

这为您提供了一个从 3(积分常数)开始以 15 倍实时速度(15 随时间积分)的时钟。由于各种类实例,电线非常方便组合。您可以使用常规运算符以及应用样式或箭头样式。想要一个从 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 可以为您做的一小部分。

关于haskell - 在 Haskell 中维护复杂状态,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15467925/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com