12

我最近问了一个关于函数式编程的问题,并收到了(好!)答案,引发了更多问题(有时似乎是学习的情况)。这里有几个例子:

  1. 一个答案提到了不可变数据结构的优势:每个线程都可以拥有自己的副本。现在,对我来说,这听起来更像是一个版本控制系统(打个比方),不是锁定某人已签出的代码以使其无法被其他任何人修改,而是每个人都可以签出自己的副本。听起来不错。但是,在 VCS 中,您有“合并”更改的概念,以防两个人更改相同的内容。看起来这个问题肯定会出现在多线程场景中......那么当线程看到最新数据很重要时,如何“合并”?

  2. 这个答案谈到了在对象的循环中执行操作的情况,以及如何每次都使用新对象而不是更新旧对象。但是,假设bankAccount在非循环场景中更新 - 例如 GUI 银行系统。操作员单击“更改利率”按钮,该按钮会触发一个事件(例如在 C# 中)执行类似bankAccount.InterestRate = newRateFromUser. 我觉得我在这里很密集,但希望我的例子是有道理的:必须有某种方式来更新对象,对吧?其他几件事可能取决于新数据。

无论如何,如果你能帮助我理解范式转变,我将不胜感激。我记得在简单的程序命令式编码方法的背景下学习 OOP 时,我的大脑经历了类似的“愚蠢阶段”。

4

6 回答 6

7

想想 .Net 中的 String 类(它是一个不可变对象)。如果你在一个字符串上调用一个方法,你会得到一个新的副本:

String s1 = "there";
String s2 = s1.Insert(0, "hello ");

Console.Writeline("string 1: " + s1);
Console.Writeline("string 2: " + s2);

这将输出:

字符串 1:那里

字符串 2:你好

将此行为与具有基本相同方法签名的 StringBuilder 进行比较:

StringBuilder sb  = new StringBuilder("there");
StringBuilder sb2 = sb.Insert(0, "hi ");

Console.WriteLine("sb 1: " + sb.ToString());
Console.WriteLine("sb 2: " + sb2.ToString());

因为 StringBuilder 是可变的,所以两个变量都指向同一个对象。输出将是:

某人 1: 你好

某人2:你好

因此,一旦创建了字符串,就绝对不能更改它。s1 将一直“在那里”直到时间结束(或直到它的垃圾被收集)。这在线程中很重要,因为您始终可以单步执行每个字符并打印其值,因为知道它总是会打印“那里”。如果您在创建 StringBuilder 后开始打印它,您可能会打印那里的前两个字符并获取'th'。现在,想象一下广告插入“嗨”出现了另一个线程。现在的价值不一样了!当您打印第三个字符时,它是 'hi' 中的空格。所以你打印:'th there'。

于 2008-12-11T23:14:19.403 回答
6

对第 1 部分的回答:不可变对象本身不支持诸如“合并”之类的东西以允许合并两个线程更新的结果。对此有两种主要策略:悲观和乐观。如果你很悲观,你会假设两个线程很可能想要同时更新同一个数据。所以你使用锁定,这样第二个线程将冻结,直到第一个线程说它已经完成。如果您乐观地认为这种情况很少发生,您可以让两个线程处理它们自己的数据逻辑副本。首先完成的一个提供新版本,另一个必须从头开始 - 只是现在它从第一个线程更改的结果开始。这种昂贵的重启只是偶尔发生,

第 2 部分:纯函数式无状态语言并没有真正消除这个问题。即使是纯 Haskell 程序也可以有与之关联的状态。不同之处在于有状态代码具有不同的返回类型。操作状态的函数表示为对表示该状态的对象进行操作的一系列操作。在一个荒谬的例子中,考虑计算机的文件系统。每次程序修改文件的内容(甚至修改单个字节)时,它都会创建整个文件系统的新“版本”。通过扩展,整个宇宙的新版本。但现在让我们关注文件系统。检查文件系统的程序的任何其他部分现在可能会受到该修改字节的影响。因此,Haskell 说,在文件系统上运行的函数必须有效地传递代表文件系统版本的对象。然后因为手动处理这将是乏味的,它把需求翻过来,并说如果一个函数想要能够做 IO,它必须返回一种容器对象。容器内部是函数想要返回的值。但是容器作为证据表明该功能也有副作用或可以看到副作用。这意味着 Haskell 的类型系统能够区分具有副作用的函数和“纯”函数。所以它有助于包含和管理代码的状态,而不是真正消除它。它将要求从里到外,并说如果一个函数想要能够进行 IO,它必须返回一种容器对象。容器内部是函数想要返回的值。但是容器作为证据表明该功能也有副作用或可以看到副作用。这意味着 Haskell 的类型系统能够区分具有副作用的函数和“纯”函数。所以它有助于包含和管理代码的状态,而不是真正消除它。它将要求从里到外,并说如果一个函数想要能够进行 IO,它必须返回一种容器对象。容器内部是函数想要返回的值。但是容器作为证据表明该功能也有副作用或可以看到副作用。这意味着 Haskell 的类型系统能够区分具有副作用的函数和“纯”函数。所以它有助于包含和管理代码的状态,而不是真正消除它。s 类型系统能够区分具有副作用的函数和“纯”函数。所以它有助于包含和管理代码的状态,而不是真正消除它。s 类型系统能够区分具有副作用的函数和“纯”函数。所以它有助于包含和管理代码的状态,而不是真正消除它。

于 2008-12-11T23:29:43.367 回答
4

关于#2...

其他几件事可能取决于新数据。

这就是纯粹主义者所说的“效果”。多个对象引用同一个可变对象的概念是可变状态的本质和问题的症结所在。在 OOP 中,您可能有一个 BankAccount 类型的对象“a”,如果您在不同时间读取 a.Balance 或诸如此类的东西,您可能会看到不同的值。相反,在纯 FP 中,如果“a”的类型为 BankAccount,那么它是不可变的,并且无论时间如何都具有相同的值。

然而,由于 BankAccount 可能是我们想要建模的对象,其状态会随时间而变化,因此我们将在 FP 中将该信息编码为类型。因此,“a”可能具有“IO BankAccount”类型,或者其他一些基本上归结为使“a”实际上是一个函数,该函数将“以前的世界状态”(或以前的银行利率状态)作为输入,或其他),并返回一个新的世界状态。更新利率将是另一个具有表示效果的类型的操作(例如另一个 IO 操作),因此将返回一个新的“世界”,并且可能取决于利率(世界状态)的所有内容都将是具有知道它需要将该世界作为输入的类型。

