我试图理解在FP中处理不可变数据的情况(特别是在F#中,但是其他FP也可以),并打破了全态思维(OOP风格)的旧习惯。所选答案的问题的一部分在这里重申,我周边的任何问题写起坐由状态表示在OOP中FP解决与不变的人搜索(对于例如:与生产者和消费者队列)。有任何想法或链接吗?提前致谢。
编辑:为了进一步澄清这个问题,不可变结构(例如:队列)将如何在FP中的多个线程(例如,生产者和消费者)之间并发共享
我试图理解在FP中处理不可变数据的情况(特别是在F#中,但是其他FP也可以),并打破了全态思维(OOP风格)的旧习惯。所选答案的问题的一部分在这里重申,我周边的任何问题写起坐由状态表示在OOP中FP解决与不变的人搜索(对于例如:与生产者和消费者队列)。有任何想法或链接吗?提前致谢。
编辑:为了进一步澄清这个问题,不可变结构(例如:队列)将如何在FP中的多个线程(例如,生产者和消费者)之间并发共享
Answers:
尽管有时用这种方式表示,但是函数式编程¹不会阻止状态计算。它的作用是迫使程序员使状态明确。
例如,让我们使用命令式队列(使用某些伪语言)来了解某些程序的基本结构:
q := Queue.new();
while (true) {
if (Queue.is_empty(q)) {
Queue.add(q, producer());
} else {
consumer(Queue.take(q));
}
}
具有功能队列数据结构的相应结构(仍然使用命令式语言,以便一次解决一个差异)如下所示:
q := Queue.empty;
while (true) {
if (q = Queue.empty) {
q := Queue.add(q, producer());
} else {
(tail, element) := Queue.take(q);
consumer(element);
q := tail;
}
}
由于队列现在是不可变的,因此对象本身不会更改。在这个伪代码中,q
它本身是一个变量。分配q := Queue.add(…)
并q := tail
使其指向另一个对象。队列函数的接口已更改:每个函数必须返回该操作产生的新队列对象。
在纯功能语言中,即在没有副作用的语言中,您需要使所有状态都明确。由于生产者和消费者可能正在做某事,因此它们的状态也必须在其调用者的界面中。
main_loop(q, other_state) {
if (q = Queue.empty) {
let (new_state, element) = producer(other_state);
main_loop(Queue.add(q, element), new_state);
} else {
let (tail, element) = Queue.take(q);
let new_state = consumer(other_state, element);
main_loop(tail, new_state);
}
}
main_loop(Queue.empty, initial_state)
请注意,现在如何显式管理每个状态。队列操作功能将队列作为输入,并产生一个新队列作为输出。生产者和消费者也通过他们的状态。
并发编程在函数式编程中不太适合,但在函数式编程中非常适合。这个想法是运行一堆单独的计算节点,并让它们交换消息。每个节点运行一个功能程序,并且其状态在发送和接收消息时发生变化。
继续该示例,由于只有一个队列,因此由一个特定节点管理。使用者向该节点发送消息以获取元素。生产者向该节点发送消息以添加元素。
main_loop(q) =
consumer->consume(q->take()) || q->add(producer->produce());
main_loop(q)
一种能够实现并发性³的“工业化”语言是Erlang。学习Erlang绝对是对并发编程的启发之路。
¹ 该术语具有多种含义;在这里,我认为您使用它来表示无副作用的编程,这就是我也在使用的意思。
² 隐式状态编程是命令式编程;面向对象是一个完全正交的问题。
³ 我知道是发炎的,但我是说真的。具有共享内存的线程是并发编程的汇编语言。消息传递更容易理解,引入并发性后,真正的副作用就是缺乏副作用。
⁴ 这是来自不是Erlang粉丝的人,但出于其他原因。
FP语言中的有状态行为是从先前状态到新状态的转换。例如,入队将是从队列和值到具有入队值的新队列的转换。出队是从队列到值的转换,以及从值删除的新队列的转换。已经设计出诸如monads之类的构造以有效方式抽象该状态转换(以及其他计算结果)
...由OOP中的状态表示与FP中的不可变表示解决的问题(例如:与生产者和消费者的队列)
您的问题是所谓的“ XY问题”。具体来说,您引用的概念(与生产者和消费者排队)实际上是一个解决方案,而不是您所描述的“问题”。这带来了一个困难,因为您要对纯天然的东西进行纯功能的实现。所以我的回答从一个问题开始:您要解决的问题是什么?
多个生产者可以通过多种方式将结果发送给单个共享消费者。F#中最明显的解决方案也许是使使用者成为代理(又名MailboxProcessor
),并使生产者Post
将结果提供给使用者代理。这在内部使用队列,并且不是纯队列(在F#中发送消息是不受控制的副作用,是一个杂项)。
但是,潜在的问题很有可能更像是并行编程中的分散收集模式。要解决此问题,您可以创建一个输入值数组,然后Array.Parallel.map
覆盖它们,并使用serial收集结果Array.reduce
。或者,您可以使用PSeq
模块中的函数来并行处理序列的元素。
我还应该强调,有状态的思考没有任何错误。纯度有优势,但它当然不是万能药,您也应该意识到它的缺点。确实,这就是为什么F#不是纯函数式语言的原因:因此,可以在首选使用杂质时使用它们。
Clojure对状态和身份的概念进行了深思熟虑,这与并发密切相关。不变性起着重要作用,Clojure中的所有值都是不变的,可以通过引用进行访问。引用不仅仅是简单的指针。它们管理对价值的获取,并且它们的多种类型具有不同的语义。可以修改引用以指向新的(不可变的)值,并且保证这种更改是原子的。但是,在修改之后,所有其他线程仍然在原始值上工作,至少直到它们再次访问该引用为止。
我强烈建议您阅读Clojure中有关状态和身份的出色文章,它比我能更好地解释细节。