跳轉到內容

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 bar 而不是 show foo 作為正確的跟蹤訊息。

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 進行除錯

[編輯 | 編輯原始碼]

一般提示

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