跳轉到內容

Haskell/除錯

來自華夏公益教科書,開放的書籍,開放的世界

使用 `Debug.Trace` 進行除錯列印

[編輯 | 編輯原始碼]

除錯列印是除錯程式的一種常見方法。在命令式語言中,我們可以簡單地在程式碼中新增列印語句到標準輸出或某些日誌檔案中,以跟蹤除錯資訊(例如,特定變數的值或一些可讀的訊息)。然而,在 Haskell 中,我們只能透過 IO 單子輸出資訊;我們不希望僅僅為了除錯而引入它。

為了解決這個問題,標準庫提供了 Debug.Trace。該模組匯出一個名為 `trace` 的函式,它提供了一種方便的方式在程式的任何地方新增除錯列印語句。例如,這個程式列印傳遞給 `fib` 的所有引數,當它們不等於 0 或 1 時

module Main where
import Debug.Trace

fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = trace ("n: " ++ show n) $ fib (n - 1) + fib (n - 2)

main = putStrLn $ "fib 4: " ++ show (fib 4)

以下是結果輸出

n: 4
n: 3
n: 2
n: 2
fib 4: 3

此外,`trace` 使得能夠跟蹤程式的執行步驟;也就是說,哪個函式首先被呼叫,其次被呼叫,等等。為此,我們註釋我們感興趣的函式部分,如下所示

module Main where
import Debug.Trace

factorial :: Int -> Int
factorial n | n == 0    = trace ("branch 1") 1
            | otherwise = trace ("branch 2") $ n * (factorial $ n - 1)

main = do
    putStrLn $ "factorial 6: " ++ show (factorial 6)

當以這種方式註釋的程式執行時,它將按註釋語句執行的順序列印除錯字串。該輸出可能有助於在缺少語句或類似情況的情況下定位錯誤。

一些額外的建議

[編輯 | 編輯原始碼]

如上所示,`trace` 可用於 IO 單子之外;實際上,它的型別簽名...

trace :: String -> a -> a

...表明它是一個純函式。然而,`trace` 確實 在列印有用資訊時執行了 IO。發生了什麼事?實際上,`trace` 使用了一種技巧來規避 IO 與純 Haskell 之間的分離。這體現在以下免責宣告中,可以在 `trace` 文件 中找到

`trace` 函式應用於除錯或監視執行。該函式不是引用透明的:它的型別表明它是一個純函式,但它具有輸出跟蹤訊息的副作用。

使用 `trace` 的一個常見錯誤:在嘗試將除錯跟蹤融入現有函式時,人們不小心將要被 `trace` 列印的值包含在要列印的訊息中;例如,不要這樣做

let foo = trace ("foo = " ++ show foo) $ bar
in  baz

這會導致無限遞迴,因為跟蹤訊息將在 bar 表示式之前被評估,這將導致 foo 按照跟蹤訊息和 bar 的順序進行評估,而跟蹤訊息將在 bar 之前被評估,如此無限迴圈。不要使用 `show foo`,而應在跟蹤訊息中使用 `show bar`

let foo = trace ("foo = " ++ show bar) $ bar
in  baz

有用的習慣用法

[編輯 | 編輯原始碼]

包含 `show` 的輔助函式可能很方便

traceThis :: (Show a) => a -> a
traceThis x = trace (show x) x

類似地,`Debug.Trace` 定義了一個 `traceShow` 函式,它“列印”它的第一個引數並評估為第二個引數

traceShow :: (Show a) => a -> b -> b
traceShow = trace . show

最後,像這樣的 `debug` 函式也可能有用

debug = flip trace

這將允許您編寫類似的程式碼...

main = (1 + 2) `debug` "adding"

...使除錯語句的註釋/取消註釋變得更容易。

使用 GHCi 進行增量開發

[編輯 | 編輯原始碼]

使用 Hat 進行除錯

[編輯 | 編輯原始碼]

一般技巧

[編輯 | 編輯原始碼]
華夏公益教科書