“纯函数式语言”中的“纯”是什么意思?

64

Haskell被称为“纯函数式语言”。

在这个背景下,“纯”是什么意思?对程序员有哪些影响?


1
结果是你不能使用其他编程范式,例如面向对象。另外,这一定是作业吧? - Rafe Kettler
8
@Rafe:不,你可以这样做(尽管可能会存在阻抗不匹配);这意味着没有任何副作用。 - Antal Spector-Zabusky
@Antal 我说的是后果,而不是意义。 - Rafe Kettler
12
缺乏副作用与面向对象编程相当契合;事实上,不可变对象的普及正是朝着这个方向迈出的一步。 - C. A. McCann
请查看http://homepages.cwi.nl/~ralf/OOHaskell/。 - Paul Johnson
@RafeKettler 特别提到 Haskell 无法进行虚拟继承,但这是由于它的类型类而不是由于它是纯声明式语言。声明式语言可以有虚拟继承(我正在开发一个)。 - Shelby Moore III
7个回答

65

在一个纯粹的函数式语言中,你不能执行任何具有副作用的操作。

副作用指的是评估表达式会改变一些内部状态,这个内部状态会导致再次评估相同的表达式结果不同。在一个纯函数式语言中,你可以使用相同的参数评估相同的表达式无限次,它会始终返回相同的值,因为没有状态可改变。

例如,纯函数式语言不能有赋值运算符或进行输入/输出操作,虽然实际上,即使是纯函数式语言也经常调用不纯的库来执行I/O操作。


13
在纯函数式编程语言中,仍然可以进行输入输出操作。你可以将输入流视为字符列表,并且同样地,你可以返回一个(可能是惰性求值的)字符列表作为输出进行写入。 - Anon.
3
实际上,早期版本的Haskell确实是这样工作的,但它被泛化以允许进行更多的I/O操作,而不仅仅是读写单个字符流。 - ephemient
23
一个仅仅调用不纯库进行I/O操作的函数式语言是不纯的。Haskell是纯的,它可以进行I/O操作。关键是将I/O“动作”封装为不可变值。因此,“getChar”是类型为“IO Char”的常量值,而IO值可以组合成更大的操作。该语言具有一个概念解释器,处理由“main”函数返回的IO值。 - Paul Johnson
19
这是一个不错的答案,但我不确定它是否是最好的答案。因此,我很惊讶它远远获得了最高的投票数。根据过去的经验,很多人都对像Haskell这样声称是纯函数式编程语言,却仍然可以做一些有用的事情(例如I/O)感到困惑。这个答案没有澄清这一点。 - Daniel Pratt
9
另外,尽管Spolsky先生是超级巨星级别的人物并且有着应有的巨大声誉,但我认为他在这里犯了一个关键错误,而那些点赞他的回答的人之所以这样做,是因为他写了它,而不是因为它是正确的。 - BMeph
显示剩余9条评论

37
“纯”和“函数式”是两个独立的概念,尽管一个没有另一个不太有用。
纯表达式是幂等的:它可以被多次评估,并且每次都产生相同的结果。这意味着表达式不能有任何可观察的副作用。例如,如果一个函数改变了它的参数、设置了某个变量或者根据除了其输入之外的其他东西改变了行为,那么该函数调用就不是纯的。
函数式编程语言是一种函数是一等公民的语言。换句话说,你可以像操作所有其他一等公民值一样轻松地操作函数。例如,在函数式编程语言中使用“返回布尔值的函数”作为表示集合的“数据结构”将是容易的。
在函数式编程语言中,通常以大多数纯的方式进行编程,而要严格保持纯度则需要函数式编程语言所提供的高阶函数操作。
Haskell是一种函数式编程语言,在这种语言中(几乎)所有的表达式都是纯的;因此,Haskell是一个纯粹的函数式编程语言。

3
好的。你是我发现的为数不多理解「不要混淆」纯函数(即 RT)和函数式编程的人之一。 - Shelby Moore III
“Pure”只是一个修饰符。你可以很容易地拥有一个纯逻辑编程语言,也可以拥有一个纯函数式语言。你可能应该说“函数式和纯函数式是两个不同的概念”。 - Hugh Allen
1
f是幂等的 => f . f = f。你的陈述将幂等性这个术语与引用透明混淆了。因此,即使你已经理解了语义,你也不应该那样使用“幂等”这个术语。 - RussellStewart
2
我认为"A pure expression is idempotent"这个说法是错误的。f(x): x + 1是一个简单的增量函数,因为它没有任何副作用或状态,所以它是"Pure"。然而,它不是幂等的,因为f(f(x)) != f(x)。相反,像g(x): x - x这样的函数既是纯的又是幂等的,因为它总是返回0。 - Kartik Sreenivasan

