异步系统中的不可变数据

Immutable data in async systems

我很清楚在我的应用程序中使用不可变数据的好处,并且我对在简单的同步编程环境中使用这些不可变结构的想法相当满意。

Stack Overflow 上的某处有一个很好的示例,它描述了通过在一系列递归调用中传递状态来管理游戏状态,如下所示:

function update(state) {
  sleep(100)

  return update({
    ticks: state.ticks + 1,
    player: player
  })
}

我们可以在函数体中做一些任意的、没有副作用的工作,然后我们 return 一个新的状态,而不是改变旧的状态。

将其转换为简单的异步模型似乎相当容易,比如 Javascript。

function update(state) {
  const newState = {
    player,
    ticks: state.ticks + 1
  };

  setTimeout(update.bind(this, newState), 100);
}

然而,一旦我们有更多的异步事件源,保持状态不可变和函数纯净似乎变得更加困难。

如果我们向示例中添加一个点击事件,我们最终会得到如下所示的代码。

window.addEventListener('click', function() {
  // I have no idea what the state is
  // because only our update loop knows about it
});

现在显然,我不想改变此方法中的状态,但我需要访问状态以创建新状态,就像这样。

window.addEventListener('click', function() {
  const state = getState();

  createState({
    player,
    clicks: clicks + 1
  });
});

但这似乎需要某种可变状态管理器?

或者,我想我可以将点击事件添加到要在更新循环中处理的操作队列中,例如:

window.addEventListener('click', function() {
  createAction('click', e);
});

function update(state, actions) {
  const newState = {
    player,
    ticks: state.ticks + 1,
    clicks: state.clicks + actions.clicks.length
  };

  setTimeout(update.bind(this, newState, []), 100);
}

同样,这感觉并不是特别实用,并且至少依赖于沿途某处的一些可变状态。这些可能是来自主要使用可变状态和命令式面向对象编程的人的幼稚方法。

当存在多个异步事件源并且我们希望所有内容都是不可变的时,系统的设计是什么样的?或者至少,在这样的系统中控制可变性的良好模式是什么?

使用Object.freeze,您可以使对象不可变:

var o = {foo: "bar"};
Object.freeze(o);
o.abc = "xyz";
console.log(o);

将产生 {foo: "bar"}。请注意,在冻结变量上设置新 属性 的尝试将无提示地失败。

在这种情况下,创建新的状态对象后,在调用更新例程或触发事件之前将其冻结以防止进一步修改。

您可能有兴趣看一看 Redux。 Redux 采用类似的方法:

  • 它将整个应用程序状态建模为单个不可变对象。
  • 用户操作本质上是发送到 store 进行任意处理的消息。
  • 操作由 reducer 函数处理,形式为 f(previousState, action) => newState。这是一种比您的原始版本更实用的方法。
  • store 运行减速器并维护单个不可变的应用程序状态。

你是对的,这不是严格不变的,因为商店本身有一个对当前状态的可变引用。但正如其他人指出的那样,对于大多数不可变数据概念来说,这似乎不是问题。

除了 UI 操作之外,您可能还有一个在循环中触发的 tick 操作 - 它只是另一个输入事件,由同一组 reducer 处理。

试图直接回答您的问题:

"What does the design for a system look like when there are multiple asynchronous event sources and we want everything to be immutable? Or at least, what's a good pattern for controlling mutability in a system like this?"

在 Unix 世界中,此设计的正确解决方案模式是异步 FIFO 消息队列 (AMQ),无论如何从 System 5 开始,虽然理论上存在竞争条件和状态不确定性可能发生的条件几乎从不练习。事实上,早期对 AMQ 可靠性的研究确定,这些错误的产生不是因为传输延迟,而是因为与同步中断请求的冲突,因为早期的 AMQ 本质上只是在内核中实现的管道 space。现代解决方案,实际上是 Scala 解决方案,是在共享受保护内存中实现 AMQ,从而消除缓慢且具有潜在危险的内核调用。

事实证明,如果您的总消息带宽小于总信道容量并且您的传输距离小于一光秒 - resistance/switching,您的失败概率非常低(就像在10^-24 的顺序)。理论上有各种各样的原因,但如果不涉及量子物理和信息论,这里就不能真正简明扼要地阐述,但是目前还没有找到数学证据来明确证明是这样,这都是估计和实践。但是 30 多年来,每种 unix 风格都依赖于这些估计来实现可靠的异步通信。

如果您想知道如何引入中断行为,设计模式是直接或次要优先级排队,添加优先级或排序级别会增加消息清单的少量开销,并且可以组成同步和异步调用。

使用多个可变指令来保存不可变起始状态的设计模式与状态保存模式类似,您可以使用历史队列或差分队列。历史队列存储原始状态和一系列状态更改,如撤消历史。而差异队列保存初始状态和所有变化的总和(稍微小一点,快一点,但现在没什么大不了的)。

最后,如果您确实需要处理在复杂网络中长距离传输或反复进出内核的大型或打包消息,设计模式是为回调添加源地址和时间戳以及一些更正处理,这就是为什么 TCP/IP、SMQ、Netbios 等都将这些包含在它们的协议中,所以如果您需要这样做,您可以将 prioritizing/ordering 队列修改为数据包感知。

我意识到这是对一个庞大主题的仓促处理,这就是为什么如果有任何进一步的问题或需要澄清的要点,我很乐意回复。

我希望我回答了你的问题并且没有偏离你的要求。 :)

Post-编辑:

这里有一些很好的例子,说明如何以及为什么将这类队列设计用于分布式并发应用程序,它们用于大多数 FRP 分布式设计解决方案的核心:

https://docs.oracle.com/cd/E19798-01/821-1841/bncfh/index.html

https://blog.codepath.com/2013/01/06/asynchronous-processing-in-web-applications-part-2-developers-need-to-understand-message-queues/

http://www.enterpriseintegrationpatterns.com/patterns/messaging/ComposedMessagingMSMQ.html

http://soapatterns.org/design_patterns/asynchronous_queuing

http://www.rossbencina.com/code/programming-with-lightweight-asynchronous-messages-some-basic-patterns

http://www.asp.net/aspnet/overview/developing-apps-with-windows-azure/building-real-world-cloud-apps-with-windows-azure/queue-centric-work-pattern

http://spin.atomicobject.com/2014/08/18/asynchronous-ios-reactivecocoa/

http://fsharpforfunandprofit.com/posts/concurrency-reactive/

以及 Martin Odersky 的视频...

https://www.typesafe.com/resources/video/martin-odersky---typesafe-reactive-platform

:)