​D​​的纯及同其他特征的交互

纯是​​程序员和编译器​​帮助​​理解代码​​的利器.

​pure​​代表​​不访问全局可变状态​​的​​函数属性​​.​​全局​​指除了​​(不能在线程间引用共享数据的)函数参数外​​的东西.访问就是读写,未标记​​纯​​则为​​不纯​​.

即​​给定参数集​​,纯函数总是具有​​相同效果和或返回相同结果​​.因而​​纯​​不能调用​​不纯​​,且不能处理​​(经典意义的)io​​.

透明引用

​D​​中的​​纯​​可改变参数.下面完全有效:

int readAndIncrement(ref int x) pure {
return x++;
}

可能会让人感到惊讶,因为理论中​​纯度​​应是引用透明.即可用​​结果​​替换,而不改变语义(无副作用).但​​D​​不是这样的.如下:

int val = 1;
auto result = readAndIncrement(val) * readAndIncrement(val);
// assert(val == 3 && result == 2);

不能这样替代:

int val = 1;
auto tmp = readAndIncrement(val);
auto result = tmp * tmp;
// assert(val == 2 && result == 1);

如果,想达到经典的​​强纯​​呢?用​​const/immutable​​来标记数据:

int a(int[] val) pure;
int b(const int[] val) pure;
int c(immutable int[] val) pure;

​a​​与上面的​​readAndIncrement​​一致.而​​b和c​​,因为​​纯​​我们知道他们​​不更改​​任何全局状态,且​​参数​​不可变.所以​​b/c​​是经典意思的​​纯​​,即​​强纯​​,无副作用,调用他们是​​透明引用​​的.

​常/不变​​在此区别不大,但有个微妙但重要的​​影响调用​​区别:取决于调用参数是否为​​const/常/不变​​.​​常​​则​​可变/不变​​均可转为它.如果是​​不变数组​​调用,​​b/c​​都可应用.

如,实现​​缓存或消除公共子表达式机制​​,遇见带​​不变​​的​​纯​​函数,只需要检查参数的​​唯一性​​,以便将​​多次调用优化为一次​​.如按比较​​运行时实现的内存地址​​,或按在优化编译器的几个非常​​简单检查​​.

另一方面,如参数类型​​有间接​​且为​​常​​.则可在两次​​调用​​间修改数据.深度比较,对​​大数据结构​​或​​大量分析数据流​​就不可行,

同样分析也适用于并行化,如果​​纯​​无或仅有​​不变​​间接,则可保证安全并行化(因为不会有​​副作用​​),且参数中无​​数据竞争​​.但​​常​​则很难推断.因为​​可变​​可能会​​修改​​他们.

返回类型中的间接

上例,​​a,b,c​​因​​可变性(间接)​​而不同.但都返回​​整​​.如果​​返回​​引用,是否要​​考虑​​更多?

第1个要点是​​地址​​.在​​函数式语言​​中,值所在的实际​​内存地址​​一般不重要.而​​D​​暴露该概念,考虑:

ulong[] primes(uint count) pure

返回分配​​count​​个​​素数​​的数组.用相同​​count​​多次调用​​primes​​函数,返回相同数字,但地址不一样.

当考虑​​返回值​​中有​​带间接​​的​​函数透明引用​​时,重要的是​​逻辑相等(==)​​而非​​按位相等(is)​​.

对透明引用,另外重要的是​​返回类型​​中的​​可变间接引用​​.

auto p = primes(42);
auto q = primes(42);
p[] *= 2;

显然,重写第2个为​​auto q = p​​是无效的.这样的话,​​q​​指向相同内存切片.执行后,​​q​​与​​p​​一样的了.

一般,​​返回类型​​中有​​可变间接​​的纯函数的调用,不能立即认为是​​透明引用​​,但仍可优化许多调用,可能取决于​​调用代码​​如何使用返回值.

弱纯允许强保证

​D​​最初为​​纯/强纯​​,但讨论后,分成:​​弱纯/强纯​​,​​弱纯​​有像上面的​​readAndIncrement和a​​的​​可变​​参数.而强纯则类似​​b/c​​一样的​​无副作用​​.

