Haskell中简单的调试方法

24

我是Haskell的新手,之前使用过Python和Java进行编程。当我在调试代码时,我有一个习惯,在代码中间放置很多打印语句print。但在Haskell中这样做会改变语义,我必须将函数签名更改为带有IO的类型。Haskeller们如何处理这个问题?我可能忽略了一些明显的东西,请给予启发。


以下是如何在 do 语句中使用 tracetrace (show state) (return ())trace "hello!" (return ()) - Richard
6个回答

12

其他答案提供了官方文档Haskell维基的链接。但如果你看到这个答案,让我们假设你由于某些原因没有从中获得所需的帮助。 Wikibook也有一个使用斐波那契数列的例子,我觉得更容易理解。以下是一个故意简单的例子,希望能够帮助你。

假设我们从这个非常简单的函数开始,出于重要的业务原因,把"bob"添加到字符串中,然后将其反转。

bobreverse x = reverse ("bob" ++ x)

在GHCI中的输出:

> bobreverse "jill"
"llijbob"

我们不认为这个过程会出现问题,但附近可能会有一些问题,因此我们添加了调试。
import Debug.Trace

bobreverse x = trace ("DEBUG: bobreverse" ++ show x) (reverse ("bob" ++ x))

输出:

> bobreverse "jill"
"DEBUG: bobreverse "jill"
llijbob"

我们使用show仅仅是为了确保正确地将x转换为字符串输出。我们还添加了一些括号,以确保参数被正确分组。
总之,trace函数是一个装饰器,它打印第一个参数并返回第二个参数。它看起来像一个纯函数,所以你不需要在函数中引入IO或其他签名来使用它。它通过欺骗实现这一点,如果你感兴趣,可以在上面链接的文档中进一步了解它的原理。

3
非常重要的一点:使用 trace 时请考虑编译器。例如,将未使用的值用 trace 包装,或在 where 块中为变量赋值,对于编译器来说可以简单地将变量替换为输出表达式,或者将 trace 分配给未使用的值...所有这些都将导致 trace 被忽略。根据文档所述: 由于惰性评估,您必须牢记,只有在需要包装的值被要求时才会打印您的跟踪信息。 - Mew
你如何在 do 块内实现这个?我已经尝试了 let x = trace "hello"x <- trace "hello"trace "hello",但它们都是语法错误。 - Richard
1
请注意,trace的签名为trace :: String -> a -> a。因此,它需要两个参数,无论是在还是不在do块中。所以像do ( trace "Hello" 1 )这样的行将起作用,其中1是一个虚拟参数,假设您没有使用返回值。话虽如此,如果您由于需要访问IO而处于do块中,则可以直接使用putStr,例如,do ( putStr "Hello" ) - Adam Burke
没有任何链接可以解释你所说的作弊行为。我已经到处搜索,试图找出在Haskell中可能存在的跟踪方式。我不明白。 - Stefan Octavian
确实,链接的细节使用了“某种肮脏的技巧”,或者如trace函数中所说:“该函数不具有参考透明性:其类型表明它是一个纯函数,但它具有输出跟踪信息的副作用。” 它的确切实现方式并不清楚。GHC源代码已经被链接,你可以在那里看到trace调用unsafePerformIO,它的类型签名为IO a -> a,并被描述为“进入IO单子的后门”。 - Adam Burke
显示剩余2条评论

5

点击这里。你可以使用Debug.Trace.trace代替打印语句进行调试。


5
你如何使用trace真是令人费解,我无法让它正常工作,而且文档真的非常糟糕。 - Alper

4
我成功地创建了一个双重性格的 IO/ST monad 类型类,当一个 monadic 计算被类型化为 IO 时会打印 debug 语句,当它被类型化为 ST 时也是如此。在这里可以看到演示和代码:Haskell -- dual personality IO / ST monad?
当然,Debug.Trace 更像一把瑞士军刀,尤其是在使用有用的特殊情况包装时。
trace2 :: Show a => [Char] -> a -> a
trace2 name x = trace (name ++ ": " ++ show x) x

这可以像这样使用:(trace2 "第一个参数" 3) + 4

编辑

如果您想要源代码位置,您甚至可以使其更加高级。

