【问题标题】:`trace` function in ghci does not print on second invocationghci 中的“trace”函数在第二次调用时不打印
【发布时间】:2020-04-10 08:49:06
【问题描述】:

问题:

trace 函数在第二次调用时不起作用,但这只有在包含 trace 的函数被加载到 ghci 时才会发生。

问题

  1. 为什么会这样?
  2. 如何在 Haskel 模块中加载并仍然具有预期的行为?

意外行为:

我有一个名为test.hs的文件

import Debug.Trace (trace)
main = trace "test" return "main is called"

然后我在 ghci 中输入以下内容。注意maintrace 的第二次调用没有打印“test”

Prelude> :l test
[1 of 1] Compiling Main             ( test.hs, interpreted )
Ok, one module loaded.
*Main> main
test
"main is called"
*Main> main      -- No output from trace?
"main is called"

预期行为

但是,如果我在 ghci 中键入 main 函数,我会得到预期的行为

Prelude> import Debug.Trace (trace)
Prelude Debug.Trace> main = trace "test" return "main is called" 
Prelude Debug.Trace> main
test
"main is called"
Prelude Debug.Trace> main -- Output from trace
test
"main is called"
Prelude Debug.Trace> 

更新

@Robin Zigmond 建议,我尝试了不同的括号但没有成功

main = trace "test" (return "main is called")
main = return(trace "test" "main is called") -- Joins the outputs into one string

【问题讨论】:

  • 我不认为我完全理解这种行为 - trace 是一件棘手的事情,因为它基本上打破了 Haskell 的正常规则,以便为您提供调试工具。但我想知道如果你将我想要的表达式括起来,行为是否会改变,如main = trace "test" (return "main is called")。或者可能是main = return (trace "test" "main is called")。您的版本实际上被解析为 main = (trace "test" return) "main is called" 所以我怀疑函数 trace "test" return 只需要评估一次。
  • 我尝试了你的建议,但没有成功。

标签: haskell ghci


【解决方案1】:

首先,大致了解 Haskell 如何评估事物。考虑这个表达式

(\x -> x * x) (2 + 2)

Haskell 是懒惰的;函数在调用之前不会评估它们的参数,所以这样做的一种天真的方法是:

(2 + 2) * (2 + 2)

但这会使工作加倍!

所做的事情是这样的:

    *
   /  \
   \  /
   (2+2)

也就是说,运行时会记住 2 + 2 来自一个地方,当它被计算时,结果会在表达式的其他部分重用:

    *
   /  \
   \  /
     4

然后

     16

trace 函数包装了一个表达式,并且仅在表达式“弹出”时第一次打印消息。如果再次请求结果,它不会再次打印消息:

ghci> (\x -> x * x) (trace "pop!" (2 + 2))
pop!
16

谜题的下一部分是IO 操作。在 Haskell 中,像 IO 动作这样的语句式的东西实际上是像其他任何东西一样的值。它们可以作为参数传递给函数,也可以从函数返回。只是运行时将它们解释为对在现实世界中执行的有效操作的描述。

因此,类型为IO something 的表达式必须在执行(在“与世界交互”的意义上)之前进行求值(纯粹的懒惰意义上) )。例如:

(\x -> x >> x) (trace "test" (putStrLn "foo"))

变成

        (>>)
       /  \
       \  /
(trace "test" (putStrLn "foo"))

这里我们弹出表达式并打印一次“test”

        (>>)
       /  \
       \  /
    putStrLn "foo"

运行时会看到类似

putStrLn "foo", then putStrLn "foo" again

其实你写的有点不一样:(trace "test" return) "main is called"。在您的代码中,trace 包装了 return 函数,而不是生成的 IO String 值。但是效果是类似的,函数也是 Haskell 中的值。类型为函数的表达式必须在调用函数之前求值。

此外,在您的情况下,发生的情况是 main 操作被评估一次,并执行多次。

请注意trace 的效果(在控制台上打印内容)与IO 操作的正常流程是带外的。它们是根据惰性评估的变幻莫测发生的。这是一个调试功能,不应该用于“真正的”工作。


更新:我忘了回答这部分问题:为什么trace 在你的 ghci 实验中会有你所期望的行为?

trace "test" (return "foo") 是一个多态值,它实际上并没有指定具体的 monad(试试:t return "foo")。 ghci 的defaulting rules 在执行之前将其实例化为IO。这种多态性使 ghci 每次输入 main 时都会重新计算该值。如果您提供显式类型注释,效果应该会消失,例如 main = (trace "test" (return "foo")) :: IO String

【讨论】:

  • 你是对的。当main 从文件加载时,它的类型为IO(),但是当输入ghci 时,main 的类型为Monad m => m [Char]
  • @ChuanyuanLiu 有一个语言扩展名为MonomorphismRestrictionstackoverflow.com/questions/32496864/… 如果您在 ghci (:set -XMonomorphismRestriction) 中激活它,则不再允许像 let main = return "foo" 这样的多态绑定。默认情况下,在 ghci gitlab.haskell.org/ghc/ghc/issues/3202 中禁用该限制,但禁用它的一个后果是有时会意外重新计算值,例如您的多态 main
猜你喜欢
  • 2015-01-28
  • 2020-01-29
  • 1970-01-01
  • 1970-01-01
  • 2018-08-26
  • 1970-01-01
  • 1970-01-01
  • 2015-10-17
  • 1970-01-01
相关资源
最近更新 更多