D中的纯

原文
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指向相同内存切片.执行后,qp一样的了.
一般,返回类型中有可变间接的纯函数的调用,不能立即认为是透明引用,但仍可优化许多调用,可能取决于调用代码如何使用返回值.

弱纯允许强保证

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开关时,允许在纯函数中使用不纯代码.

posted @   zjh6  阅读(17)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示