调用Math.random()的函数是纯函数吗?

115

以下函数是纯函数吗?

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

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

  1. 它返回从参数计算出的值
  2. 除了计算返回值之外,它不执行任何其他工作

如果这个定义正确,那么我的函数是一个纯函数吗?或者说,我对定义纯函数的理解是错误的吗?


68
除了计算返回值以外,它不执行任何工作。但是它调用了 Math.random() 方法,这会改变随机数生成器的状态。 - Paul Draper
1
第二点更像是“它不改变外部状态(函数之外的状态)”; 第一点应该补充一些,比如“它返回从相同参数计算出的相同值”,就像下面人们写的那样。 - MVCDS
是否有半纯函数的概念,允许随机性?例如,test(a,b)总是返回相同的对象Random(a,b)(它可以表示不同的具体数字)?如果保持Random符号,则在经典意义上是纯的,如果您早期评估并输入数字,可能作为一种优化,该函数仍然保留一些“纯度”。 - jdm
1
“任何考虑使用算术方法生成随机数字的人,当然是处于一种罪恶状态。”- 约翰·冯·诺伊曼 - Steve Kuo
1
@jdm 如果你遵循“半纯”的线索,其中你认为函数在某些明确定义的副作用模下是纯的,那么你可能最终会发明单子。欢迎来到黑暗面。>:) - luqui
显示剩余5条评论
9个回答

193
不是这样的。对于相同的输入,此函数将返回不同的值。因此,您无法构建一个将输入和输出映射的“表格”。 来自Pure function维基百科文章: 该函数始终在给定相同的参数值时计算相同的结果值。函数结果值不能依赖于程序执行期间或程序的不同执行之间可能发生变化的任何隐藏信息或状态,也不能依赖于来自I/O设备的任何外部输入。 此外,另一件事是,可以使用代表从输入到输出的映射的表格替换纯函数,如this thread所解释的那样。 如果要重写此函数并将其更改为纯函数,则还应将随机值作为参数传递。
function test(random, min, max) {
   return random * (max - min) + min;
}

然后这样调用它(例如,使用2和5作为最小值和最大值):

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

2
如果每次在调用 Math.random 之前在函数内部重新播种随机生成器会怎样? - cs95
18
即使这样,它仍然会有副作用(改变未来的 Math.random 输出);要使其纯净,您需要以某种方式保存当前的 RNG 状态,重新生成种子,调用 Math.random,然后将其恢复到先前的状态。 - LegionMammal978
3
所有计算的随机数生成都是基于伪造随机性。必须有一些底层运行的东西导致它看起来是随机的,而你无法对其进行解释,这使它不纯。此外,对于你的问题来说可能更重要的是,你无法种植Math.random。 - zfrisch
15
…并且要原子化地执行。 - wchargin
2
@cᴏʟᴅsᴘᴇᴇᴅ 有一些方法可以使用纯函数来操作RNG,但这需要将RNG状态传递给函数,并使函数返回替换的RNG状态,这就是Haskell(一种强制实现函数纯度的函数式编程语言)实现的方式。 - Pharap
显示剩余10条评论

53

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

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

像所有的伪随机数生成器一样,Math.random() 从一个“种子”值开始。然后使用该值作为低级位操作或其他操作链的起点,产生一个不可预测(但并非真正的随机)输出。

在 JavaScript 中,涉及的过程是与具体实现相关的,而且与许多其他语言不同,JavaScript 没有提供一种方法来选择种子:无法通过用户的手动设置来重置或获取初始的种子

实现选择了用于随机数生成算法的初始种子;用户不能对其进行选择或重置。

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

如果您想将其变成纯函数,可以使用其中一个替代的随机数生成器,这些生成器在这里描述。将该生成器称为 seedable_random,它接受一个参数(种子)并返回一个“随机”数。当然,这个数实际上并不是真正的随机数;它由种子唯一确定。这就是为什么这是一个纯函数。 seedable_random 的输出只是“随机”的,因为基于输入预测其输出是困难的。

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

function test(min, max, seed) {
   return  seedable_random(seed) * (max - min) + min;
}
对于给定的三个参数 (min、max、seed),这个函数每次调用都会得到相同的结果。
请注意,如果您希望 seedable_random 的输出是真正的随机数,您需要找到一种随机化种子的方法!而无论使用什么策略,都必须从函数外部收集信息,因此它不可避免地是非纯的。就像mtraceurjpmc26提醒我的那样,这包括所有物理方法:硬件随机数生成器带有镜头盖的网络摄像头大气噪声采集器 - 甚至熔岩灯。所有这些都涉及在函数外部计算和存储数据。

