在Haskell中维护复杂的状态 [英] Maintaining complex state in Haskell

查看:114
本文介绍了在Haskell中维护复杂的状态的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设你在Haskell中构建了一个相当大的仿真。有许多不同类型的实体,其属性随着模拟的进展而更新。比方说,为了举例,你的实体被称为猴子,大象,熊等。

你最喜欢维护这些实体状态的方法是什么?



我想到的第一个也是最明显的方法是这样的:

  mainLoop :: [Monkey]  - > [Elephant]  - > [熊]  - >字符串
mainLoop猴子大象熊=
让猴子'= updateMonkeys猴子
大象'= updateElephants大象
熊'= updateBears熊
如果shouldExit猴子大象熊然后完成其他
mainLoop猴子'大象熊'

它已经很丑在 mainLoop 函数签名中明确提到了每种类型的实体。你可以想象如果你有20种类型的实体,它会变得非常糟糕。 (20对于复杂的模拟并不是不合理的。)所以我认为这是一种不可接受的方法。但是它的保存优点是像 updateMonkeys 这样的函数在它们的功能上是非常明确的:它们取得一个Monkeys列表并返回一个新列表。



所以接下来的想法是把所有的东西放到一个保存所有状态的大数据结构中,因此清理了 mainLoop 的签名: p>

  mainLoop :: GameState  - >字符串
mainLoop gs0 =
let gs1 = updateMonkeys gs0
gs2 = updateElephants gs1
gs3 = updateBears gs2
in
if shouldExit gs0 thenDoneelse
mainLoop gs3

有些人会建议我们包装 GameState 在State Monad中,并在 do 中调用 updateMonkeys 等。没关系。有人宁愿建议我们用功能组合清理它。我觉得还好。 (顺便说一下,我是Haskell的新手,所以也许我错了一些。)



但问题是,像 updateMonkeys 不会为您提供来自其类型签名的有用信息。你不能确定他们做了什么。当然, updateMonkeys 是一个描述性的名字,但这没什么安慰。当我传入神物并说请更新我的全球状态时,我觉得我们重新回到了当务之急的世界。它的感觉就像全局变量的另一个名字:你有一个函数可以对全局状态执行 ,你称之为,并且你希望最好。 (我想你还是避免了一些并发问题,这些问题会在全局变量中出现在命令式程序中,但是,并发并不是全局变量唯一的问题。)



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

  stomp :: Elephant  - >猴子 - > (大象,猴子)
stomp大象猴=
(elongateEvilGrin大象,递减健康猴子)

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



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

只是为了比较,下面是我可能会解决OOP世界中的问题。会有 Monkey Elephant 等对象。我可能有类方法在所有活动物中进行查找。也许你可以通过位置,ID,查找任何内容。由于查找函数的基础数据结构,它们将保持分配在堆上。 (我假定GC或引用计数。)他们的成员变量会一直变化。任何类别的任何方法都可以改变任何其他类别的任何活体动物。例如。一个 Elephant 可以有一个 stomp 方法来减少传入的 Monkey 对象,并且不需要传递那个



同样,在Erlang或其他面向actor的设计中,您可以解决这些问题相当优雅:每个角色都保持自己的循环,从而保持自己的状态,所以你永远不需要神的对象。消息传递允许一个对象的活动触发其他对象中的更改,而无需传递大量的东西来备份调用堆栈。然而我听说它说Haskell中的演员们对此不以为然。 解决方案

答案是功能性反应式编程(FRP)。它是两种编码风格的混合体:组件状态管理和时间依赖值。由于FRP实际上是一整套设计模式,我希望更具体:我推荐 Netwire



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

虽然这听起来很复杂且效率低下,但它实际上只是一个非常薄的常规功能层。 Netwire实施的设计模式受到AFRP(Arrowized Functional Reactive Programming)的启发。它可能有不同的值得拥有自己的名字(WFRP?)。您可能需要阅读教程



在任何情况下,都会有一个小的演示。您的积木是电线:

  myWire :: WireP AB 

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

  particle :: WireP [Particle] Particle 

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

  time :: WireP a Time 

这是时间 (= Double )的随时间变化的值。那么,现在是时间本身了(每当有线网络启动时,从0开始计数)。由于它不依赖于其他随时间变化的值,因此您可以随意输入它,因此可以输入多态输入类型。也有恒定的线(时变值不会随着时间而改变):

 纯15 ::连线整数

- 甚至:
15 ::连线整数



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

  integral_ 3。 15 

这给你一个15倍实时速度的时钟(随着时间的推移积分为15) 3(积分常数)。由于各种类别的实例,电线非常方便组合。您可以使用常规操作员以及应用样式或箭头样式。想要一个从10开始并且是实时速度的两倍的时钟?

  10 + 2 *时间

想要一个以(0,0)速度开始和(0,0)并以(2,1)加速的粒子秒每秒?

  integral_(0,0)。 integral_(0,0)。纯(2,1)

想在用户按下空格键时显示统计信息吗?

  stats。 keyDown空格键< |> 目前已禁用的统计数据

这只是Netwire能为您做的一小部分。 p>