24

纯函数指没有副作用的函数——它接受一个值并返回一个值,没有全局状态会被函数修改。纯函数式编程语言是强制函数必须是纯函数的语言。纯函数的一些有趣后果包括:计算可以是惰性的,因为函数调用没有其他目的而是返回一个值,如果您不打算使用它的值,则不需要实际执行该函数。由于这个特性,Haskell中的递归函数和无限列表非常常见。

另一个后果是函数的评估顺序无关紧要——因为它们互相不影响,您可以按任何方便的顺序进行评估。这意味着并行编程提出的一些问题根本不存在,因为函数执行的顺序没有“错误”或“正确”之分。


15
严格来说,一个“纯”函数式语言是一种函数式语言(即函数是第一类值的语言),其中表达式没有副作用。术语“纯函数式语言”是同义词。
按照这个定义,Haskell不是一个纯函数式语言。任何你可以编写显示结果、读写文件、拥有GUI等程序的语言都不是纯函数式的。因此,没有通用的编程语言是纯函数式的(但有一些有用的特定领域的纯函数式语言:它们通常可以被看作某种嵌入式语言)。
有一种有用的松散意义,可以将像Haskell和Erlang这样的语言视为纯函数式的,但像ML和Scheme这样的语言则不行。如果一个语言中存在一个相当大、有用且良好描述的子集,其中副作用是不可能的,那么这个语言就可以被认为是纯函数式的。例如,在Haskell中,所有类型不是由IO或其他表示效果的单子构建的程序都是无副作用的。在Erlang中,所有不使用IO诱导库或并发功能的程序都是无副作用的(这比Haskell的情况更加牵强)。相反,在ML或Scheme中,副作用可以隐藏在任何函数中。
从这个角度来看,Haskell的纯函数式子集可以被看作是处理每个monad内部行为的嵌入式语言(当然,这是一个奇怪的观点,因为几乎所有的计算都发生在这个“嵌入式”子集中),而Erlang的纯函数式子集可以被看作是处理本地行为的嵌入式语言。

格雷厄姆·哈顿纯函数式语言的话题有一种稍微不同但非常有趣的看法:

有时,“纯函数式”这个术语也被广泛地用来指那些可能包含计算效果但不改变“函数”概念的语言(正如函数的基本属性得以保留)。通常,表达式的求值可以产生一个“任务”,然后单独执行该任务以引起计算效果。评估和执行阶段是分开的,使得评估阶段不会损害表达式和函数的标准属性。例如,Haskell 的输入/输出机制就属于这类。

也就是说,在 Haskell 中,函数的类型为 a -> b 并且不能具有副作用。类型为 IO(a -> b) 的表达式可以具有副作用,但它不是一个函数。因此,在 Haskell 中函数必须是纯函数,因此 Haskell 是纯函数式的。


2
一个IO函数有稍微不同的类型a -> IO b,实际上它是一个仅返回一个被IO包装的值的函数。 - fuz
1
@FUZxxl 但是这并不使IO成为纯函数(编译器在谎称它是纯的)。请参见我在Joel Spolsky的回答下面的评论和链接。 - Shelby Moore III
@ShelbyMooreIII 这里没有人声称IO是纯粹的(那将是非常愚蠢的)。编译器并没有说谎,除非您认为无限运行的程序是不纯的。 - Gilles 'SO- stop being evil'
@Gilles "没有人"? 我在 Joel Spolsky 的回答下看到了 Paul Johnson 的评论,他声称 Haskell 中的 IO 是纯的,并获得了 5 个赞。为什么你提到了“非终止”?你是否混淆了我写关于底部类型的另一页?似乎你正在混淆一些与我在 Haskell 中呈现的 IO 单子逻辑无关的东西。请解释一下。 - Shelby Moore III
1
@ShelbyMooreIII 你似乎混淆了类型为 IO a 的值的计算(Paul所指的“不可变值”)与执行这些值的具有效果性质的操作(Paul所说的“概念解释器”)。我提到非终止是因为严格来说,非终止并不是引用透明的,因为如果您至少评估一次或根本不评估它们,则程序的行为会有所不同。这与底部类型无关;它与在每种类型中包含底部值息息相关。 - Gilles 'SO- stop being evil'
@Gilles 执行与此无关。 IO 的命令式代码包含在程序中。程序包含命令式代码,因此程序不是100%纯的。 Haskell将其隐藏起来,但它仍然在程序执行之前包含在内。就像您使用Scala编写某些部分纯粹而其他部分没有RT一样。我非常清楚每个Haskell归纳类型都由底部值填充,因为在惰性语言中,底部是一个值,而不是一个效果 - Shelby Moore III

