纯函数的好处

82

今天我在读有关“纯函数”的内容时,对它的使用感到困惑:

如果一个函数对于相同的输入返回相同的输出,并且没有任何可观察到的副作用,则称其为“纯函数”。

例如,strlen()是一个纯函数,而rand()则是不纯的。

__attribute__ ((pure)) int fun(int i)
{
    return i*i;
}

int main()
{
    int i=10;
    printf("%d",fun(i));//outputs 100
    return 0;
}

http://ideone.com/33XJU

如果没有更改输出,那么上面的程序的行为与没有 pure 声明的函数行为相同。

如果没有更改输出,声明一个函数为 pure 有什么好处呢?


7
好的,看生成的汇编代码。 - Philip Kendall
4
我认为这个关于纯度的定义是不正确的——例如,printf 就符合条件(使用相同参数两次调用将产生相同的返回值),但它并不是纯的。 - tdammers
14
确实,它缺少“...没有副作用...”这一部分。 - Frerich Raabe
2
@Ben:熵从哪里来?我们正在处理(理论上)确定性机器,将真正的熵引入它们的唯一方法是来自外部来源,这意味着副作用。当然,我们可以允许编程语言定义不确定函数,假装技术副作用不存在,并且函数确实是不确定的;但如果我们这样做,大多数跟踪纯度的实际好处都会丧失。 - tdammers
3
tdammers 是正确的 - 上面给出的“纯”的定义是不正确的。纯指的是输出仅取决于函数的输入;此外,也必须没有可观察到的副作用。“对于相同的输入产生相同的输出”是这些要求的一个非常不准确的总结。请参考http://en.wikipedia.org/wiki/Pure_function。 - Dancrumb
显示剩余15条评论
6个回答

146

pure 声明可以让编译器知道关于函数的某些优化:想象一下这样一段代码:

for (int i = 0; i < 1000; i++)
{
    printf("%d", fun(10));
}

使用纯函数,编译器可以知道它只需要评估fun(10)一次,而不是1000次。对于复杂的函数来说,这是一个巨大的优势。


你可以安全地使用记忆化。 - Joel Coehoorn
@mob 你是什么意思?为什么不呢? - Konrad Rudolph
15
因为你可以修改字符串(从某个地址开始的字符序列)而不修改输入(指向字符串开头地址的指针),因此无法对其进行记忆化。只有在具有不可变字符串的语言中(比如Java),它才是一个纯函数。 - mob
5
想象一个长度为1000的字符串,对它调用strlen函数。然后再调用一次。是不是一样的结果?现在把第二个字符修改为\0。现在strlen函数返回的值是否仍然是1000呢?虽然输入字符串的地址没有变,但是函数却返回了一个不同的值。请注意,此处不提供解释。 - Mike Bailey
5
@mob 这是个好的反对意见,显然你是正确的。我被“即使书籍”这一事实所误导,这些书声称strlen(在GCC / glibc中)实际上是纯函数。但是查看glibc实现后发现这是错误的。 - Konrad Rudolph

34
当你说一个函数是“纯”的时候,你保证它没有外部可见的副作用(正如一条注释所说,如果你撒谎了,会发生不好的事情)。知道一个函数是“纯”的对编译器有好处,因为编译器可以利用这个知识来进行某些优化。
以下是GCC文档关于属性的说明:

pure

Many functions have no effects except the return value and their return value depends only on the parameters and/or global variables. Such a function can be subject to common subexpression elimination and loop optimization just as an arithmetic operator would be. These functions should be declared with the attribute pure. For example,

          int square (int) __attribute__ ((pure));

菲利普的回答已经展示了知道一个函数是“纯”的如何帮助循环优化。

这里有一个关于公共子表达式消除的例子(假设foo是纯的):

a = foo (99) * x + y;
b = foo (99) * x + z;

可以变成:

_tmp = foo (99) * x;
a = _tmp + y;
b = _tmp + z;

3
我不确定是否有人这样做,但纯函数可以让编译器重新排序函数调用的顺序,如果编译器认为重新排序会更有益的话。当存在副作用的可能性时,编译器需要更加保守。 - mpdonadio
@MPD - 是的,这听起来很合理。而且由于“调用”指令对于超标量 CPU 来说是一个瓶颈,编译器的一些帮助可以提升性能。 - ArjunShankar
我依稀记得在几年前使用过一种 DSP 编译器,它会使用这种技术来更早/更晚地获取返回值。这使它能够最小化流水线停顿。 - mpdonadio
1
“foo(99)”可以预先计算吗?因为99是一个常量,而且foo总是返回相同的结果。也许可以在某种两阶段编译中实现? - markwatson
1
@markwatson - 我不确定。有时候可能根本不可能实现。例如,如果 foo 是另一个编译单元(另一个 C 文件)的一部分,或者在预编译库中。在这两种情况下,编译器不知道 foo 的作用,并且无法进行预计算。 - ArjunShankar

29

除了可能带来的运行时好处外,纯函数在阅读代码时更易于理解。此外,由于您知道返回值仅取决于参数的值,因此测试纯函数要容易得多。


3
+1,你关于测试的观点很有意思。无需设置和撤销。 - ArjunShankar

15
一个非纯函数。
int foo(int x, int y) // possible side-effects

就像是纯函数的扩展一样

int bar(int x, int y) // guaranteed no side-effects

除了显式的函数参数x和y,您还可以将整个宇宙(或任何计算机可以通信的内容)作为隐含的潜在输入。同样地,除了显式的整数返回值,计算机可以写入的任何东西都是隐含的返回值的一部分。

很明显,纯函数比非纯函数更容易推理,这一点应该是清楚的。


1
使用宇宙作为潜在输入是解释纯和非纯之间差异的一种非常好的方式。 - ArjunShankar
的确,这就是单子背后的想法。 - Kristopher Micinski

7

作为补充,我想提到C++11使用constexpr关键字对一些东西进行了规范化,例如:

#include <iostream>
#include <cstring>

constexpr unsigned static_strlen(const char * str, unsigned offset = 0) {
        return (*str == '\0') ? offset : static_strlen(str + 1, offset + 1);
}

constexpr const char * str = "asdfjkl;";

constexpr unsigned len = static_strlen(str); //MUST be evaluated at compile time
//so, for example, this: int arr[len]; is legal, as len is a constant.

int main() {
    std::cout << len << std::endl << std::strlen(str) << std::endl;
    return 0;
}

限制constexpr的使用方法使该函数能被证明为纯函数。这样,编译器可以更积极地优化(请确保您使用尾递归!),并在编译时而不是运行时计算函数。因此,如果您正在使用C ++(我知道您说了C,但它们相关),以正确的方式编写纯函数允许编译器对函数进行各种酷炫的操作 :-)

4
一般而言,纯函数比非纯函数有三个优点,编译器可以从中受益:

缓存

假设您有一个纯函数f,它被调用了100000次,因为它是确定性的,并且仅取决于其参数,所以编译器可以计算其值一次,并在必要时使用它。

并行处理

纯函数不读取或写入任何共享内存,因此可以在不产生任何意外后果的情况下在单独的线程中运行。

通过引用传递

函数f(struct t)按值获取其参数t,而编译器可以将t作为引用传递给f,如果它被声明为纯函数,则保证t的值不会发生变化并具有性能增益。
除了编译时考虑因素外,纯函数易于进行测试:只需调用它们即可。
无需构建对象或模拟连接到数据库/文件系统。

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