これは何?
上記アドベントカレンダーの9日目の記事です。
本記事では、Haskellの純粋関数でデバックを楽にする方法をいろいろ試行錯誤した結果をまとめます。
はじめに: 純粋関数はprintデバックができない
以下のようなプログラムがあるとする。
-- リストから偶数だけを取り出して合計を出す。
solve :: [Int] -> Int
solve xs =
let evenXs = filter (even) xs -- 確認したい場所。
in sum evenXs
main :: IO ()
main = do
let result = solve [1, 2, 3, 4, 5]
print result
純粋関数出ない場合には、print evenXsすれば良い話なのだが、Haskellのような純粋関数型言語では副作用がなく、入力に対して1つの出力を返すことしかできない。
以下のようにsolveをIO対応すればsolve内からprintは実行できなくはない。
-- リストから偶数だけを取り出して合計を出す。
-- solve :: [Int] -> Int
-- solveをIO対応してデバックしたい値を出力する処理を戻り値にした例
solve :: [Int] -> IO ()
solve xs = do
let evenXs = filter even xs
print evenXs
-- in sum evenXs -- 戻り値がIOになっているので後続の処理はコメントアウトするしかない
main :: IO ()
main = do
solve [1, 2, 3, 4, 5]
-- let result = solve [1, 2, 3, 4, 5]
-- print result
だが、これをやるくらいならデバックしたい値を返すように変更するほうがマシかも?
-- リストから偶数だけを取り出して合計を出す。
-- solve :: [Int] -> Int
-- solveの戻り値をデバックしたい値にした例
solve :: [Int] -> [Int]
solve xs =
let evenXs = filter (even) xs
in evenXs
-- in sum evenXs -- 戻り値をeventXsにしたので後続の値はコメントアウト
main :: IO ()
main = do
let result = solve [1, 2, 3, 4, 5]
print result
デバック方針1: ghciを有効活用する
Haskellにはghciという対話環境があるので、簡単なデバックなどに活用できる。
$ ghci
ghci> filter (even) [1,2,3,4,5]
[2,4]
短い式のデバックであれば、ghciで事足りるが、
デバックしたい部分に変数が増えてくると辛くなってくる。
-- リストから偶数だけを取り出して冒頭nの合計を出す。
solve :: [Int] -> Int -> Int
solve xs n =
let evenXs = take n $ filter (even) xs
in sum evenXs
main :: IO ()
main = do
let result = solve [1, 2, 3, 4, 5] 3
print result
ghci> take 3 $ filter (even) [1,2,3,4,5]
[2,4]
デバック方針2: 処理を細かく分割してみる
シンプルな例を使っているため、分割する必要性を感じないかもしれないが以下を分割してみる。
-- 再掲
-- リストから偶数だけを取り出して冒頭nの合計を出す。
solve :: [Int] -> Int -> Int
solve xs n =
let evenXs = take n $ filter (even) xs
in sum evenXs
main :: IO ()
main = do
let result = solve [1, 2, 3, 4, 5] n
print result
-- リストから偶数だけを取り出して冒頭nの合計を出す。
solve :: [Int] -> Int -> Int
solve xs n =
let evenXs = filter (even) xs
takeXs = take n evenXs
in sum takeXs
main :: IO ()
main = do
let result = solve [1, 2, 3, 4, 5] 3
print result
このように分割することで、確認したい部分が小さくなり、デバックが捗るようになる。
この例ではletを使って式を分けているが、関数を分けてしまうのも一つの手だと思う。
デバック方針3: Debug.Traceを使う
以下にきれいにまとまっているが、ざっくりいうと標準入力ではなく、標準エラー出力にデバック出力する機能である。
詳しく知りたい方は以下を参照すると良い。
import Control.Arrow ((>>>))
import Data.List (mapAccumL)
import Data.Set qualified as S
import Debug.Trace
-- リストから偶数だけを取り出して冒頭nの合計を出す。
solve :: [Int] -> Int -> Int
solve xs n =
let evenXs = filter (even) xs
takeXs = traceShow evenXs take n evenXs -- デバック出力
in sum takeXs
main :: IO ()
main = do
let result = solve [1, 2, 3, 4, 5] 3
print result
# 標準エラー出力のみ表示
runghc test.hs 2>&1 >/dev/null
[2,4]
# 標準出力のみ表示
runghc test.hs 2>/dev/null
6
# 両方表示
runghc test.hs
[2,4]
6
Debug.Traceのうれしいところとしては、提出時にわざわざソースコードを編集する必要がないところだ(競技プログラミングのジャッチに影響するのは標準出力のみである場合)。
また、以下のブログで紹介されている環境変数を使ってローカルのみデバック出力がされるようにする設定と、dgbId関数を定義しておくと便利だと感じた。
- 入力によっては、デバックにより出力される標準出力にかかる計算コストが高くつく場合があるので提出時にはデバック出力をオフにしたい
-
dbgId関数を使うことで、デバックが書きやすくなる- 素の
traceShowはデバックしたい変数が使用される場所に配置する必要がある
let evenXs = filter (even) xs takeXs = traceShow evenXs take n evenXs -- 使用する場所にtraceShowを書く。-
dbgIdは変数の定義場所にかける
let evenXs = dbgId (filter even xs) - 素の
{-# LANGUAGE CPP #-}
import Control.Arrow ((>>>))
import Data.List (mapAccumL)
import Data.Set qualified as S
import Debug.Trace
#ifdef ATCODER
debug :: Bool ; debug = False
#else
debug :: Bool ; debug = True
#endif
-- 値を表示しつつ、その値を使う場合
dbgId :: (Show a) => a -> a
dbgId x
| debug = let !_ = traceShow x () in x
| otherwise = x
-- リストから偶数だけを取り出して冒頭nの合計を出す。
solve :: [Int] -> Int -> Int
solve xs n =
-- let evenXs = filter (even) xs
-- takeXs = traceShow evenXs take n evenXs
let evenXs = dbgId (filter even xs)
takeXs = take n evenXs
in sum takeXs
main :: IO ()
main = do
let result = solve [1, 2, 3, 4, 5] 3
print result
Haskellは遅延評価型の言語であるため、値を定義するだけでは、評価されない。
値が使用される際にtraceShowを通り、標準エラー出力に出力される。
dbgIdの場合は引数を関数の戻り値にするため、ここで値が使用されたと判定され、traceShowを通せる。
追記: ちなみに、traceShowIdを使っても定義場所にデバックをかける
traceShowIdを使えば、dgbIdと同じように値の定義位置における
let evenXs = traceShowId (filter even xs)
-- 参考: dbgIdをtraceShowIdを使って書き直す
-- traceShowIdを使うバージョン
dbgId :: (Show a) => a -> a
dbgId x
| debug = let !_ = traceShow x () in x
| otherwise = x
-- `traceShowId`を使うバージョン
dbgId' :: (Show a) => a -> a
dbgId' x
| debug = traceShowId x
| otherwise = x
まとめ
- 純粋関数で競技プログラミングをしていると
printデバックができないというやりづらさがある - いろいろデバックするための手段はある
-
ghciを使う。 - 処理をわける。関数をわける
-
Debug.Traceを使う
-