あどけない話

Internet technologies

HaskellとテストとBDD

Haskellでの BDD を実践するとどうなるかを考えるためのメモ。

豊かなデータ型とセクシーな型システムを持つ Haskell では、型が以下のような意味を持つ

  • 仕様
  • 保守性の向上
  • 簡単なドキュメント
  • 設計図

BDD では、テストの用語ではなく設計の用語を使ってテストを記述する。だから Haskell で、まず型を書く習慣があれば、ある意味 BDD を実践していると言える。この感覚は、他の言語のプログラマには分からないかもしれない。

fromList :: Ord a => [a] -> Set a
fromList = undefined

このコードはコンパイルを通過するので、型に関する誤りがないことを確かめられる。

僕はへなちょこなので、型を先に書くこともあれば、後から書くこともある。

  • 単純なコードはさっさと実装したい
    • 型は GHC に推測させて、ghc-mod で自動挿入する
  • 難しいコードは型を書いてよく考える
    • 型のレベルで設計する。そうすれば型が実装を導いてくれる

型に関する仕様が書けたら、次は値に関する仕様を書くべきだ。依存型とかを使えば、それ以上のことができるという意見もあるだろうけど、とっつきにくいので今は考えない。

Haskell には、Rubyrspec をまねた hspec があり一部の人は絶賛している。rspecチュートリアルを読んでみて、気持ちはわかったのだけれど、僕にはピンとこなかった。

結局 rspec は、仕様書だと思ってテストコードをコードとは別のファイルに書く訳で、それがドキュメントに反映されない。(反映するツールはあるのかもしれないけれど。)

それよりも、Python の doctest の方が、僕としてはいいと思う。Haskell には Python をまねた doctestがある。

{-| Creating a set from a list. O(N log N)

>>> empty == fromList []
True
>>> singleton 'a' == fromList ['a']
True
>>> fromList [5,3,5] == fromList [5,3]
True
-}

fromList :: Ord a => [a] -> Set a
fromList = undefined

このように対話的な使用例を書く。この書式には haddock が対応しているので、doctest は haddock から使用例のデータをもらって、対話的に実行し検査する。BBD 風にするには、それぞれの使用例にもう少し説明を加えるべきかもしれないと思う一方で、これで十分な気もする。

でも、まだいろいろ不満がある。

  • test-framework と統合されていない
  • 対話的な例ではなく、等式だけを書きたい
  • QuickCheck のプロパティが書けない

test-framework

test-framework のドライバは、HUnit 用、QuickCheck2 用、doctest 用が用意されている。test-framework-th を使えば、HUnit のユニットテストと QuickCheck2 のプロパティを書くのが簡単になる。それぞれ、"case_" と "prop_" で始まる関数を書けばいい。

module Main where

import Test.Framework.TH
import Test.Framework.Providers.HUnit
import Test.Framework.Providers.QuickCheck2
import Test.QuickCheck2
import Test.HUnit

import Data.MySet

main :: IO ()
main = $(defaultMainGenerator)

prop_toList :: [Int] -> Bool
prop_toList xs = ordered ys
  where
    ys = toList . fromList $ xs
    ordered (x:y:xys) = x <= y && ordered (y:xys)
    ordered _         = True

case_ticket4242 :: Assertion
case_ticket4242 = (valid $ deleteMin $ deleteMin $ fromList [0,2,5,1,6,4,8,9,7,11,10,3]) @?= True

defaultMain に与える Test のリストが Template Haskell で自動生成され、すごく便利だ。しかし、doctest が統合されていないので、test-framework-th-prime を作成してみた。手伝ってくれた @mr_konn さん、ありがとう!

module Main where

import Test.Framework.TH.Prime
import Test.Framework.Providers.DocTest
import Test.Framework.Providers.HUnit
import Test.Framework.Providers.QuickCheck2
import Test.QuickCheck2
import Test.HUnit

import Data.MySet

main :: IO ()
main = $(defaultMainGenerator)

doc_test :: DocTests
doc_test = docTest ["../Data/MySet.hs"] ["-i.."]

prop_toList :: [Int] -> Bool
prop_toList xs = ordered . toList . fromList $ xs
  where
    ordered (x:y:xys) = x <= y && ordered (y:xys)
    ordered _         = True

case_ticket4242 :: Assertion
case_ticket4242 = (valid $ deleteMin $ deleteMin $ fromList [0,2,5,1,6,4,8,9,7,11,10,3]) @?= True

なお、Mac 上の GHC 7.0 でこのコードを実行するにはコンパイルする必要がある。runghc などで動かすと segfault する。GHC 7.4 では直るようだ(未確認)。

等式

対話的な例よりも、等式の方がしっくりくる場合がある。

{-| Creating a set from a list. O(N log N)

>>> empty == fromList []
>>> singleton 'a' == fromList ['a']
>>> fromList [5,3,5] == fromList [5,3]
-}

fromList :: Ord a => [a] -> Set a
fromList = undefined

こうなると、こういう具体例だけではなく、QuickCheck のプロパティ、つまり関数の持つ性質を記述したくなる

QuickCheck

Simon M さんは、すでに haddock に対し、"prop>"という新しい書式を導入することに賛成しているそうだ。加えて "unit> という新しい書式を導入し対話的な例と区別することにすると、こんな感じになるだろうか?

{-| Creating a set from a list. O(N log N)

prop> empty == fromList []

unit> singleton 'a' == fromList ['a']
prop> singleton (x :: Char) == fromList [x]

unit> fromList [5,3,5] == fromList [5,3]
prop> fromList (xs :: [Int]) == fromList (uniq xs)

prop> ordered . toList . fromList $ (xs :: [Int])
-}

fromList :: Ord a => [a] -> Set a
fromList = undefined

test-framework のテストコードに書いているコードの多くはドキュメントに移動することになる。こんな環境が整ったら、テストコードには以下を記述することになるだろう。

考察

"prop>" と "unit>" は統合できるのか否か? 乱数的な変数のないプロパティを QuickCheck に与えると、1回しか実行しなくていいのに、100 回実行してしまう問題がある。

プロパティに現れる自由変数はどうする?無名関数を書かせて、閉じたλ式にさせる?

prop> \(xs :: [Int]) -. ordered . toList . fromList $ xs

そもそも、テストコードは test-framework の延長ではなく、hspec を発展させる方がいいのではないかという意見もあるかもしれない。そういう意見の方は、ぜひ僕を納得させてほしい。