简单说一下 Haskell 中的调试技巧

96
aaaron7
2016.03.07 11:09* 字数 1322

入坑差不多一个月,一直就没关注过 Haskell 中调试这一茬,一是没写什么复杂的程序,大多数都是编译错误,调调编译过了就结了。二是手头的几本书,Learn you a haskellReal World Haskell 还有 The craft of functional Programming 都没讲关于 Haskell 调试的技巧。当然,这也很符合 Haskell 比较高冷的特质。虽然都会介绍 putStrLn 这个函数,但这函数必须跟 IO Monad 一起使用,并不方便,如果用的地方多了会非常不美观。

所以一直以来遇到的运行时错误,我都是采用 BVM(Brain Virtual Machine, _)的方式来查找。

当然,BVM 也就适用于简单的程序。最近在写一个简单的 Compiler,在生成 LLVM IR 的时候遇到一个 Bug,对所有 Variable 类型的数据均会失败,返回一个 <badref>, 当尝试去 load 的时候就会报一个 casting error

在我用 BVM 在大脑中把流程演绎的千百万遍依然特喵没有找到这 bug 的时候。我彻底服了,乖乖的去学习了一下 Haskell 的调试技巧。

对于命令式编程,最基本的调试技巧就是在适当的地方插入 print 语句来把一些 context info 打印出来。那在 Haskell 中能否优雅地实现呢?

The answer is YES!

trace

GHC 其实内置了相当多的 debug 方法,具体见这里 ,这里就讲一下最简单但也是最实用的一个方法—— Debug.Trace

使用 trace 很简单,只需要在文件开头 import Debug.Trace 即可。

假设你有这样一个函数:

subStringToIndex :: String -> Int -> String
subStringToIndex s n = take n s 

故名思议,接受一个字符串 s 和一个整数 n ,返回一个 s[0..<n] 的 substring。如果这个函数不 work 。那我们首先会想到的就是要看看传进去的参数是不是正确,毕竟逻辑这么简单,不大可能在函数内部有什么bug。对此,我们只需要简单的写成这样:

subStringToIndex :: String -> Int -> String
subStringToIndex s n = trace ("para s is " ++ show s)  take n s 

运行一下,能看到这样的输出:

para s is "abcde"
ab

是不是有一种重新看到熟悉的 printf 的感觉? 一股浓浓的亲切之风铺面而来。trace 也是函数,使用非常简单,这一点可以从其原型的声明中看出来:

trace :: String -> a -> a

trace 的作用官方文档的话说,他接受一个字符串,以及一个任意类型 a 的参数, 然后打印出这个字符串,并返回 a。

什么鬼?相信我,我第一次看的时候和你有相同的感觉。

用人话来说,String 参数, 就是你要打印的,里面包含了你需要的 context info 的字符串,而 a,一般是一个表达式,或者一个函数, 这里写 a 代表其执行的结果可以是任意类型。trace 会先打印字符串,然后把第二个参数作为结果返回。

所以就程序的执行来说,trace "message : " (f x) 和直接执行 f x 效果是一样的,只是 trace 在执行前就会打印出相关的信息。

调试 do block

事实上,Haskell 程序中大多数比较复杂的逻辑都是以 monadic 的形式来写的。也就是我们熟悉的 do block, 看如下的例子:

monadicSubStringToIndex ::  Int -> State String String
monadicSubStringToIndex n = do
    s1 <- get
    traceM $ "original string is " ++ show s1
    return $ take n s1


main :: IO ()
main = putStrLn $ show $ runState (monadicSubStringToIndex 2) "abcde"

这次我们要操作的字符串不是通过参数传入了,而是 wrap 在 State Monad 里的一个字符串。如果这个时候我们要来 debug monadicSubStringToIndex 该怎么做呢?我们都知道 do block 里的语句块都接受当前的 monad 作为输入,然后也必须输出一个 monad。显然上述的 trace 并不能适用于这里的情况。

像大多数 Haskell 的函数一样,trace 也有一个 monadic 的孪生兄弟—— traceM, 这让一切都变得很简单。

我们只需要简单加一句话:

monadicSubStringToIndex ::  Int -> State String ()
monadicSubStringToIndex n = do
    s <- get
    traceM $ "original string is " ++ show s   <-- 看这里看这里
    modify $ (\s -> take n s)


main :: IO ()
main = putStrLn $ snd $ runState (monadicSubStringToIndex 2) "abcde"

运行程序,输出:

original string is "abcde"
ab

你看,我们成功的像命令式语言一样在可能会弄错的地方打 log 来帮我们 debug。虽然这并不是那么容易理解。

traceM 的定义为:

traceM :: Monad m => String -> m ()

trace 比较,traceM 只有一个参数,就是要打印的字符串,然后返回一个任意类型的 monad。但值得注意的是任何 monadic 的函数其实都是有一个隐藏 的输入参数的,那就是当前的 monad。 所以结构上, tracetraceM 是一致的。从这里也能看到普通的 Haskell 编程是以函数调用为主线,而 monadic 得写法则是以 monad 的传递为主线的。

要注意的一点是,无论是 trace 还是 traceM, 都是有副作用的。使得使用的地方不 pure, 不 referentially transparent。 简单的说他们并不是满足 Haskell 语言特性的 feature,所以仅供调试的时候使用。


想看更多内容? 可以关注我的知乎

FP 与 Compiler
Web note ad 1