在 Haskell 中从多个值返回中提取值是否会多次调用该函数?

问题描述 投票:0回答:1

为了披露,我对 Haskell 相当陌生,正在弄清楚语法。让我用一个例子来说明我的问题。假设您有一个函数,它返回多个值的元组或具有多个字段的数据类型。

let
    tuple = functionWhichReturnsTuple
    value1 = fst tuple
    value2 = snd tuple
in
    otherFunction value1 value2

一旦执行“in”块中的行,“functionWhichReturnsTuple”是否会被调用两次,一次用于 value1,一次用于 value2?如果是这样,在不启动多个函数调用的情况下一次提取多个值的更好方法是什么?

haskell return-value function-call
1个回答
0
投票

这确实是一个微妙的点。这里要记住的关键事实是:

  1. 从结果(即语义)的角度来看,

    functionWhichReturnsTuple
    被调用多少次并不重要:即使
    functionWhichReturnsTuple
    被调用两次,它也总是返回相同的结果,因为Haskell是引用透明的。这是纯函数式编程的主要特征:当使用相同的参数调用函数时,函数总是返回相同的结果。

  2. 从性能的角度来看,它确实很重要。多次调用函数意味着重新计算相同的结果,因此会显着减慢程序速度。如果这种情况发生在递归函数的每一步,复杂性可能会从线性增加到指数。

现在,编译器尝试优化性能,因此它会尽可能避免重新计算结果。除了一些晦涩的情况之外,它只会计算定义

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 优化器可以完全自由地执行任何操作,因此我们上面讨论的内容并不完全是一成不变的。通常,优化器会做得很好。它总是避免重新计算非多态值。在某些情况下,它甚至可能避免重新计算多态,但我们不应该过度依赖它。

© www.soinside.com 2019 - 2024. All rights reserved.