在Haskell中维护复杂状态
假设你正在Haskell中build立一个相当大的模拟。 有许多不同types的实体,其属性随着仿真的进行而更新。 比方说,为了举例,你的实体被称为猴子,大象,熊等。
你维护这些实体的状态的首选方法是什么?
我想到的第一个也是最明显的方法是:
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
函数签名中明确提到每种types的实体已经很难mainLoop
。 你可以想象,如果你有20种实体,它将会变得非常糟糕。 (对于复杂的模拟来说,20是不合理的。)所以我认为这是一个不可接受的方法。 但是它的updateMonkeys
是像updateMonkeys
这样的函数在它们的function上是非常明确的:它们取得一个猴子列表并返回一个新的列表。
那么接下来的想法就是把所有的状态都放到一个大数据结构中,这样就清理了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
有些人会build议我们把GameState
封装在一个State Monad中, updateMonkeys
在一个do
调用updateMonkeys
等等。 没关系。 有人宁愿build议我们用function组合来清理它。 还好,我想。 (顺便说一下,我是Haskell的新手,所以也许我错了一些。)
但是接下来的问题是,像updateMonkeys
这样的函数不会从types签名中为您提供有用的信息。 你不能确定他们在做什么。 当然, updateMonkeys
是一个描述性的名字,但这没有什么安慰。 当我传入一个上帝的对象,并说“请更新我的全球状态”,我觉得我们回到了当务之急的世界。 它的感觉就像全局variables的另一个名字:你有一个对全局状态做一些事情的函数,你叫它,并且你希望最好。 (我想你还是要避免一些并发性问题,这些问题会在全局variables中出现在一个命令式程序中,但是,并发性并不是全局variables唯一的问题。
还有一个问题是:假设对象需要交互。 例如,我们有一个这样的function:
stomp :: Elephant -> Monkey -> (Elephant, Monkey) stomp elephant monkey = (elongateEvilGrin elephant, decrementHealth monkey)
说这在updateElephants
被调用,因为这是我们检查是否有任何大象在跺脚任何猴子的范围。 在这种情况下,你如何优雅地将变化传播给猴子和大象? 在我们的第二个例子中, updateElephants
接受并返回一个god对象,所以它可以影响两个变化。 但是,这只是进一步混淆了水域,并强调了我的观点:用神对象,你实际上只是在改变全局variables。 如果你不使用上帝的对象,我不知道你将如何传播这些types的变化。
该怎么办? 当然,许多程序需要pipe理复杂的状态,所以我猜测这个问题有一些众所周知的方法。
只是为了比较,这里是我可以解决OOP世界的问题。 会有Monkey
, Elephant
等物体。 我可能有class级的方法来查找所有活动物的集合。 也许你可以通过位置,ID,查找什么。 由于查找函数的底层数据结构,它们将保持分配在堆上。 (我假设GC或引用计数。)他们的成员variables会一直变化。 任何类别的任何方法都可以改变任何其他类别的活动物。 例如,一头Elephant
可能有一种能够减less过去的Monkey
物体的健康的stomp
方法,并且不需要通过
同样,在Erlang或其他面向主angular的devise中,你可以相当优雅地解决这些问题:每个angular色都维护着自己的循环,因此也就是自己的状态,所以你永远不需要上帝的对象。 消息传递允许一个对象的活动触发其他对象的变化,而不会传递一堆东西,一路支持调用堆栈。 然而,我听说它说,哈斯克尔的演员们都皱起了眉头。
答案是function性反应式编程 (FRP)。 它是两种编码风格的混合:组件状态pipe理和时间依赖值。 由于玻璃钢实际上是一个完整的devise模式家庭,我想要更具体一些:我推荐Netwire 。
其基本思想非常简单:您可以编写许多小型自包含组件,每个组件都有自己的本地状态。 这实际上相当于时间相关的值,因为每次查询这样的组件时,您可能会得到不同的答案并导致本地状态更新。 然后你将这些组件组合起来形成你的实际程序。
虽然这听起来很复杂,效率也不高,但实际上它只是规则函数的一个非常薄的层。 Netwire实现的devise模式受到AFRP(Arrowized Functional Reactive Programming)的启发。 这可能是不同的,值得拥有自己的名字(WFRP?)。 你可能想阅读教程 。
在任何情况下,一个小演示如下。 你的积木是电线:
myWire :: WireP AB
把这看作是一个组件。 Btypes的时变值取决于typesA的时变值,例如模拟器中的粒子:
particle :: WireP [Particle] Particle
它取决于一个粒子列表(例如所有当前存在的粒子),本身就是一个粒子。 我们使用预定义的线(使用简化types):
time :: WireP a Time
这是时间types的时间变化值(= Double )。 那么,现在是时间本身了(每当有线networking启动时,从0开始计数)。 由于它不依赖于另一个随时间变化的值,因此可以随意input它,因此也就是多态的inputtypes。 也有恒定的电线(随时间变化的时变值):
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能为你做的一小部分。