【问题标题】:Is a function that calls Math.random() pure?调用 Math.random() 的函数是纯函数吗?
【发布时间】:2018-04-12 14:35:54
【问题描述】:

下面是纯函数吗?

function test(min,max) {
   return  Math.random() * (max - min) + min;
}

我的理解是纯函数遵循以下条件:

  1. 它返回一个根据参数计算的值
  2. 除了计算返回值,它不做任何工作

如果这个定义是正确的,我的函数是纯函数吗?还是我对纯函数定义的理解不正确?

【问题讨论】:

  • "它除了计算返回值之外不做任何工作" 但是它调用Math.random() 改变了RNG的状态。
  • 第二点更像是“它不会改变外部(函数)状态”;正如人们在下面所写的那样,第一个应该补充一点,就像“它返回从 SAME 参数计算的 SAME 值”
  • 是否存在允许随机性的半纯函数的概念?例如。 test(a,b) 总是返回同一个对象 Random(a,b) (可以代表不同的具体数字)?如果你保持Random符号化,它在经典意义上是纯粹的,如果你早期评估它并输入数字,也许作为一种优化,函数仍然保留一些“纯粹性”。
  • “任何考虑产生随机数字的算术方法的人,当然都处于犯罪状态。” ——约翰·冯·诺依曼
  • @jdm 如果您遵循“半纯”的主题,您认为函数纯模有一些定义明确的副作用,您最终可能会发明单子。欢迎来到黑暗面。 >:)

标签: javascript function pure-function


【解决方案1】:

不,不是。给定相同的输入,此函数将返回不同的值。然后你就不能建立一个映射输入和输出的“表”。

来自Pure function 的维基百科文章:

函数总是在给定相同的情况下评估相同的结果值 参数值。函数结果值不能依赖于任何 程序执行时可能改变的隐藏信息或状态 进行或在程序的不同执行之间,也不能 依赖于来自 I/O 设备的任何外部输入

另外,另一件事是纯函数可以替换为表示输入和输出映射的表,如 this thread 中所述。

如果你想重写这个函数并将其更改为纯函数,你也应该将随机值作为参数传递

function test(random, min, max) {
   return random * (max - min) + min;
}

然后以这种方式调用它(例如,以 2 和 5 作为最小值和最大值):

test( Math.random(), 2, 5)

【讨论】:

  • 如果在调用 Math.random 之前每次都在函数内部重新播种随机生成器会怎样?
  • @cᴏʟᴅsᴘᴇᴇᴅ 即使那样,它仍然会有副作用(改变未来Math.random 输出);为了让它变得纯净,你必须以某种方式保存当前的 RNG 状态,重新播种,调用Math.random,并将其恢复到以前的状态。
  • @cᴏʟᴅsᴘᴇᴇᴅ 所有计算的 RNG 都是基于伪造随机性。必须有一些东西在下面运行,导致它看起来是随机的,你无法解释这一点,使其不纯。此外,可能对您的问题更重要的是,您不能播种 Math.random
  • @LegionMammal978 ...并以原子方式进行。
  • @cᴏʟᴅsᴘᴇᴇᴅ 有一些方法可以让 RNG 使用纯函数进行操作,但它涉及将 RNG 状态传递给函数并让函数返回替换的 RNG 状态,这就是 Haskell(函数式强制功能纯度的编程语言)实现了它。
【解决方案2】:

您的问题的简单答案是 Math.random() 违反了规则 #2。

这里的许多其他答案都指出Math.random() 的存在意味着这个函数不是纯粹的。但我认为值得一说为什么 Math.random() 污染了使用它的函数。

与所有伪随机数生成器一样,Math.random() 以“种子”值开头。然后,它使用该值作为一系列低级位操作或其他操作的起点,这些操作会导致不可预测(但不是真正的随机)输出。

在 JavaScript 中,所涉及的过程是依赖于实现的,与许多其他语言不同,JavaScript 提供了no way to select the seed:

实现选择随机数生成算法的初始种子;用户不能选择或重置它。

这就是为什么这个函数不是纯函数的原因:JavaScript 本质上是在使用一个你无法控制的隐式函数参数。它从其他地方计算和存储的数据中读取该参数,因此违反了您定义中的规则 #2。

如果你想让它成为一个纯函数,你可以使用here 描述的替代随机数生成器之一。打电话给那个生成器seedable_random。它接受一个参数(种子)并返回一个“随机”数。当然,这个数字根本不是随机的。它是由种子唯一决定的。这就是为什么这是一个纯函数。 seedable_random 的输出只是“随机”,因为很难根据输入预测输出。

这个函数的纯版本需要三个参数:

function test(min, max, seed) {
   return  seedable_random(seed) * (max - min) + min;
}

对于任何给定的三元组(min, max, seed) 参数,这将始终返回相同的结果。

请注意,如果您希望seedable_random 的输出是真正随机的,您需要找到一种随机化种子的方法!你使用的任何策略都不可避免地是不纯的,因为它需要你从你的职能之外的来源收集信息。正如mtraceur 和jpmc26 提醒我的那样,这包括所有物理方法:hardware random number generators、webcams with lens caps、atmospheric noise collectors——甚至是lava lamps。所有这些都涉及使用在函数外部计算和存储的数据。

