【问题标题】:Why is function with useless isolated `static` considered impure?为什么具有无用的孤立“静态”的函数被认为是不纯的?
【发布时间】:2020-06-18 11:53:04
【问题描述】:

Pure function 上的维基百科文章中,有一个这样的不纯函数示例:

void f() {
  static int x = 0;
  ++x;
}

加上“因为局部静态变量的突变”的说法。

我想知道为什么它不纯?它是从单位类型到单位类型的,因此对于相同的输入,它总是返回相同的结果。而且它没有副作用,因为尽管它有 static int 变量,但除此 f() 之外的任何其他函数都无法观察到它,因此其他函数可能使用的全局状态没有可观察到的突变。

如果有人认为不允许任何全局突变,无论它们是否可观察,那么现实生活中的任何函数都不能被认为是纯函数,因为任何函数都会在堆栈上分配其内存,而分配是不纯的,因为它涉及通过操作系统与 MMU 通信,分配的页面可能位于不同的物理页面中,等等。

那么,为什么这个无用的隔离 static int 会使函数不纯?

【问题讨论】:

  • 例如:如果我没记错的话,经常调用f 会导致整数溢出和未定义的行为。
  • 如果x 真的足够孤立,那么f 可以被认为是纯粹的。但通常这意味着您实际上并不需要该变量。
  • 我认为这个问题可以更好地表述为“为什么将这种不纯函数的退化情况视为不纯的情况有用?”因为我们不能反对所提供的定义(函数改变了一个局部静态变量。这就是定义。讨论结束)。
  • 该示例被精简以仅突出这一方面:局部静态变量的突变。马虎地说“纯粹”的意思是,对于相同的输入,函数的作用完全相同
  • 如果x 只是被操纵而从未被访问过,那么它是一个足够聪明的优化编译器消除的候选对象。这里的问题是“x 是否会影响任何其他计算?”如果没有,则可以安全地删除 x。如果可以删除,那么该功能可以制作纯功能,但需要删除该术语。

标签: c++ functional-programming category-theory


【解决方案1】:

纯函数的结果完全由其输入参数定义。在这里,结果不仅意味着返回值,还意味着在 C/C++ 标准定义的虚拟机方面的效果。换句话说,如果函数偶尔相同输入参数的情况下表现出未定义的行为,它不能被认为是纯粹的(因为行为不同于一个调用另一个具有相同的输入)。

在使用静态局部变量的特殊情况下,如果在多个线程中同时调用 f,该变量可能会成为数据竞争的来源。数据竞争意味着未定义的行为。 UB 的另一个可能来源是有符号整数溢出,这最终可能会发生。

【讨论】:

  • 我看不出数据竞争是如何发生的。对x 的任何更改都无关紧要,无论计算是否有效(x 已递增)与否(UB 发生,x 已递增两次等),因为 f() 在其形式中是没有办法的要将x 的内容传达给调用者,调用者都不能以任何方式绕过f() 读取x。并且允许纯函数crashnever return,因为_ -> ⊥ 的有效期限就像这样f = f ()
  • > 对 x 的任何更改都无关紧要——它确实很重要。数据竞争意味着行为是真正未定义的。该实现允许崩溃或格式化您的硬盘驱动器或其他任何东西。实际上,增量期间的数据竞争可能会产生int 的捕获表示,这将在下次调用f 时发出信号并崩溃。
  • 我不同意。如果我有一个函数int f(i) { return i + 1; }i + 1 可能会溢出。这不纯粹吗?我会认为这个函数是纯粹的。
  • > 并且允许纯函数崩溃或永不返回 -- 是的,但仅对于相同的输入始终一致。 IE。如果它因某些输入而崩溃,它必须每次都因相同的输入而崩溃,但是无论何时调用该函数。
  • @Ayxan 行为(或未定义行为)取决于输入,因此它是纯粹的。在 OPs 情况下,这取决于您调用函数的频率(对于溢出情况)。
【解决方案2】:

