为了披露,我对 Haskell 相当陌生,正在弄清楚语法。让我用一个例子来说明我的问题。假设您有一个函数,它返回多个值的元组或具有多个字段的数据类型。
let
tuple = functionWhichReturnsTuple
value1 = fst tuple
value2 = snd tuple
in
otherFunction value1 value2
一旦执行“in”块中的行,“functionWhichReturnsTuple”是否会被调用两次,一次用于 value1,一次用于 value2?如果是这样,在不启动多个函数调用的情况下一次提取多个值的更好方法是什么?
这确实是一个微妙的点。这里要记住的关键事实是:
从结果(即语义)的角度来看,
functionWhichReturnsTuple
被调用多少次并不重要:即使functionWhichReturnsTuple
被调用两次,它也总是返回相同的结果,因为Haskell是引用透明的。这是纯函数式编程的主要特征:当使用相同的参数调用函数时,函数总是返回相同的结果。
从性能的角度来看,它确实很重要。多次调用函数意味着重新计算相同的结果,因此会显着减慢程序速度。如果这种情况发生在递归函数的每一步,复杂性可能会从线性增加到指数。
现在,编译器尝试优化性能,因此它会尽可能避免重新计算结果。除了一些晦涩的情况之外,它只会计算定义
x = ....
一次。让我们考虑一下您的代码:
tuple = functionWhichReturnsTuple
value1 = fst tuple
value2 = snd tuple
在 Haskell 中,当需要一个值时,计算是从外部触发的:这可能是因为模式匹配,因为一些严格的函数(如
+
上的 Int
),因为我们要在 上打印一个值屏幕等
假设需要 value1
。然后,GHC 计算 fst tuple
,这又会要求计算 tuple
,强制对 functionWhichReturnsTuple
进行评估。当返回 (2,73)
时,GHC 会有效地重写定义来存储结果:
tuple = (2, 73)
value1 = 3
value2 = snd tuple -- still to be evaluated
这样,如果稍后还需要
value2
,我们可以访问已经计算出的 tuple
并得到 73
。无需重新计算函数。
(挑剔:上面我忽略了这样一个事实:由于懒惰,元组可能只评估一个组件。但这与这个问题并不真正相关。)
最后,上面我提到了“几个不起眼的案例”。在某些情况下,优化器不能仅调用该函数一次。当函数是多态(“通用”)时,可能会发生这种情况。考虑这段代码:
dup :: a -> (a,a)
dup x = (x,x)
tuple :: forall a . Num a => (a,a)
tuple = dup (1+2)
value1 :: forall a . Num a => a
value1 = fst tuple
value2 :: forall a . Num a => a
value2 = snd tuple
在这里,类型是多态的(
forall a . ...
),并且完全有可能稍后我们为value1
和value2
选择两种不同的类型!
例如在
print (value1 :: Int, value2 :: Double)
。
在这种情况下,我们必须在
(1+2)
和 Int
上计算 Double
。优化无法阻止这种情况。 dup
也必须调用两次。
这里的问题是,多态值可能看起来像普通值,但实际上是伪装的函数。上面的
tuple
就像一个函数,期望传递类型a
,并返回该类型内的元组。上面我们看到 tuple
的定义被重写,但这只是因为它是一个非函数值而发生:GHC 不会尝试重写函数的定义,因为下次可能会使用不同的参数调用它们。
由于多态值是伪装的函数,因此它们不会被重写,并且在每次使用时都会重新计算。
最后,请注意 Haskell 定义并不强制要求重新计算发生或不发生。 GHC 优化器可以完全自由地执行任何操作,因此我们上面讨论的内容并不完全是一成不变的。通常,优化器会做得很好。它总是避免重新计算非多态值。在某些情况下,它甚至可能避免重新计算多态,但我们不应该过度依赖它。