维持在Haskell复杂状态(Maintaining complex state in Haskel

2019-07-21 20:18发布

假设你正在构建在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是不是不合理的复杂的模拟)。所以我觉得这是一个不能接受的做法。 但它的可取之处,像功能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在一国单子起来,并打电话updateMonkeys在等do 。 没关系。 有的宁愿建议我们用函数组合清理。 也没事,我想。 (顺便说一句,我与哈斯克尔是新手,所以也许我错了一些这方面。)

但问题是,像功能updateMonkeys不给你从他们的签名类型的有用信息。 你不能真的相信他们做什么。 当然, updateMonkeys是一个描述性的名称,但这是一点安慰。 当我传递一个神的对象 ,并说:“请更新我的全局状态,”我觉得我们又回到祈使句的世界。 这感觉就像被另一名全局变量:您有做一些事情的全局状态的功能,你怎么称呼它,你希望最好的。 (我想你还是避免一些并发问题,这将是目前在一个命令式程序的全局变量。但是MEH,并发不是几乎唯一错的全局变量。)

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

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

之所以这样说,被称为updateElephants ,因为这是我们检查,看看是否有任何大象在任何猴子的跺脚范围。 你如何优雅地更改传播到在这种情况下,猴子和大象两者兼而有之? 在我们的第二个例子, updateElephants采用并返回一个神的对象,因此它可能影响方面的变化。 但是,这只是muddies水域进一步加强与我的观点:随着神的对象,你只是有效地变异全局变量。 如果您使用的不是神的对象,我不知道你会如何传播这些类型的变化。

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

只是为了便于比较,这里就是我可能会在OOP世界解决问题。 会有MonkeyElephant等对象。 我可能不得不类的方法做查找集合中的所有活的动物。 也许你可以通过位置查找,通过ID,等等。 由于潜在的查找功能,数据结构,他们会留在堆上分配。 (我假设GC或引用计数。)他们的成员变量会得到突变所有的时间。 任何类的任何方法将能够突变任何其他类的任何活的动物。 例如,一个Elephant可以有一个stomp ,将减少一个传入的健康方法Monkey对象,就没有必要传递

同样,在一个Erlang或其他面向演员设计,可以解决这些问题相当优雅:每个演员维护自己的循环,因此自己的状态,所以你永远需要一个神的对象。 和消息传递允许一个对象的活动触发其他对象的变化没有通过的东西一大堆一路备份调用堆栈。 然而,我已经听到有人说,演员在Haskell是令人难以接受的。

Answer 1:

答案是官能的反应性编程 (FRP)。 它,它的两个编码样式的混合:组分状态管理和时间依赖性的值。 由于玻璃钢实际上是整个家庭的设计模式,我想更具体:我建议Netwire 。

其基本思想很简单:你写了很多小的,自包含的每一个都有自己的本地状态的组件。 这实际上等同于时间相关的值,因为每次查询这样一个组件时,你可能会得到不同的答案,并引起局部状态更新。 然后,你把这些组件,形成你的实际程序。

虽然这听起来很复杂,效率低下它实际上是围绕常规功能只是很薄的一层。 通过Netwire实现的设计图案通过AFRP启发(Arrowized官能反应性编程)。 它可能存在很大差异,应该以自己的名义(WFRP?)。 您可能需要阅读教程 。

在任何情况下,一个小的演示如下。 你的积木是金属丝:

myWire :: WireP A B

可以把它看作一个组成部分。 它是类型B的随时间变化的值,该值取决于类型A的随时间变化的值,例如在一个仿真器中的粒子:

particle :: WireP [Particle] Particle

这取决于颗粒的列表(例如,所有当前存在的颗粒),并且其本身的颗粒。 让我们用一个预定义线(带的简化型):

time :: WireP a Time

这是类型时间 (= )的随时间变化的值。 嗯,这是时间本身(从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能为你做一小部分。



Answer 2:

我知道这是老话题了。 但我现在面临同样的问题,而试图实现铁路护栏网从exercism.io密码锻炼。 这是很令人失望地看到其在Haskell这样恶劣的关注这样一个共同的问题。 我不认为做一些为保持状态,我需要学习FRP一样简单。 所以,我继续使用Google,发现解决方案寻找更加简单-国家单子: https://en.wikibooks.org/wiki/Haskell/Understanding_monads/State



文章来源: Maintaining complex state in Haskell