4

在纯函数式代码中,不可能有任何副作用,因此测试变得更加容易,因为没有外部状态需要检查或验证。同样,由于这个原因,扩展代码也可能变得更加容易。

每当我尝试扩展/修复代码时,非明显的副作用问题让我感到困扰,已经数不清了。


4
正如其他人所提到的,“纯”在“纯函数式编程语言”中的意思是缺乏可观察的副作用。对我来说,这直接引出了一个问题:
什么是副作用?
我曾看到副作用被解释为:
1. 除了简单计算结果之外,函数执行的其他操作。 2. 影响函数结果的东西,而不仅仅是函数输入。
如果第一个定义是正确的,那么任何进行I/O(例如写入文件)的函数都不能被称为“纯”函数。虽然Haskell程序可以调用导致I/O执行的函数,但根据这个定义,Haskell似乎不能被称为纯函数式编程语言(尽管它声称自己是)。
因此,基于这个和其他原因,我认为第二个定义更有用。根据第二个定义,Haskell仍然可以声称是完全纯的函数式编程语言,因为导致I/O执行的函数仅基于函数输入计算结果。Haskell如何协调这些看似矛盾的要求非常有趣,但我会抵制诱惑,不再偏离实际问题的答案。

4
你误解了Haskell如何进行I/O操作。在早期的原型中,main是一个纯函数,它将来自世界的惰性流I/O响应转换为向世界发出的惰性流I/O请求。现代Haskell使用单子(monads)使这个过程更容易编程,但概念仍然相同:一个纯值描述要应用于外部不纯世界的变换。 - ephemient
3
Haskell 没有执行 I/O 操作的函数。它有值(和返回值的函数),当它们在 IO Monad 中按顺序排列在 main 中时,结果是由运行时执行 I/O 操作。为使内容更通俗易懂,可能需要进一步解释 Monad 和 Runtime 的含义,但这不在翻译范围之内。 - ephemient
2
是的,让我们都忽略不安全的函数吧。我的抱怨是,在 Haskell 的大部分安全区域内,“函数所做的除了简单计算结果之外的事情”并不存在。当然,如果你包括运行时的执行,那么可能会发生一些副作用,但你可以用 ST 替换整个 IO 单子,你的程序仍然是纯的,而且不会有任何区别。 - ephemient
3
@Daniel:点赞你强调这是一个棘手的问题,但“Haskell如何协调这些看似相互冲突的要求”是问题的核心。此外,你给出的第二个副作用定义存在缺陷——例如它没有把打印输出视为副作用。第一个定义是正确的(在其简化的限制内)。 - Gilles 'SO- stop being evil'
1
@ShelbyMooreIII 当然,这是一个棘手的问题(而您似乎也没有完全掌握)。 Haskell并不假装知道世界的内部结构; 相反,它尽可能少地了解它。顺便说一下,在计算模型中,时间几乎从来不是连续的。 - Gilles 'SO- stop being evil'
显示剩余6条评论

4
Amr Sabry写了一篇关于纯函数式语言的论文。按照这个定义,如果我们忽略像unsafePerformIO这样的东西,Haskell被认为是纯的。使用这个定义也使得ML和Erlang成为不纯的。大多数语言都有合格的纯子集,但我个人认为谈论C是一个纯语言并没有什么用处。
高阶性与纯度是正交的,你可以设计一个纯的一阶函数式语言。

2
不是说你的回答不正确,但我怀疑 Stack Overflow 的用户可能不会喜欢看到“阅读这篇论文,你就能理解 X”这样的回答。只是这么说一下... :) - BMeph

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接