【讨论】:

  • Math.random() 不仅读取它的“种子”,还修改它,以便下一次调用将返回不同的东西。依赖和修改静态状态对于纯函数肯定是不好的。
  • @NateEldredge,确实如此!尽管简单地读取依赖于实现的值就足以破坏纯度。例如,有没有注意到 Python 3 哈希在进程之间是如何不稳定的?
  • 如果Math.random 不使用 PRNG 而是使用硬件 RNG 实现,这个答案将如何变化?硬件 RNG 并没有真正意义上的状态,但它确实会产生随机值(因此无论输入如何,函数输出仍然不同),对吧?
  • @mtraceur,没错。但我认为答案不会有太大变化。实际上,这就是为什么我不在回答中花时间谈论“状态”的原因。从硬件 RNG 中读取也意味着从“在其他地方计算和存储的数据”中读取。只是数据在与环境交互时被计算并存储在计算机本身的物理介质中。
  • 同样的逻辑甚至适用于更复杂的随机化方案,甚至像 Random.org's atmospheric noise 这样的随机化方案。 +1
【解决方案3】:

纯函数是返回值仅由其输入值决定的函数,没有可观察到的副作用

通过使用 Math.random,您可以通过输入值以外的其他方式确定其值。这不是一个纯函数。

source

【讨论】:

    【解决方案4】:

    不,它不是纯函数,因为它的输出不依赖于仅提供的输入(Math.random() 可以输出任何值),而纯函数应该始终输出相同输入的相同值。

    如果一个函数是纯函数,则可以安全地优化具有相同输入的多个调用并重用之前调用的结果。

    P.S 至少对我和其他许多人来说,redux 使 纯函数 一词流行起来。 Straight from the redux docs:

    你不应该在 reducer 中做的事情:

    • 改变其参数;

    • 执行 API 调用和路由转换等副作用;

    • 调用非纯函数,例如Date.now() 或 Math.random()。

    【讨论】:

    • 虽然其他人已经提供了很好的答案,但是当我想到 redux doc 并且其中特别提到 Math.random() 时,我无法抗拒自己:)
    【解决方案5】:

    从数学的角度来看,你的签名不是

    test: <number, number> -> <number>
    

    但是

    test: <environment, number, number> -> <environment, number>
    

    environment 能够提供Math.random() 的结果。 并且实际上生成随机值会改变环境作为副作用,所以你也返回一个新的环境,它不等于第一个!

    换句话说,如果您需要任何类型的输入不是来自初始参数(&lt;number, number&gt; 部分),那么您需要提供执行环境(在此示例中为Math 提供状态) .这同样适用于其他答案中提到的其他事情,例如 I/O 等。


    作为一个类比,您还可以注意到这就是面向对象编程的表示方式 - 如果我们说,例如

    SomeClass something
    T result = something.foo(x, y)
    

    那么实际上我们正在使用

    foo: <something: SomeClass, x: Object, y: Object> -> <SomeClass, T>
    

    将调用其方法的对象作为环境的一部分。为什么SomeClass 部分结果?因为something 的状态也可能发生了变化!

    【讨论】:

    • 更糟糕的是,环境也发生了变异,所以test: &lt;environment, number, number&gt; -&gt; &lt;environment, number&gt;应该是
    • 我不确定 OO 示例是否非常相似。 a.F(b, c) 可以看作是F(a, b, c) 的语法糖,具有一个特殊的规则,可以根据a 的类型发送F 的重载定义(这实际上是Python 表示它的方式)。但是a 在这两种符号中仍然是显式的,而源代码中从未提及非纯函数中的环境。
    【解决方案6】:

    纯函数总是为相同的输入返回相同的值。 纯函数是可预测的并且是引用透明的,这意味着我们可以用返回的输出替换函数调用,它不会改变程序的工作。

    https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch3.md

    【讨论】:

      【解决方案7】:

      除了正确指出此函数如何不确定的其他答案外,它还有一个副作用:它将导致将来对math.random() 的调用返回不同的答案。不具有该属性的随机数生成器通常会执行某种 I/O,例如从操作系统提供的随机设备中读取。对于纯函数来说,要么是禁止的。

      【讨论】:

        【解决方案8】:

        不,不是。你根本想不通结果,所以这段代码无法测试。为了使该代码可测试,您需要提取生成随机数的组件:

        function test(min, max, generator) {
          return  generator() * (max - min) + min;
        }
        

        现在,您可以模拟生成器并正确测试您的代码:

        const result = test(1, 2, () => 3);
        result == 4 //always true
        

        在你的“生产”代码中:

        const result = test(1, 2, Math.random);
        

        【讨论】:

        • ▲ 对于可测试性的思考。稍加注意,您还可以在接受util.Random 的同时生成可重复的测试,您可以在测试运行开始时播种以重复旧行为或新的(但可重复的)运行。如果是多线程,您可以在主线程中执行此操作并使用该Random 播种可重复线程本地Randoms。但是,据我了解,test(int,int,Random) 不被认为是纯粹的,因为它改变了Random 的状态。
        【解决方案9】:

        您可以接受以下内容吗:

        return ("" + test(0,1)) + test(0,1);
        

        相当于

        var temp = test(0, 1);
        return ("" + temp) + temp;
        

        ?

        你看,pure 的定义是一个函数,它的输出除了输入之外没有任何变化。如果我们说 JavaScript 有办法标记一个纯函数并利用这一点,那么优化器将被允许将第一个表达式重写为第二个。

        我有这方面的实践经验。 SQL 服务器允许 getdate() 和 newid() 在“纯”函数中使用,优化器会随意对调用进行重复数据删除。有时这会做一些愚蠢的事情。

        【讨论】:

          猜你喜欢
          • 2017-01-22
          • 2019-10-05
          • 1970-01-01
          • 2021-05-15
          • 1970-01-01
          • 2019-12-14
          • 2019-12-04
          • 1970-01-01
          • 2018-04-30
          相关资源
          最近更新 更多