【问题标题】:Pure Functions: Does "No Side Effects" Imply "Always Same Output, Given Same Input"?纯函数:“无副作用”是否意味着“总是相同的输出,给定相同的输入”?
【发布时间】:2019-07-26 07:12:28
【问题描述】:

定义函数为pure的两个条件如下:

  1. 没有副作用(即只允许更改本地范围)
  2. 在给定相同输入的情况下始终返回相同的输出

如果第一个条件始终为真,是否有任何时候第二个条件不为真?

即真的只有第一个条件才有必要吗?

【问题讨论】:

  • 您的场所不明确。 “输入”太宽泛了。可以认为函数有两种输入。他们的论点和“环境”/“上下文”。如果您不区分这两种输入,则可以认为返回系统时间的函数是纯函数(即使它不是 obv)。
  • @Alexander:在“纯函数”的上下文中,“输入”通常被理解为显式传递的参数/参数(通过编程语言使用的任何机制)。这是“纯功能”定义的一部分。但你是对的,记住定义很重要。
  • 简单反例:返回一个全局变量的值。没有副作用(仅读取全局变量!),但每次仍然可能产生不同的结果。 (如果您不喜欢全局变量,请返回一个局部变量的地址,该变量取决于运行时的调用堆栈)。
  • 你需要扩展你对“副作用”的定义;您说纯方法不会产生副作用,但您还需要注意纯方法不会消耗其他地方产生的副作用。
  • @sleske 也许大家都能理解,但缺乏这种区别才是 OP 混乱的确切原因。

标签: javascript functional-programming language-lawyer pure-function


【解决方案1】:

以下是几个不改变外部范围但仍被认为不纯的反例:

  • function a() { return Date.now(); }
  • function b() { return window.globalMutableVar; }
  • function c() { return document.getElementById("myInput").value; }
  • function d() { return Math.random(); }(诚然确实改变了 PRNG,但不被认为是可观察的)

访问非常量非局部变量足以违反第二个条件。

我一直认为纯洁的两个条件是互补的:

  • 结果评估不得对侧面状态有影响
  • 评估结果不得受到侧面状态的影响

术语side effect 仅指第一个,修改非本地状态的功能。但是,有时读取操作也被视为副作用:当它们是操作并且也涉及写入时,即使它们的主要目的是访问一个值。示例包括生成修改生成器内部状态的伪随机数、从推进读取位置的输入流读取数据或从涉及“进行测量”命令的外部传感器读取数据。

【讨论】:

  • 谢谢伯吉。出于某种原因,我认为副作用包括 reading 局部范围之外的变量,但我想如果它 write 这样的外部变量只是一个副作用。
  • 如果prompt("you choose")没有副作用,我们应该退后一步,明确副作用的含义。
  • @Magnus 是的,这正是 effect 的意思。我也会尝试在我的回答中澄清一下,我没想到会受到如此大的关注,并且想让答案值得几十票:-)
  • 众所周知,Math.random() 返回一个热二极管。实际上并没有指定使用错误的 RNG。
  • 在这两种情况中,我听说前者称为“效应”,而后者称为“协同效应”。两者都是“副作用”且不纯。 f(coeffects, input) -> effects, output Coeffects是来自更广泛环境变化的输入,effects是改变更广泛环境的输出。例如,Elm 和 Clojurescrips 重新构建了使用此模型的框架。
【解决方案2】:

描述纯函数是什么的“正常”方式是根据引用透明度。如果一个函数是引用透明的,那么它就是

引用透明,粗略的意思是,你可以在程序中的任何一点用函数的返回值替换对函数的调用,反之亦然,而不改变程序的含义。

因此,例如,如果 C 的 printf 是引用透明的,那么这两个程序应该具有相同的含义:

printf("Hello");

5;

并且以下所有程序应该具有相同的含义:

5 + 5;

printf("Hello") + 5;

printf("Hello") + printf("Hello");

因为printf 返回写入的字符数,在本例中为 5。

使用void 函数会更加明显。如果我有一个函数void foo,那么

foo(bar, baz, quux);

应该和

一样
;

即由于foo 什么都不返回,我应该可以在不改变程序含义的情况下将其替换为空。

那么很明显,printffoo 都不是引用透明的,因此它们都不是纯的。事实上,void 函数永远不可能是引用透明的,除非它是空操作。

我发现这个定义比你给出的更容易处理。它还允许您以任何所需的粒度应用它:您可以将它应用到单个表达式、函数、整个程序。例如,它允许您谈论这样的函数:

func fib(n):
    return memo[n] if memo.has_key?(n)
    return 1 if n <= 1
    return memo[n] = fib(n-1) + fib(n-2)

我们可以分析组成函数的表达式,很容易得出结论,它们不是引用透明的,因此也不是纯粹的,因为它们使用可变数据结构,即memo 数组。但是,我们也可以查看该函数,并且可以看到它引用透明的,因此是纯的。这有时被称为外部纯度,即一个在外界看来是纯粹的,但在内部实现却不纯粹的函数。

这样的函数还是有用的,因为杂质会感染它周围的一切,外部的纯接口构建了一种“纯度屏障”,杂质只感染函数的三行,但不会泄漏到其余部分的程序。这三行代码比整个程序更容易分析正确性。

【讨论】:

  • 一旦你有了并发,杂质就会影响整个程序。
  • @R.. 你能想出一种方法,并发可以使所描述的斐波那契函数在外部不纯吗?我不能。写入memo[n] 是幂等的,无法读取它只会浪费 CPU 周期。
  • 我同意你们两个。杂质可能导致并发问题,但在这种特定情况下不会。
  • @R.. 不难想象一个并发感知版本。
  • @Brilliand 例如,memo[n] = ... 可能首先创建一个字典条目,然后将值存储到其中。这会留下一个窗口,在此期间另一个线程可以看到未初始化的条目。
