纯功能的好处


81

今天,我在阅读有关纯函数的信息,并对其使用感到困惑:

如果函数针对同一组输入返回相同的一组值并且没有任何可观察到的副作用,则称该函数为纯函数。

例如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
是的-查看生成的程序集。
菲利普·肯德尔

4
我认为纯度的定义不正确-printf例如,可以限定(用相同的参数两次调用它会产生相同的返回值),但是它不是纯净的。
tdammers 2012年

14
@tdammers:的确,它缺少这一...and no side-effects...部分。
Frerich Raabe 2012年

2
@Ben:熵从何而来?我们在这里(理论上)使用确定性机器,将真正的熵引入其中的唯一方法是来自外部来源,这意味着副作用。当然,我们可以允许编程语言定义不确定的函数,假装没有技术副作用,而这些函数实际上是不确定的。但是,如果这样做,跟踪纯度的大多数实际好处都会丢失。
tdammers 2012年

3
tdammers是正确的-上面给出的pure定义不正确。Pure表示输出取决于函数的输入。另外,也必须没有明显的副作用。“相同输入的相同输出”是这些要求的非常不准确的摘要。en.wikipedia.org/wiki/Pure_function
Dancrumb

Answers:


144

pure 让编译器知道它可以对该函数进行某些优化:想象一下类似的代码

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

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


即,您可以放心使用备忘录
Joel Coehoorn

@mob是什么意思?为什么不?
Konrad Rudolph 2012年

15
因为您可以修改字符串(从某个地址开始的字符序列),而无需修改输入(指向字符串开始的地址的指针),即您无法记住它。在具有不可变字符串(例如Java)的语言中,它只会是纯函数。
暴民2012年

5
@KonradRudolph:想象一个长度为1000的字符串。调用strlen就可以了。然后再说一次。是吗?现在将第二个字符修改为\0。是否strlen还是回到1000了吗?起始地址相同(==输入相同),但函数现在返回不同的值。
Mike Bailey 2012年

5
@mob这是一个很好的反对意见,显然您是对的。我的事实,误导甚至书声称,strlen(/ GCC中的glibc)实际上是纯粹的。但是看一下glibc实现,就发现这是错误的。
Konrad Rudolph

34

当您说一个函数是“纯”函数时,您就在保证它没有外部可见的副作用(正如评论所说,如果您撒谎,则可能会发生不好的事情)。知道函数是“纯粹的”会对编译器有所帮助,编译器可以使用此知识进行某些优化。

这是GCC文档关于该pure属性的内容:

除了返回值外,许多函数均无效,并且它们的返回值仅取决于参数和/或全局变量。就像算术运算符一样,可以对此类函数进行常见的子表达式消除和循环优化。这些函数应使用属性pure声明。例如,

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

Philip的答案已经表明,知道一个函数是“纯”的可以如何帮助优化循环。

这是一种常见的子表达式消除方法(foo纯粹是给定的):

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

可以变成:

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

3
我不确定是否可以这样做,但是纯函数是否还允许编译器在调用函数时重新排序,如果认为重新排序会是有益的。如果有副作用的可能性,则编译器需要更加保守。
mpdonadio 2012年

@MPD-是的,听起来很合理。并且由于call指令是超标量CPU的瓶颈,因此编译器的一些帮助会有所帮助。
ArjunShankar

我隐约想起了几年前使用DSP编译器的情况,该编译器将使用此技术早/晚获取返回值。这样可以最大程度地减少管道停顿。
mpdonadio 2012年

1
既然99是一个常量,是否可以预先计算“ foo(99)”,而foo总是返回相同的结果?也许以某种两阶段的方式进行编译?
markwatson 2012年

1
@markwatson-我不确定。在某些情况下,这根本不可能。例如,如果它foo是另一个编译单元(另一个C文件)的一部分,或者是在预编译的库中。在这两种情况下,编译器都不知道做什么foo,并且无法进行预先计算。
ArjunShankar 2012年

28

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


2
+1,关于测试的观点很有趣。无需设置和拆卸。
ArjunShankar 2012年

15

非纯函数

int foo(int x, int y) // possible side-effects

就像纯函数的扩展

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

在其中,除了显式函数参数x,y之外,还有宇宙的其余部分(或计算机可以与之通信的任何内容)作为隐式潜在输入。同样,除了显式整数返回值之外,计算机可以写入的任何内容都隐含在返回值中。

应该清楚的是,为什么对纯函数进行推理要比非纯函数要容易得多。


1
+1:使用宇宙作为潜在输入是解释纯净与不纯净之间差异的一种很好的方法。
ArjunShankar 2012年

的确,这就是单子背后的想法。
Kristopher Micinski 2012年

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

通常,与不纯函数相比,Pure函数具有3个优点,编译器可以利用不纯函数:

快取

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

平行性

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

通过引用

一个函数按值f(struct t)获取其参数t,另一方面,编译器可以通过t引用传递给f它是否声明为纯函数,同时保证的值t不会改变并提高性能。


除了考虑编译时间外,还可以对纯函数进行相当简单的测试:只需调用它们即可。

无需构造对象或模拟与数据库/文件系统的连接。

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.