两次函数调用但只显示一条轨迹

Two function calls but only one trace displayed

使用 GHC 8.0.2 版以下程序:

import Debug.Trace

f=trace("f was called")$(+1)

main = do
    print $ f 1
    print $ f 2

输出:

f was called
2
3

这是预期的行为吗?如果是,为什么?我希望字符串 f was called 被打印两次,一次在 2 之前,一次在 3.

之前

TIO 上的相同结果:Try it online!

编辑 但是这个程序:

import Debug.Trace

f n=trace("f was called:"++show n)$n+1

main = do
    print $ f 1
    print $ f 2

输出:

f was called:1
2
f was called:2
3

Try it online!

我怀疑这些行为与懒惰有关,但我的问题仍然存在:这是预期的行为吗?如果是,为什么?

黑客断言:

The trace function outputs the trace message given as its first argument, before returning the second argument as its result.

我在第一个示例中没有看到它。

编辑 2 基于@amalloy 评论的第三个例子:

import Debug.Trace

f n=trace "f was called"$n+1

main = do
    print $ f 1
    print $ f 2

输出:

f was called
2
f was called
3

您的跟踪在定义 f 时打印,而不是在调用它时打印。如果您希望跟踪作为调用的一部分发生,您应该确保在收到参数之前不会对其进行评估:

f x = trace "f was called" $ x + 1

此外,当我 运行 你的 TIO 时,我根本看不到痕迹出现。 trace 并不是真正可靠的打印方式,因为它欺骗了构建该语言的 IO 模型。评估顺序中最细微的变化都会扰乱它。当然,对于调试,您可以使用它,但即使是这个简单的示例,也不能保证有多大帮助。

在您的编辑中,您引用了 trace 的文档:

The trace function outputs the trace message given as its first argument, before returning the second argument as its result.

事实上,这正是您程序中发生的事情!当定义f,

trace "f was called" $ (+ 1)

需要评估。首先,打印 "f was called" 。然后,trace 的计算结果为 returns、(+ 1)。这是 trace 表达式的最终值,因此 (+ 1) 就是 f 的定义。 trace 消失了,看到了吗?

确实是懒惰的结果

懒惰意味着仅仅定义一个值并不意味着它会被评估;只有在某些事情需要它时才会发生。如果不需要,则实际生成它的代码不会 "do anything"。如果需要特定值 ,则代码为 运行,但仅在第一次需要;如果有其他引用相同的值并再次使用,这些使用将直接使用第一次产生的值。

您必须记住,函数在任何意义上都是值;适用于普通值的一切也适用于函数。因此,您对 f 的定义只是为一个值编写一个表达式,表达式的求值将被推迟到实际需要 f 的值时,并且由于它需要表达式计算的值(函数)的两倍将被保存并再次使用。

让我们更详细地看一下:

f=trace("f was called")$(+1)

您正在使用一个简单的等式定义一个值 f(不使用任何语法糖来在等式左侧编写参数,或通过多个等式提供案例)。所以我们可以简单地将右侧作为定义值 f 的单个表达式。只是定义它什么都不做,它会一直坐在那里直到你调用:

print $ f 1

现在 print 需要计算它的参数,所以这会强制表达式 f 1。但是我们不能在不先强制 f 的情况下将 f 应用于 1。所以我们需要弄清楚表达式 trace "f was called" $ (+1) 的计算结果是什么函数。所以 trace 实际上被调用,它的不安全 IO 打印和 f was called 出现在终端,然后 trace returns 它的第二个参数:(+1).

所以现在我们知道函数 f 是什么:(+1)f 现在将直接引用该函数,如果再次调用 f 则无需评估原始代码 trace("f was called")$(+1)。这就是为什么第二个 print 什么都不做。

这个案例很不一样,尽管它看起来很相似:

f n=trace("f was called:"++show n)$n+1

这里我们通过在左侧编写参数来使用语法糖来定义函数。让我们将其脱糖为 lambda 表示法,以更清楚地了解绑定到 f 的实际值是什么:

f = \n -> trace ("f was called:" ++ show n) $ n + 1

这里我们直接写了一个函数值,而不是一个可以通过计算得出函数结果的表达式。因此,当 f1 上被调用之前需要求值时,f 的值就是整个函数; trace 调用是 inside 函数,而不是在函数中调用 result 的东西。所以 trace 不是作为评估 f 的一部分调用的,而是作为评估应用程序 f 1 的一部分调用的。如果您保存了结果(比如 let x = f 1),然后多次打印,您只会看到一条痕迹。但是当我们计算 f 2 时,trace 调用仍然存在于函数内部,即 f 的值,因此当再次调用 ftrace.