我最近asked a question关于函数式编程,并收到了(好的!)答案,这些答案提出了更多问题(有时似乎是学习的情况)。以下是几个例子:
一个答案提到了不可变数据结构的优点:每个线程都有自己的副本。现在,对我来说,这听起来更像是一个版本控制系统(使用类比),而不是锁定某人已经签出的代码,以便其他人无法修改,每个人都可以查看自己的副本。听起来不错。但是,在VCS中,您有“合并”更改的概念,如果两个人更改了相同的内容。似乎这个问题肯定会出现在多线程场景中......那么当线程看到最新数据很重要时,如何“合并”呢?
This answer讨论了在对象的循环中执行操作的情况,以及如何每次使用新对象而不是更新旧对象。但是,假设bankAccount
正在非循环场景中更新 - 例如GUI银行系统。操作员单击“更改利率”按钮,该按钮将触发一个事件(例如,在C#中)执行bankAccount.InterestRate = newRateFromUser
之类的操作。我觉得我在这里很密集,但希望我的例子是有意义的:必须有某种方式来更新对象,对吧?其他一些事情可能取决于新数据。
无论如何,如果你能帮我理解范式转变,我会很感激。我记得我的大脑经过类似的“愚蠢阶段”,在学习OOP后,采用简单的程序性命令式编码方法。
答案 0 :(得分:7)
考虑.Net中的String类(它是一个不可变对象)。如果在字符串上调用方法,则会得到一个新副本:
String s1 = "there";
String s2 = s1.Insert(0, "hello ");
Console.Writeline("string 1: " + s1);
Console.Writeline("string 2: " + s2);
这将输出:
string 1:
string 2:你好
将此行为与StringBuilder进行比较,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是可变的,所以两个变量都指向同一个对象。输出将是:
sb 1:你好
sb 2:你好
因此,一旦创建了字符串,就绝对无法更改字符串。 s1将一直“存在”直到时间结束(或直到它的垃圾收集)。这在线程中很重要,因为你总是可以逐步浏览每个角色并打印它的值,因为它知道它将始终打印在那里。如果你在创建后开始打印StringBuilder,你可以打印那里的前两个字符并得到''。现在,想象另一个线程出现广告插入'hi'。价值现在不同了!当你打印第三个字符时,它是'hi'的空格。所以你打印:'那里'。
答案 1 :(得分:6)
对第1部分的回答:不可变对象本身不支持“合并”之类的任何东西,以允许组合两个线程更新的结果。有两个主要策略:悲观和乐观。如果你是悲观的,你会认为两个线程很可能想要同时更新同一条数据。因此,您使用锁定,这样第二个线程将冻结,直到第一个线程说它已完成。如果您乐观地认为这种情况很少发生,那么您可以让两个线程都使用自己的数据逻辑副本。完成的那个首先提供新版本,另一个必须从头开始 - 只是现在它从第一个线程的更改结果开始。这种昂贵的重新启动只会偶尔发生,所以由于缺乏锁定,它总体上表现得更好(尽管如果你的乐观情绪很好地解决了碰撞发生的频率很少的话)。
第2部分:纯功能无状态语言并没有真正消除这个问题。即使是纯粹的Haskell程序也可以拥有与之相关的状态。不同之处在于有状态代码具有不同的返回类型。操纵状态的函数表示为对表示该状态的对象进行操作的一系列操作。在一个荒谬的例子中,考虑计算机的文件系统。每次程序修改文件的内容(即使是单个字节),它都会创建整个文件系统的新“版本”。并且通过扩展,整个宇宙的新版本。但是现在让我们关注文件系统。检查文件系统的程序的任何其他部分现在可能受该修改字节的影响。因此,Haskell说,在文件系统上运行的函数必须有效地传递代表文件系统版本的对象。然后因为手动处理这将是繁琐的,它将需求内部化,并说如果一个函数想要能够做IO,它必须返回一种容器对象。容器内部是函数想要返回的值。但该容器可作为该功能也具有副作用或可见副作用的证据。这意味着Haskell的类型系统能够区分功能与副作用和“纯”功能。因此,有助于包含和管理代码的有状态,而无需真正消除代码。
答案 2 :(得分:4)
关于#2 ......
其他一些事情可能取决于 新数据。
这就是纯粹主义者所说的“效果”。多个对象引用同一个可变对象的概念是可变状态的本质和问题的关键。在OOP中,您可能有一个BankAccount类型的对象“a”,如果您在不同的次读取a.Balance或whatnot,您可能会看到不同的值。相反,在纯FP中,如果“a”具有BankAccount类型,则它是不可变的,并且无论时间如何都具有相同的值。
但是,因为BankAccount可能是我们想要建模的对象,其状态确实随时间变化,我们会在FP中对该类型的信息进行编码。因此,“a”可能具有“IO BankAccount”类型,或者其他一些monadic类型,其实质上归结为使“a”实际上是一个函数,将“世界的先前状态”(或之前的银行利率状态)视为输入或者其他什么),并返回一个新的世界状态。更新利率将是具有表示效果的类型的另一个操作(例如,另一个IO操作),因此将返回新的“世界”,并且可能取决于利率(世界状态)的所有内容将是具有知道它需要将这个世界作为输入的类型。
因此,唯一可能的方法是调用“a.Balance”或者其他什么方法来使用代码,这要归功于静态类型,强制执行一些“让我们到现在为止的世界历史”已被正确地探测到调用的重点,无论世界历史如何,输入都会影响我们从a.Balance得到的结果。
阅读State Monad可能有助于了解您如何纯粹模拟'共享可变状态'。
答案 3 :(得分:3)
不可变数据结构与VCS不同。将不可变数据结构视为只读文件。如果它是只读的,那么在任何给定时间阅读文件的哪个部分并不重要,每个人都会阅读正确的信息。
答案是关于http://en.wikipedia.org/wiki/Monad_(functional_programming)
答案 4 :(得分:3)
Rich Hickey在video presentations中描述了您所指的问题的解决方案。
简而言之:不是通过引用直接将数据传递给客户端,而是在间接上添加一个级别,并将引用传递给对数据的引用。 (嗯,实际上你想要至少有一个间接级别。但是我们假设数据结构非常简单,就像“数组”一样。)
由于数据是不可变的,因此每次更改数据时,都会创建更改部分的副本(如果是数组,则应创建另一个数组的!),另外还要创建对所有数据的另一个引用“改变”数据
因此,对于使用第一版数组的所有客户端,他们使用对第一个版本的引用。每个尝试访问第二个版本的客户都使用第二个引用
“数组”数据结构对于此方法不是很有趣,因为您无法拆分数据,因此您不得不复制所有内容。但是对于像树一样的更复杂的数据结构,数据结构的某些部分可以“共享”,因此您不必每次都复制所有内容。
有关详细信息,请查看本文:Chris Okasaki的"Purely Functional Data Structures"。
答案 5 :(得分:1)
“不可变”意味着:它不会改变。
功能程序更新的方式是传递新东西。现有值永远不会改变:您只需构建一个新值并传递它。新价值通常与旧价值分享;该技术的好例子是由缺点单元格组成的列表,以及zipper。