纯度​​根据参数/返回类型的不同​​,而得到不同保证.

放松规则,允许更多的​​强纯​​函数.

如每次​​画三角形​​都复制​​帧缓冲区​​肯定不好.而在​​D​​中可实现​​纯​​三角形绘画函数,而不担心性能:

alias ubyte[4] Color;
struct Vertex { float[3] position; /* ... */ }
alias Vertex[3] Triangle;
void drawTriangle(Color[] framebuffer, const ref Triangle tri) pure;//画三角.

这里,​​画三角​​不可能是透明引用,因为它要写入​​帧缓冲区​​.但​​纯​​仍保证它不会​​更改任何隐藏/全局状态​​.同时,作为​​纯​​,别的​​纯​​可调用你.

如果可以​​每帧分配​​新缓冲区,这可能是渲染​​三角形组成的整个场景​​的函数.

Color[] renderScene(//渲染场景
const Triangle[] triangles,
ushort width = 640,
ushort height = 480
) pure {
auto image = new Color[width * height];
foreach (ref triangle; triangles) {
drawTriangle(image, triangle);
}
return image;
}

​注意​​,​​renderScene​​无可变间接.虽然内部调用​​可变参的drawTriangle​​,但​​renderScene​​作为整体是​​透明引用​​的!​​放松纯​​增加了​​纯​​函数.

现代编程,不鼓励​​使用​​全局状态,应将​​不处理I/O​​的大多数函数标记为​​纯​​.因为​​纯​​添加​​晚​​了,所以默认​​不纯​​.

模板和纯度

函数模板​​是否是纯​​可能由​​被实例化​​类型决定.如写接受​​区间​​,返回​​数组​​函数:

auto array(R)(R r) if (isInputRange!R) {
ElementType!R[] result;
while (!r.empty) {
result ~= r.front;
r.popFront();
}
return result;
}

问题是,能为​​纯​​吗?如果是​​map或filter​​,则可纯,但从​​标准输入​​读呢?这里没有​​r.empty,r.front和r.popFront()​​,如果标记为​​纯​​,则不能操作他们了.

​D​​最后为了实例化模板,其源必须可用,编译器自动​​推导纯度(及如不抛等其他类似属性)​​.

即,如果区间允许​​纯​​,则从​​纯函数调用​​.

如果​​纯​​不依赖于模板参数,显示指定​​纯​​也是好的.

纯成员函数

​构/类​​成员函数,也可为​​纯​​,与​​自由函数​​一样.只是对​​纯​​语义,增加了隐式的​​this​​参数.可以说​​纯函数​​可​​访问和修改​​成员变量了.

class Foo {
int getBar() const pure {
return bar;
}

void setBar(int bar) pure {
this.bar = bar;
}//纯

private int bar;
}

允许​​纯函数​​访问成员变量.标记​​const或immutable​​同样应用至​​this​​参数.

对类继承而言,​​子类成员函数​​假设更少,保证更多.​​纯函数​​可覆盖​​不纯​​函数,但反之则不然.

覆盖​​纯基类​​方法,隐式标记为​​纯​​.

纯与不变

由于​​与类型系统集成​​,纯函数​​返回值​​有时​​可安全转换​​为​​不变​​.考虑上面的​​ulong[] primes(uint n) pure​​.第1眼,下面代码为何编译,不明显:

immutable ulong[] p = primes(5);

这是在​​假设​​不存在其他可变引用​​primes​​的返回值,因而当然为​​纯​​(只用了一次嘛).它不带​​可变​​间接参数,不访问​​全局可变状态​​.

这实际上很有用.因为它允许在(​​函数式​​)不变数据​​上下文​​中无缝使用函数.

但仍要调用不纯函数

如处理遗留代码,通过用​​cast​​来处理,取函数指针,通过​​转换(cast)​​加​​pure​​属性.在​​@safe​​中是​​禁止​​的.

可引入​​assumePure​​模板来封装它.

为了调试,加​​不纯​​语句,因而,有​​debug​​开关时,允许在​​纯函数​​中使用​​不纯​​代码.