【解决方案3】:

在我看来,您描述的第二个条件比第一个条件更弱。

让我举个例子,假设你有一个函数可以添加一个也记录到控制台的函数:

function addOneAndLog(x) {
  console.log(x);
  return x + 1;
}

您提供的第二个条件得到满足:当给定相同的输入时,此函数始终返回相同的输出。但是,它不是一个纯函数,因为它包含登录到控制台的副作用。

严格来说,纯函数是满足referential transparency属性的函数。这是我们可以在不改变程序行为的情况下用函数应用程序产生的值替换函数应用程序的属性。

假设我们有一个简单的添加函数:

function addOne(x) {
  return x + 1;
}

我们可以在程序中的任何位置将addOne(5) 替换为6,不会有任何改变。

相比之下,我们不能在程序中的任何地方用值 6 替换 addOneAndLog(x) 而不改变行为,因为第一个表达式导致将某些内容写入控制台,而第二个则不会。

我们将addOneAndLog(x) 执行的任何这种额外行为视为副作用之外的任何返回输出。

【讨论】:

  • “在我看来,您描述的第二个条件比第一个条件更弱。”不,这两个条件在逻辑上是独立的。
  • @sleske 你错了。我已经为术语纯粹和副作用提供了明确的定义。在这些约束条件下,除了为给定的输入返回相同的输出之外,没有任何副作用的函数。然而,我提供了在没有第一个条件的情况下可以满足第二个条件的示例。理解纯度概念的基本概念是参照透明性。
  • 小错别字:除了为给定输入返回相同的输出之外,没有副作用的函数无能为力
  • 返回当前时间之类的呢?这没有副作用,但它确实为相同的输入返回不同的输出。或者更一般地说,任何结果不仅取决于输入参数,还取决于(可变)全局变量的函数。
  • 看来您使用的“副作用”定义与常用定义不同。副作用通常被定义为“除了返回值之外的可观察到的效果”或“可观察到的状态变化” - 参见例如Wikipediathis post on softwareengineering.SE。你完全正确Date.now() 不是纯粹的/引用透明的,但不是因为它有副作用,而是因为它的结果不仅仅取决于它的输入。
【解决方案4】:

可能存在来自系统外部的随机性来源。假设您的计算部分包括室温。然后根据室温的随机外部因素,每次执行该函数都会产生不同的结果。执行程序不会改变状态。

反正我能想到的。

【讨论】:

  • 在我看来,这些“来自系统外部的随机性”是一种副作用。具有这些行为的函数不是“纯粹的”。
  • 您可以将温度作为参数传递给函数
【解决方案5】:

FP 定义的问题在于它们非常人为。每个评估/计算对评估者都有副作用。理论上是正确的。否认这一点仅表明 FP 辩护者忽略了哲学和逻辑:“评估”意味着改变某些智能环境(机器、大脑等)的状态。这是评估过程的本质。没有变化 - 没有“演算”。效果非常明显:CPU 发热或其故障、主板过热时关闭等等。

当您谈论引用透明性时,您应该了解有关这种透明性的信息对于作为整个系统的创建者和语义信息持有者的人类来说是可用的,而对于编译器可能是不可用的。例如,一个函数可以读取一些外部资源,它的签名中有 IO monad,但它会始终返回相同的值(例如,current_year &gt; 0 的结果)。编译器不知道函数总是返回相同的结果,所以函数是不纯的,但具有引用透明的属性,可以用True常量替换。

所以,为了避免这种不准确,我们应该区分数学函数和编程语言中的“函数”。 Haskell 中的函数总是不纯的,与它们相关的纯度定义总是非常有条件的:它们运行在具有真实副作用和物理特性的真实硬件上,这对于数学函数是错误的。这意味着带有“printf”函数的例子是完全错误的。

但并非所有数学函数都是纯函数:每个以t(时间)为参数的函数可能不纯:t 包含函数的所有效果和随机性质:通常情况下,您有输入信号和不知道实际值,甚至可能是噪音。

【讨论】:

    【解决方案6】:

    如果第一个条件始终为真,那么是否有任何时候第二个 条件不成立?

    是的

    考虑下面的简单代码sn-p

    public int Sum(int a, int b) {
        Random rnd = new Random();
        return rnd.Next(1, 10);
    }
    

    此代码将为相同的给定输入集返回随机输出 - 但是它没有任何副作用。

    你提到的#1和#2点结合在一起的整体效果意味着:在任何时间点,如果函数Sum具有相同的i/p在程序中被替换为它的结果,总体而言程序的含义不会改变。这不过是Referential transparency

    【讨论】:

    • 但是在这种情况下,第一个条件没有得到验证:写入控制台被认为是副作用,因为它会改变机器本身的状态。
    • @Rightleg 感谢您指出这一点。不知何故,我完全以其他方式误解了 OP。正确答案。
    • 不会改变随机发生器的状态吗?
    • 生成随机数本身就是一个副作用,除非显式提供随机数生成器的状态,这将使函数满足条件 2。
    • rnd 不会转义函数,因此其状态更改这一事实与函数的纯度无关,但 Random 构造函数使用当前时间作为种子值意味着除了ab 之外还有“输入”。
    猜你喜欢
    • 2017-07-17
    • 2015-12-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-15
    • 1970-01-01
    • 2018-11-16
    相关资源
    最近更新 更多