我很清楚在我的应用程序中使用不可变数据的好处,我对在简单的同步编程环境中使用这些不可变结构的想法非常满意 .
Stack Overflow上有一个很好的例子,描述了通过在一系列递归调用中传递状态来管理游戏的状态,如下所示:
function update(state) {
sleep(100)
return update({
ticks: state.ticks + 1,
player: player
})
}
我们可以在函数体中做一些任意的,无副作用的自由工作,然后我们返回一个新状态,而不是改变旧状态 .
将它转换为简单的异步模型似乎相当容易,比如说Javascript .
function update(state) {
const newState = {
player,
ticks: state.ticks + 1
};
setTimeout(update.bind(this, newState), 100);
}
但是,只要我们有更多的异步事件源,管理保持状态不可变和函数纯净似乎变得更加困难 .
如果我们在示例中添加click事件,我们最终得到的代码如下所示 .
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
});
});
但似乎这需要某种可变的状态管理器?
或者,我想我可以将click事件添加到要在更新循环中处理的动作队列,例如:
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);
}
同样,这并不是特别有用,并且在此过程中某处依赖至少一些可变状态 . 这些可能是天真的方法,来自那些主要使用可变状态和命令式面向对象编程的人 .
当存在多个异步事件源并且我们希望一切都是不可变的时,系统的设计是什么样的?或者至少,在这样的系统中控制可变性的好模式是什么?
3 回答
您可能有兴趣看一下Redux . Redux采用了类似的方法:
它将整个应用程序状态建模为单个不可变对象 .
用户操作本质上是分派给
store
进行任意处理的消息 .动作由
f(previousState, action) => newState
形式的reducer函数处理 . 这是一种比原始版本更实用的方法 .store
运行reducers并维护单个不可变的应用程序状态 .你是对的,这不是严格不可变的,因为商店本身有一个对当前状态的可变引用 . 但正如其他人所指出的那样,这似乎不是大多数不可变数据概念的问题 .
除了UI操作之外,您可能还有一个在循环中触发的
tick
操作 - 它只是另一个输入事件,由同一组reducers处理 .试图直接回答您的问题:
"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),无论如何从系统5开始,虽然理论上有条件下竞争条件和状态不确定性在实践中可能发生,但几乎从来没有 . 实际上,对AMQ可靠性的早期研究确定这些错误不是因为传输滞后而是因为与同步中断请求的冲突,因为早期的AMQ基本上只是在内核空间中实现的管道 . 现代解决方案,实际上是Scala解决方案,是在共享受保护的内存中实现AMQ,从而消除缓慢且潜在危险的内核调用 .
事实证明,如果您的总信息带宽小于总信道容量并且您的传输距离小于一秒光 - 电阻/切换,则您的故障概率在宇宙上很低(就像大约10 ^ -24的情况) ) . 理论上存在各种各样的理论原因,但如果没有对量子物理学和信息理论的深入研究,就不能在这里简明扼要地解释,但是还没有找到数学证据来明确证明这种情况就是这样,它是所有的估计和实践 . 但是,unix的每一种风格都依赖于这些估计30年来可靠的异步通信 .
如果您想知道如何引入中断行为,设计模式可以是直接或次优先级排队,添加优先级或排序级别会给消息清单增加很小的开销,以及如何组 Contract 步和异步调用 .
使用多个可变指令保留不可变起始状态的设计模式与状态保留模式类似,您可以使用历史或差异队列 . 历史队列存储原始状态和状态更改数组,如撤消历史记录 . 差异队列保持初始状态和所有变化的总和(稍微更小和更快,但这些天没有那么大的交易) .
最后,如果您确实需要处理在复杂网络的大距离内或在内核内外重复的大型或分组化消息,则设计模式是添加来源回调的地址和时间戳以及一点纠正处理,这就是为什么TCP / IP,SMQ,Netbios等都在其协议中包含这些,所以你需要这样做,你将优先级/排序队列修改为数据包知道的 .
我意识到这是对一个大型主题的仓促对待,这就是为什么我很高兴回答是否有任何需要澄清的问题或要点 .
我希望我回答你的问题,并没有转向你所要求的 . :)
Post-Edit:
以下是一些很好的例子,说明了如何以及为什么将这些类型的队列设计用于分布式并发应用程序,它们是大多数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
:)
使用Object.freeze,可以使对象不可变:
将产生
{foo: "bar"}
. 请注意,尝试在冻结变量上设置新属性将无提示失败 .在这种情况下,在创建新状态对象之后,在调用更新例程或触发事件之前将其冻结以防止进一步修改 .