2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

これは何?

上記アドベントカレンダーの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つの出力を返すことしかできない。

以下のようにsolveIO対応すれば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を使う
2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?