函数式编程—不变性


12

我试图理解在FP中处理不可变数据的情况(特别是在F#中,但是其他FP也可以),并打破了全态思维(OOP风格)的旧习惯。所选答案的问题的一部分在这里重申,我周边的任何问题写起坐由状态表示在OOP中FP解决与不变的人搜索(对于例如:与生产者和消费者队列)。有任何想法或链接吗?提前致谢。

编辑:为了进一步澄清这个问题,不可变结构(例如:队列)将如何在FP中的多个线程(例如,生产者和消费者)之间并发共享


处理并发问题的一种方法是每次都制作队列的副本(虽然有些昂贵,但可以)。
Job

Answers:


19

尽管有时用这种方式表示,但是函数式编程¹不会阻止状态计算。它的作用是迫使程序员使状态明确。

例如,让我们使用命令式队列(使用某些伪语言)来了解某些程序的基本结构:

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粉丝的人,但出于其他原因。


2
+1更完整的答案,尽管我想人们可能会质疑Erlang不是纯粹的FP语言。
Rein Henrichs

1
@Rein Henrichs:的确如此。实际上,在所有现有的主流语言中,Erlang是最忠实地实现面向对象的语言。
约尔格W¯¯米塔格

2
@约尔格同意。再次,尽管人们可能会质疑纯FP和OO是正交的。
Rein Henrichs

因此,为了在并发软件中实现不可变队列,需要在节点之间发送和接收消息。未决消息存储在哪里?
mouviciel 2013年

@mouviciel队列元素存储在节点的传入消息队列中。此消息队列工具是分布式基础结构的基本功能。一种适合本地并发但不适用于分布式系统的替代基础结构设计是阻塞发送方,直到接收方准备就绪。我意识到这并不能解释所有内容,因此需要一本关于并发编程的书中的一到两章来全面解释这一点。
吉尔斯(Gillles)“所以-别再邪恶了”

4

FP语言中的有状态行为是从先前状态到新状态的转换。例如,入队将是从队列和值到具有入队值的新队列的转换。出队是从队列到值的转换,以及从值删除的新队列的转换。已经设计出诸如monads之类的构造以有效方式抽象该状态转换(以及其他计算结果)


3
如果它是每个添加/删除操作的新队列,那么两个(或多个)异步操作(线程)将如何共享该队列?它是抽象队列的新模式吗?
venkram 2011年

并发是一个完全不同的问题。我无法在评论中提供足够的答案。
Rein Henrichs

2
@Rein Henrichs:“无法在评论中提供足够的答案”。这通常意味着您应该更新答案以解决与评论相关的问题。
S.Lott

并发也可以是一元的,请参见haskells Control.Concurrency.STM。
替代

1
在这种情况下,@ S.Lott表示OP应该提出一个新问题。并发是这个问题的关键,它是关于不可变数据结构的。
Rein Henrichs

2

...由OOP中的状态表示与FP中的不可变表示解决的问题(例如:与生产者和消费者的队列)

您的问题是所谓的“ XY问题”。具体来说,您引用的概念(与生产者和消费者排队)实际上是一个解决方案,而不是您所描述的“问题”。这带来了一个困难,因为您要对纯天然的东西进行纯功能的实现。所以我的回答从一个问题开始:您要解决的问题是什么?

多个生产者可以通过多种方式将结果发送给单个共享消费者。F#中最明显的解决方案也许是使使用者成为代理(又名MailboxProcessor),并使生产者Post将结果提供给使用者代理。这在内部使用队列,并且不是纯队列(在F#中发送消息是不受控制的副作用,是一个杂项)。

但是,潜在的问题很有可能更像是并行编程中的分散收集模式。要解决此问题,您可以创建一个输入值数组,然后Array.Parallel.map覆盖它们,并使用serial收集结果Array.reduce。或者,您可以使用PSeq模块中的函数来并行处理序列的元素。

我还应该强调,有状态的思考没有任何错误。纯度有优势,但它当然不是万能药,您也应该意识到它的缺点。确实,这就是为什么F#不是纯函数式语言的原因:因此,可以在首选使用杂质时使用它们。


1

Clojure对状态和身份的概念进行了深思熟虑,这与并发密切相关。不变性起着重要作用,Clojure中的所有值都是不变的,可以通过引用进行访问。引用不仅仅是简单的指针。它们管理对价值的获取,并且它们的多种类型具有不同的语义。可以修改引用以指向新的(不可变的)值,并且保证这种更改是原子的。但是,在修改之后,所有其他线程仍然在原始值上工作,至少直到它们再次访问该引用为止。

我强烈建议您阅读Clojure中有关状态和身份出色文章,它比我能更好地解释细节。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.