F#(或任何函数式语言)可以在同一输入上多次应用函数的理论漏洞是什么?

7
在F#中,如果我写下以下代码:
let p = printfn "something"
它将一次性地评估表达式。对p的任何后续引用都将评估为单位。 从理论上讲,函数的定义是合理的。函数应该只针对相同的输入返回相同的结果。 但是如果我想要出现副作用(即输出到屏幕),那么我需要向p传递一个参数。通常这个参数是unit值。
let p () = printfn "something"

但是为什么 F# 会在每次应用函数时都评估函数,即使参数每次都相同?难道不应该像第一个情况一样应用相同的推理吗?函数的输入 p 没有改变,因此没有必要评估它超过一次。


2
我不懂F#,但第一个看起来像是你让p成为printfn“something”立即返回的值,而第二个看起来像是你定义了一个可以被调用的函数p。 - Marcus Müller
@MarcusMüller 是的,没错。也许就是这样,我想太多了。 - sashang
2
它的工作方式是为了允许效果而特别设计的。然而,并非所有的函数式语言都是这样的。例如,Haskell和PureScript被称为“纯函数式”,在这种情况下,效果被实现为实际值,而不是作为调用函数的结果发生。 - Fyodor Soikin
1
@sashang 如果是这样的话,那就是这样了。正如Fyodor所说,这只是这种语言的设计方式。 - Marcus Müller
2个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
14
printfn不是一个严格意义上的函数,特别地,它也不是一个纯函数,因为它不是引用透明的。这是因为F#不是一个严格的函数式语言(而是一个“以函数为主”的语言),它没有明确区分纯函数和非纯操作。 printfn "something"的返回值是()unit),这意味着p被绑定到了()的值。打印在屏幕上的something是对表达式求值的副作用。 F#是一种急切求值的语言。这就是为什么当把printfn "something"绑定到p时,你会看到something作为求值的副作用打印在屏幕上。一旦表达式被求值,p只被绑定到() - 这个值。 F#不会记忆函数调用,所以当你将p更改为函数时,每次使用()调用函数时它都会被重新计算。由于所有的函数都可以是非纯的,编译器无法确定是否适合进行记忆化,因此不会这样做。

其他编程语言采用不同的方式实现这一点。例如,Haskell语言采用惰性求值,并明确区分纯函数和不纯操作,从而能够在这些情况下应用不同的优化策略。


2
对评论中给出的答案进行进一步解释,第一个 p 是一个不可变值,而第二个 p 是一个函数。如果您多次引用不可变值,则其值随时间不会改变(显然)。但是,如果您多次调用函数,则每次都会执行,即使每次参数都相同。 请注意,即使对于纯函数式语言(如 Haskell),这也是正确的。如果要避免此执行成本,则可以使用特定技术称为 memoization 返回缓存结果,当再次出现相同输入时。但是,记忆化有自己的成本,我不知道任何主流函数式语言都自动将所有函数调用进行记忆化。

4
就Haskell而言,严格来说并不完全正确。因为Haskell是纯函数式的,编译器可以自由地在多个位置重用子表达式的值,并且在某些情况下会这样做(当它确定这样做会导致更好的性能时)。因此,在Haskell中,函数可能会在每次调用时执行,也可能不会执行。然而,在F#中,编译器无法合理地这样做,因为每个函数都可能执行效果,所以它必须在每次调用时诚实地执行每个函数。 - Fyodor Soikin
@FyodorSoikin 那么在 Haskell 中,是否可以说函数并不总是被评估/执行,而是否这样做取决于传递给函数的参数的性质? - sashang
更多或更少,是的。但如果你这样说,另一个原因也是正确的:Haskell使用正常的求值顺序,因此即使只引用一次函数,它也可能永远不会被评估。 - Fyodor Soikin

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