假设您正在 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 对于复杂的模拟来说并非不合理。)所以我认为这是一种不可接受的方法。但它的可取之处在于,like 函数updateMonkeys
的作用非常明确:它们获取一个 Monkey 列表并返回一个新的。
所以接下来的想法是将所有内容滚动到一个包含所有状态的大数据结构中,从而清理 的签名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 中结束,然后updateMonkeys
在do
. 没关系。有些人宁愿建议我们用函数组合来清理它。也很好,我想。(顺便说一句,我是 Haskell 的新手,所以也许我对其中的一些错误。)
但问题是,像updateMonkeys
这样的函数并没有从它们的类型签名中给你有用的信息。你不能确定他们在做什么。当然,updateMonkeys
是一个描述性的名称,但这并不能算是安慰。当我传入一个上帝对象并说“请更新我的全局状态”时,我感觉我们回到了命令世界。感觉就像是另一个名字的全局变量:你有一个对全局状态做某事的函数,你调用它,你希望它是最好的。(我想你仍然可以避免一些在命令式程序中会出现在全局变量中的并发问题。但是,并发几乎不是全局变量的唯一错误。)
另一个问题是:假设对象需要交互。例如,我们有一个这样的函数:
stomp :: Elephant -> Monkey -> (Elephant, Monkey)
stomp elephant monkey =
(elongateEvilGrin elephant, decrementHealth monkey)
假设这被调用updateElephants
,因为这是我们检查是否有任何大象在任何猴子的踩踏范围内的地方。在这种情况下,你如何优雅地将变化传播给猴子和大象?在我们的第二个示例中,updateElephants
接受并返回一个上帝对象,因此它可以影响两种更改。但这只会进一步混淆水域并强化我的观点:使用上帝对象,您实际上只是在改变全局变量。如果您不使用上帝对象,我不确定您将如何传播这些类型的更改。
该怎么办?当然,许多程序都需要管理复杂的状态,所以我猜测有一些众所周知的方法可以解决这个问题。
只是为了比较,这就是我可能如何解决 OOP 世界中的问题。会有Monkey
,Elephant
等对象。我可能有类方法可以在所有活体动物的集合中进行查找。也许您可以按位置、ID 等进行查找。由于查找函数底层的数据结构,它们将保持在堆上分配。(我假设 GC 或引用计数。)它们的成员变量会一直发生变异。任何类的任何方法都可以使任何其他类的任何活体动物发生变异。例如,一个Elephant
可以有一个stomp
方法会减少传入Monkey
对象的健康,并且不需要传递它
同样,在 Erlang 或其他面向角色的设计中,您可以相当优雅地解决这些问题:每个角色都维护自己的循环,从而维护自己的状态,因此您永远不需要上帝对象。消息传递允许一个对象的活动触发其他对象的更改,而无需将一堆东西一直传递到调用堆栈。然而,我听说 Haskell 中的演员不受欢迎。