因此,调用“a.Balance”或诸如此类的唯一可能方法是使用由于静态类型而强制执行某些“让我们到现在为止的世界历史”的代码已被正确探测到调用,无论输入的世界历史是什么,都会影响我们从 a.Balance 获得的结果。

阅读State Monad可能有助于了解您如何纯粹地建模“共享可变状态”。

于 2008-12-12T00:39:36.707 回答
3
  1. 不可变数据结构不像 VCS。将不可变数据结构视为只读文件。如果它是只读的,那么在任何给定时间谁在读取文件的哪个部分都没有关系,每个人都会读取正确的信息。

  2. 这个答案是关于http://en.wikipedia.org/wiki/Monad_(functional_programming)

于 2008-12-11T21:54:53.590 回答
3

MVCC多版本并发控制

Rich Hickey 在他的视频演示中描述了您所指问题的解决方案。

简而言之:不是通过引用直接将数据传递给客户端,而是在间接上添加一层并将引用传递给对数据的引用。(好吧,实际上你希望至少多一层间接性。但我们假设数据结构非常简单,就像“数组”一样。
由于数据是不可变的,每次数据都应该改变,你创建更改部分的副本(如果是数组,您应该创建另一个数组!)加上您创建对所有“更改”数据的另一个引用。
因此,对于使用第一个版本的数组的所有客户端,他们使用对第一个版本的引用。每个尝试访问第二个版本的客户端都使用第二个引用。
对于这种方法,“数组”数据结构不是很有趣,因为您无法拆分数据并且您被迫复制所有内容。但是对于像树这样更复杂的数据结构,数据结构的某些部分可以“共享”,因此您不必每次都复制所有内容。

有关详细信息,请查看这篇论文: Chris Okasaki 的“Purely Functional Data Structures”

于 2010-02-17T08:51:02.617 回答
1

“不可变”的确切含义是:它不会改变。

函数式程序进行更新的方式是传递新事物。现有值永远不会改变:您只需构建一个新值并传递它。很多时候,新值与旧值共享状态;该技术的好例子是由 cons 单元组成的列表和zipper

于 2008-12-11T23:21:27.423 回答