関数をデバッグするために、引数と戻り値をそれぞれ表示するというのを誰しもやったことがあると思う。今回はそれを自動化するからくりをHaskellで実装してみる。
目標となるのは、関数が与えられたとき、その引数と結果をターミナルに出力する関数に変換する高階関数、probe :: Traceable a => String -> a -> a
である。
testDelay :: Double -> Double -> IO () testDelay dur dur' = threadDelay $ round $ (dur + dur') * 1000000
*Probe> probe "testDelay" testDelay 1 2 testDelay 1.0 2.0: ()
これは型クラスを活用すればお茶の子さいさいである。以下のように型によって挙動を切り替える関数withTraceData
を定義すればよい。
- 関数
r -> a
は、rの値を文字列にしてリストに積み、aについても再帰的にwithTraceData
を適用する。 IO
アクションからは結果が返されるとみなし、与えられた引数のリストと結果を表示する。
実装するとこのようになる。
module Probe (Traceable(..), probe) where import System.IO class Traceable a where withTraceData :: String -- 名前 -> [String] -- 文字列化した引数 -> a -> a instance (Show r, Traceable a) => Traceable (r -> a) where withTraceData name args cont arg = withTraceData name (showsPrec 11 arg "" : args) -- showsPrecを使うことで適切に括弧を表示する (cont arg) instance Show a => Traceable (IO a) where withTraceData name args m = do hPutStr stderr $ unwords (name : args) ++ ": " hFlush stderr a <- m hPrint stderr a pure a probe :: Traceable a => String -> a -> a probe name = withTraceData name []
このままでは純粋な関数には使えないので、unsafePerformIO
を使ったインスタンスも作っておこう。
newtype Result a = Result { unResult :: a } instance Show a => Traceable (Result a) where withTraceData name args (Result a) = Result $ unsafePerformIO $ withTraceData name args $ pure a
Traceable Int
などを直接定義せず、まずnewtypeへのインスタンスとして定義するのには理由がある。
Haskellの多引数関数はカリー化されており、「a -> b -> c
はb -> c
を結果として返す関数」でもあるため、どこで区切るかがwell-definedではない。そのため、明示的な型によって区別することは理にかなっている。
さらに、他のプリミティブ型の定義をDerivingViaとStandaloneDerivingによって簡潔に書けるという最近のGHCならではの利点もある。
deriving via Result Bool instance Traceable Bool deriving via Result Int instance Traceable Int deriving via Result Double instance Traceable Double ...
小さなコードではあるが、ここで紹介したテクニックは日常で役に立つに違いない。 この手法は色々と応用が利く。例えば、printする代わりにシリアライズして外部のファイルに書き込めば、probe関数を刺して動かすだけでテストケースを抽出できる道具にもなる。
最近やる気が出てきたので、これらのアイデアをそのうちライブラリにまとめるかもしれない。