我是Haskell的新手,之前使用过Python和Java进行编程。当我在调试代码时,我有一个习惯,在代码中间放置很多打印语句print
。但在Haskell中这样做会改变语义,我必须将函数签名更改为带有IO
的类型。Haskeller们如何处理这个问题?我可能忽略了一些明显的东西,请给予启发。
其他答案提供了官方文档和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
或其他签名来使用它。它通过欺骗实现这一点,如果你感兴趣,可以在上面链接的文档中进一步了解它的原理。trace
时请考虑编译器。例如,将未使用的值用 trace
包装,或在 where
块中为变量赋值,对于编译器来说可以简单地将变量替换为输出表达式,或者将 trace
分配给未使用的值...所有这些都将导致 trace
被忽略。根据文档所述: 由于惰性评估,您必须牢记,只有在需要包装的值被要求时才会打印您的跟踪信息。 - Mewdo
块内实现这个?我已经尝试了 let x = trace "hello"
,x <- trace "hello"
和 trace "hello"
,但它们都是语法错误。 - Richardtrace :: String -> a -> a
。因此,它需要两个参数,无论是在还是不在do
块中。所以像do ( trace "Hello" 1 )
这样的行将起作用,其中1是一个虚拟参数,假设您没有使用返回值。话虽如此,如果您由于需要访问IO而处于do块中,则可以直接使用putStr
,例如,do ( putStr "Hello" )
。 - Adam Burketrace
函数中所说:“该函数不具有参考透明性:其类型表明它是一个纯函数,但它具有输出跟踪信息的副作用。” 它的确切实现方式并不清楚。GHC源代码已经被链接,你可以在那里看到trace
调用unsafePerformIO
,它的类型签名为IO a -> a
,并被描述为“进入IO单子的后门”。 - Adam BurkeDebug.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
我非常喜欢唐关于这个的简短博客: 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
^^
这可能是对这个问题的一个非正统回答,但是让我来说清楚:
我有一个习惯,在代码中间放置打印语句。
这是一个非常糟糕的习惯。很多编程学生在学习如何进行心理代码漫步时会养成这种习惯(当然我自己也养成了这种习惯)。这种习惯会一直伴随着学生,并且很难改掉。在代码中放置并随后删除打印语句以确保代码正确性并使您了解正在发生的事情是浪费时间的。
在任何编程语言中,您不应该用打印语句来污染您的代码。相反,您应该编写测试用例来代替。对于Haskell,您应该学习编写doctest或使用hspec编写单元测试。使用测试来帮助您了解哪些功能有效,哪些无效,并编写测试以确保函数的正确性。
为了理解Haskell代码,请使用GHCI
。使用交互式解释器与函数进行交互。加载您正在使用的模块,并使用GHCI进行交互式“检查”。
当然,有时候需要调试。所有其他优秀的答案都会给您一些关于如何在Haskell中进行调试的见解。但是,在我个人的经验中,Haskell中的调试是一位严厉的女士,最好避免使用。除非您真的非常需要。
trace
:trace (show state) (return ())
或trace "hello!" (return ())
。 - Richard