8
Math.random()不仅读取其“种子”,而且还修改它,以便下一次调用将返回不同的内容。根据和修改静态状态对于纯函数来说绝对是不好的。 - Nate Eldredge
2
@NateEldredge,确实如此!但仅仅读取一个依赖于实现的值就足以破坏纯度。例如,你是否注意到Python 3哈希在进程之间不稳定? - senderle
2
如果 Math.random 不使用伪随机数生成器而是使用硬件随机数生成器实现,那么这个答案会有什么变化呢?硬件随机数生成器在正常情况下并没有状态,但它确实产生随机值(因此函数的输出仍然与输入无关),对吗? - mtraceur
@PaŭloEbermann,我能理解“预测”并不完全正确。但这通常是哈希函数的描述方式,而你所说的也是它们的真实情况,不是吗? - senderle
1
即使是更复杂的随机化方案,甚至像Random.org的大气噪声这样的方案,也适用于相同的逻辑。+1 - jpmc26
显示剩余5条评论

40

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

使用 Math.random 会通过其他方式确定其值,这不是一个纯函数。

来源


25
不,它不是纯函数,因为其输出不仅取决于提供的输入(Math.random()可以输出任何值),而纯函数应始终为相同的输入输出相同的值。
如果一个函数是纯函数,那么安全地优化多次使用相同输入的调用,并且只需重复使用先前调用的结果。
P.S 对我来说至少对许多人来说,redux使术语“纯函数”变得流行。来自redux文档
不应在reducer内执行以下操作:
- 更改其参数; - 执行诸如API调用和路由转换之类的副作用; - 调用非纯函数,例如Date.now()或Math.random()。

3
虽然其他人已经给出了很好的答案,但当我想到Redux文档并特别提到Math.random()时,我还是忍不住了 :) - Shubhnik Singh

21

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

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

但是

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

在这里,环境能够提供Math.random()的结果。实际生成随机值将会作为副作用改变环境,因此你需要返回一个新的环境,它与第一个环境不相等!

换句话说,如果您需要任何不来自初始参数(<number, number>部分)的输入,则需要提供执行环境(在本例中为Math状态提供状态)。其他回答提到的其他内容(例如I/O等)也适用于此。


类比地说,您还可以注意到这是面向对象编程的表示方式——如果我们说,例如

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

那么实际上我们正在使用

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

通过调用方法的对象成为环境的一部分。为什么结果中有SomeClass这部分呢?因为something的状态也可能已经改变了!


7
更糟糕的是,环境也发生了变异,因此 test:<环境,数字,数字>→<环境,数字> - Bergi
1
我不确定面向对象的例子是否非常相似。a.F(b, c)可以看作是对F(a, b, c)的语法糖,有一个特殊规则来根据a的类型分配到重载定义的F(这实际上是Python的表示方式)。但是,不管哪种写法,a都是显式的,而在非纯函数中,环境从未在源代码中提到。 - IMSoP

11

10

除了其他正确指出这个函数是不确定性的答案之外,它还有一个副作用:它会导致未来调用math.random()返回不同的答案。而没有这种属性的随机数生成器通常会执行某种I/O操作,例如从操作系统提供的随机设备中读取。这两种操作都不适用于纯函数。


7
不,这不是可测试的。你根本无法预测结果,因此这段代码无法进行测试。为了使该代码可测试,您需要提取生成随机数的组件:
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);

1
▲为了测试可测试性而思考。只需小心,您也可以在接受util.Random的同时生成可重复的测试,您可以在测试运行开始时对其进行种子处理以重复旧行为或进行新的(但可重复的)运行。如果涉及多线程,则可以在主线程中执行此操作并使用该Random来种子可重复的线程本地Random。但是,据我所知,test(int,int,Random)不被认为是纯净的,因为它改变了Random的状态。 - PJTraill

2
你对以下内容是否满意:

您是否愿意接受以下内容:

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

等同于

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

你知道,纯函数的定义是其输出仅与输入相关。如果我们说JavaScript有一种方法可以标记一个函数为纯函数,并利用它,那么优化器将被允许将第一个表达式重写为第二个表达式。

我在实践中有过这方面的经验。SQL Server允许在“纯”函数中使用getdate()newid(),并且优化器会根据需要去重调用。但有时这会做出一些愚蠢的事情。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接