{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH
import Language.Haskell.TH.Syntax as TH
import Debug.Trace

withLocation :: Q Exp -> Q Exp
withLocation f = do
    let error = locationString =<< location
    appE f error
    where
        locationString :: Loc -> Q Exp
        locationString loc = do
            litE $ stringL $ formatLoc loc

formatLoc :: Loc -> String
formatLoc loc = let file = loc_filename loc
                    (line, col) = loc_start loc
                in concat [file, ":", show line, ":", show col]

trace3' (loc :: String) msg x =
    trace2 ('[' : loc ++ "] " ++ msg) x
trace3 = withLocation [| trace3' |]

然后,在一个与上述定义不同的文件中,您可以编写:
{-# LANGUAGE TemplateHaskell #-}
tr3 x = $trace3 "hello" x

并测试它

> tr3 4
[MyFile.hs:2:9] hello: 4

1
我认为这个答案不适合初学者。 - Alper
@gatoatigrado 你能详细解释一下你的答案吗?假设我有一个递归函数(fun :: a -> a),它有多个模式匹配实现。我想知道执行顺序,按行号排序。我在类型声明后面写了这个(fun arg | $trace3 ("fun : " ++ show arg) False = undefined),它打印出了行号,但是它们都链接到这一行,而不是实现。简而言之,我想要一个简单的启用跟踪调试的一行代码。 - nutella_eater

2

4
如何使用它?文档都没有用处。 - Alper

-1

我非常喜欢唐关于这个的简短博客: https://donsbot.wordpress.com/2007/11/14/no-more-exceptions-debugging-haskell-code-with-ghci/

简而言之:使用ghci,例如一个名为HsColour.hs的代码程序。

 $ ghci HsColour.hs
    *Main> :set -fbreak-on-exception
    *Main> :set args "source.hs"

现在打开跟踪功能并运行你的程序,GHCi会在调用error时停止你的程序:

 *Main> :trace main
    Stopped at (exception thrown)

好的,很好。我们遇到了一个异常...让我们回退一下,看看我们现在在哪里。现在观察我们使用(奇怪,我知道)“:back”命令通过程序向后时间旅行:

  [(exception thrown)] *Main> :back
    Logged breakpoint at Language/Haskell/HsColour/Classify.hs:(19,0)-(31,46)
    _result :: [String]

这告诉我们,在出现错误之前,我们正在文件Language/Haskell/HsColour/Classify.hs的第19行。现在我们状态相当不错。让我们看看具体在哪里:

 [-1: Language/Haskell/HsColour/Classify.hs:(19,0)-(31,46)] *Main> :list
    18  chunk :: String -> [String]
        vv
    19  chunk []    = head []
    20  chunk ('\r':s) = chunk s -- get rid of DOS newline stuff
    21  chunk ('\n':s) = "\n": chunk s
                                       ^^

这个回答不幸地展示了为什么链接式的答案会成为一个问题的好例子 - 他们所引用的页面可能已经发生了改变(在这种情况下: 看来它已经消失了)。 - Frerich Raabe

-4

这可能是对这个问题的一个非正统回答,但是让我来说清楚:

  • 问:如何在Haskell中进行典型的“打印”式调试代码?
  • 答:不要这样做。不要在Haskell中进行“打印”式调试。不要在任何编程语言中这样做!学会如何不这样做!

我有一个习惯,在代码中间放置打印语句。

这是一个非常糟糕的习惯。很多编程学生在学习如何进行心理代码漫步时会养成这种习惯(当然我自己也养成了这种习惯)。这种习惯会一直伴随着学生,并且很难改掉。在代码中放置并随后删除打印语句以确保代码正确性并使您了解正在发生的事情是浪费时间的。

在任何编程语言中,您不应该用打印语句来污染您的代码。相反,您应该编写测试用例来代替。对于Haskell,您应该学习编写doctest或使用hspec编写单元测试。使用测试来帮助您了解哪些功能有效,哪些无效,并编写测试以确保函数的正确性。

为了理解Haskell代码,请使用GHCI。使用交互式解释器与函数进行交互。加载您正在使用的模块,并使用GHCI进行交互式“检查”。

当然,有时候需要调试。所有其他优秀的答案都会给您一些关于如何在Haskell中进行调试的见解。但是,在我个人的经验中,Haskell中的调试是一位严厉的女士,最好避免使用。除非您真的非常需要。


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