Suppose you're building a fairly large simulation in Haskell. There are many different types of entities whose attributes update as the simulation progresses. Let's say, for the sake of example, that your entities are called Monkeys, Elephants, Bears, etc..

What is your preferred method for maintaining these entities' states?

The first and most obvious approach I thought of was this:

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'

It's already ugly having each type of entity explicitly mentioned in the mainLoop function signature. You can imagine how it would get absolutely awful if you had, say, 20 types of entities. (20 is not unreasonable for complex simulations.) So I think this is an unacceptable approach. But its saving grace is that functions like updateMonkeys are very explicit in what they do: They take a list of Monkeys and return a new one.

So then the next thought would be to roll everything into one big data structure that holds all state, thus cleaning up the signature of 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

Some would suggest that we wrap GameState up in a State Monad and call updateMonkeys etc. in a do. That's fine. Some would rather suggest we clean it up with function composition. Also fine, I think. (BTW, I'm a novice with Haskell, so maybe I'm wrong about some of this.)

But then the problem is, functions like updateMonkeys don't give you useful information from their type signature. You can't really be sure what they do. Sure, updateMonkeys is a descriptive name, but that's little consolation. When I pass in a god object and say "please update my global state," I feel like we're back in the imperative world. It feels like global variables by another name: You have a function that does something to the global state, you call it, and you hope for the best. (I suppose you still avoid some concurrency problems that would be present with global variables in an imperative program. But meh, concurrency isn't nearly the only thing wrong with global variables.)

A further problem is this: Suppose the objects need to interact. For example, we have a function like this:

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

Say this gets called in updateElephants, because that's where we check to see if any of the elephants are in stomping range of any monkeys. How do you elegantly propagate the changes to both the monkeys and elephants in this scenario? In our second example, updateElephants takes and returns a god object, so it could effect both changes. But this just muddies the waters further and reinforces my point: With the god object, you're effectively just mutating global variables. And if you're not using the god object, I'm not sure how you'd propagate those types of changes.

What to do? Surely many programs need to manage complex state, so I'm guessing there are some well-known approaches to this problem.

Just for the sake of comparison, here's how I might solve the problem in the OOP world. There would be Monkey, Elephant, etc. objects. I'd probably have class methods to do lookups in the set of all live animals. Maybe you could lookup by location, by ID, whatever. Thanks to the data structures underlying the lookup functions, they'd stay allocated on the heap. (I'm assuming GC or reference counting.) Their member variables would get mutated all the time. Any method of any class would be able to mutate any live animal of any other class. E.g. an Elephant could have a stomp method that would decrement the health of a passed-in Monkey object, and there would be no need to pass that

Likewise, in an Erlang or other actor-oriented design, you could solve these problems fairly elegantly: Each actor maintains its own loop and thus its own state, so you never need a god object. And message passing allows one object's activities to trigger changes in other objects without passing a bunch of stuff all the way back up the call stack. Yet I have heard it said that actors in Haskell are frowned upon.

解决方案

The answer is functional reactive programming (FRP). It it a hybrid of two coding styles: component state management and time-dependent values. Since FRP is actually a whole family of design patterns, I want to be more specific: I recommend Netwire.

The underlying idea is very simple: You write many small, self-contained components each with their own local state. This is practically equivalent to time-dependent values, because each time you query such a component you may get a different answer and cause a local state update. Then you combine those components to form your actual program.

While this sounds complicated and inefficient it's actually just a very thin layer around regular functions. The design pattern implemented by Netwire is inspired by AFRP (Arrowized Functional Reactive Programming). It's probably different enough to deserve its own name (WFRP?). You may want to read the tutorial.

In any case a small demo follows. Your building blocks are wires:

myWire :: WireP A B

Think of this as a component. It is a time-varying value of type B that depends on a time-varying value of type A, for example a particle in a simulator:

particle :: WireP [Particle] Particle

It depends on a list of particles (for example all currently existing particles) and is itself a particle. Let's use a predefined wire (with a simplified type):

time :: WireP a Time

This is a time-varying value of type Time (= Double). Well, it's time itself (starting at 0 counted from whenever the wire network was started). Since it doesn't depend on another time-varying value you can feed it whatever you want, hence the polymorphic input type. There are also constant wires (time-varying values that don't change over time):

pure 15 :: Wire a Integer

-- or even:
15 :: Wire a Integer

To connect two wires you simply use categorical composition:

integral_ 3 . 15

This gives you a clock at 15x real time speed (the integral of 15 over time) starting at 3 (the integration constant). Thanks to various class instances wires are very handy to combine. You can use your regular operators as well as applicative style or arrow style. Want a clock that starts at 10 and is twice the real time speed?

10 + 2*time

Want a particle that starts and (0, 0) with (0, 0) velocity and accelerates with (2, 1) per second per second?

integral_ (0, 0) . integral_ (0, 0) . pure (2, 1)

Want to display statistics while the user presses the spacebar?

stats . keyDown Spacebar <|> "stats currently disabled"

This is just a small fraction of what Netwire can do for you.

这篇关于在Haskell中维护复杂的状态的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