纯函数的概念似乎只在...函数式语言中很重要?如果我错了纠正我。您提供的维基百科链接在顶部附近提供了两个参考,其中之一是Professor Frisby's Mostly Adequate Guide to Functional Programming。纯函数有几种不同的限定条件,包括:

没有任何可观察到的副作用

这很重要,因为我们可以对纯函数(而不是不纯函数)做的事情之一是记忆(来自上面的链接)或输入/输出缓存。纯函数也是可测试的、合理的和自我记录的。

我猜记忆对编译器很重要,所以询问一个函数是否“纯”可以被认为等同于询问编译器是否可以记忆该函数。似乎没有其他代码涉及的 static 局部变量的概念只是坏代码,编译器应该发出警告。但是编译器应该优化它吗?编译器是否应该尝试确定任何给定的static 局部变量是否实际上没有副作用?

似乎更容易将编译器设计为总是标记一个函数是不纯的,如果它有一个 static 本地,而不是编写逻辑来处理函数是否可记忆。看到本地的static?繁荣:不再纯粹。

所以从编译器的角度来看,它是不纯的。

纯函数的其他性质呢?

可测试、合理且可自我记录

测试通常是由一个人编写的,所以我认为这个函数是可测试的。尽管一些自动化测试编写软件可能会再次发现它不可记忆,并选择完全忽略为它编写测试。这个假设的软件可能会跳过本地statics 的任何内容。同样,假设性。

代码合理吗?当然不是。虽然我不确定这有多重要。它不任何事情。这让人很难理解。 (“为什么 Bob 以这种方式编写函数?这是一个 Magic/More Magic 情况吗?”)。

代码是自记录的吗?再说一次,我会说不是。但同样,这是退化的示例代码。

我认为反对这被认为是纯函数的最大论据是,如果函数式语言编译器只是假设它不是纯函数,那么它是完全合理的。

我认为 这被认为是一个纯函数的最大论据是,我们可以用自己的眼球观察它,并看到显然没有外部行为。忽略有符号溢出未定义的事实。将其替换为已定义溢出且是原子的数据类型。好吧,现在没有未定义的行为,但它仍然看起来很奇怪。

“总而言之,我不在乎它是否纯净。”

让我重新表述一下我之前的(以上)结论。

我倾向于只扫描一个函数以查找静态变量的任何突变,然后就结束了。轰隆隆,不再纯粹。

如果我们认真思考,这个函数是否可以被认为是纯函数?当然。但有什么意义呢?如果需要更改纯函数的定义,请争取更改。好像你认为这是一个纯函数。很好,我看到了其中的优点。我也看到了将其视为不纯函数的优点。

尽管这是一个非答案,这真的取决于您使用 pure 的定义是为了什么。如果它正在编写一个编译器?可能希望使用更保守的 pure 定义,允许误报并排除此功能。如果是为了在听 Zep 的同时给一群大二的 CS 学生留下深刻印象?去寻找承认这没有副作用的定义,然后收工。

【讨论】:

  • “我不在乎它是否纯净。”嗯,我愿意。至于您描述的属性(例如“自我记录”和“合理”),它们很花哨,但与纯度无关,因为您无法正式定义它们。不过,参考透明度之类的东西也可以。
  • @toriningen 我们每天处理未正式定义的事情。这就是生命的本质。如果你想写一篇关于纯的非常严格的定义很重要的论文,那么请将此函数称为纯。如果你想使用 pure 的定义来做有用的工作,你可能需要稍微放松一下,为了5点回家,调用这个函数不纯也可以。
  • 我宁愿说反话。术语“纯函数”被定义为实用。如果定义会说“那么纯粹意味着相同输入的相同输出”那将是无用的,但是对该术语有一个精确的定义才是它有用的原因
猜你喜欢
  • 2020-06-09
  • 2016-04-06
  • 2011-10-24
  • 2016-04-22
  • 2015-05-30
  • 1970-01-01
  • 1970-01-01
  • 2015-12-13
  • 1970-01-01
相关资源
最近更新 更多