You don't have javascript enabled. Good luck! :(

QuickCheck

QuickCheck は凄く面白いので、Haskeller なら使いこなしたいところです。しかしながら、慣れるまでは結構難しいので実例を見ながら使い方を理解していきたいと思います。

パッケージのインストール

HSpec の時と同様に package.yamltestsQuickCheck パッケージを追記します。quickcheck では無いのでスペルミスに注意してください。

QuickCheck パッケージは更新が頻繁に行われているのでバージョンごとに書き方が違う場合があります。今回明示的には指定していませんが、2.9.2 として進めていこうと思います。

QuickCheck に慣れよう!

まずは QuickCheck が生成するランダムな値について理解を深めたいと思います。

この sample 関数を使うことによって、どんな値が生成されるのかデバッグすることができます。型を見る通り Gen a の値を適用すれば良さそうに見えますが、ここが少し変わっているので注意してください。

実際に値をいくつか生成してみます。

ここで重要な点は2つです。

  • arbitrary :: Gen [Int]
  • 生成される値は実行のたびにランダムに変化する

面白いので他にも生成してみます。1行で表示させるために sample' を利用することにします。

このように、生成したい値の型を Gen aa に指定してあげることでランダムな値を生成できることがわかりました。また、多相型についてはエラーになります。

他にも、QuickCheck モジュールではいくつか便利な関数を提供しています。

-- 与えられた範囲でランダムな値を生成する
choose :: Random a => (a, a) -> Gen a
> sample' (choose (1,10))
[1,6,10,1,3,1,9,1,1,10,5]


-- 与えられたリストの中からランダムに値を生成する
elements :: [a] -> Gen a
> sample' (elements ["patek", "omega", "seiko"])
["omega","patek","omega","omega","patek","patek","omega","omega","patek","omega","patek"]


-- 与えられたジェネレータのリストの中からランダムに選択し、それを利用してランダムな値を生成する
> let genHighPriceWatch = elements ["patek", "ap", "vc"]
> let genMiddlePriceWatch = elements ["seiko", "omega", "rolex"]
> sample' (oneof [genHighPriceWatch, genMiddlePriceWatch])
["vc","omega","seiko","patek","patek","rolex","rolex","rolex","seiko","omega","seiko"]


-- 出現頻度を指定してランダムな値を生成する (この例では1/100,99/100の出現確率に設定)
frequency :: [(Int, Gen a)] -> Gen a
> let genHighPriceWatchWithFreq = (1, genHighPriceWatch)
> let genMiddlePriceWatchWithFreq = (99, genMiddlePriceWatch)
> sample' (frequency [genHighPriceWatchWithFreq, genMiddlePriceWatchWithFreq])
["seiko","rolex","seiko","omega","seiko","seiko","omega","rolex","omega","omega","omega"]

-- ランダムに生成された値に対して、与えられた条件満たす値のみを生成する
suchThat :: suchThat :: Gen a -> (a -> Bool) -> Gen a
> sample' ((arbitrary :: Gen Int) `suchThat` even)
[-2,0,-4,2,4,6,-10,-10,-8,4,6]

> sample' ((arbitrary :: Gen Int) `suchThat` (>0))
[3,1,5,1,1,10,3,14,11,10,21]

-- ランダムに生成された値のリストを生成する
> sample' (listOf (arbitrary:: Gen Int))
[[],[1,2],[-1,2,-4,2],[5,-1],[6,-3,6,4,0],[-10,8,-10,-5,-2,0,2,-10],[5,5,9,-7,-8,-8,7,9,-6,11],[],[4,14,13,0,13,5],[0,9],[17,-12,12,-3,-7,-13,-1,-1,-19,10,11,16,2,-20,-5,-4,0,12]]

-- 空リストを除く
> sample' (listOf1 (arbitrary:: Gen Int))
[[0],[-1,-1],[4,1,4,0],[2,0],[1,-1,-7,8,-2,1,2,7],[-2,-6,-6,2],[11,-3,-5,4,1,0,-3,9,-10],[2,2,-9,-14,5,13,-13,11,14,12,-9,12,13],[7,10,5,-12,-4],[8,-16],[3,14,2,-17,3,-18,-4,17,16,-2,8,-11,14,20,-1,-10,2]]

-- 長さを指定してランダムな値のリストを生成する
> sample' (vectorOf 3 (arbitrary:: Gen Int))
[[0,0,0],[2,1,1],[3,-1,-2],[5,-2,-5],[2,1,5],[5,-6,4],[2,-12,8],[4,-2,-1],[2,9,-6],[-11,18,-6],[17,-7,-15]]

-- 与えられたリストをシャッフルしたランダムな値のリストを生成する
> sample' (shuffle [1..5])
[[2,5,1,3,4],[3,4,2,1,5],[1,4,5,2,3],[4,1,2,5,3],[5,2,4,1,3],[3,1,4,2,5],[1,3,5,4,2],[1,4,2,3,5],[5,2,3,1,4],[1,4,5,2,3],[4,3,2,1,5]]

結構たくさんあるので、基本的なテストであればこれらの関数で十分対応可能です。

ここまで具体例をいくつか見てきたので、QuickCheck を使う際には arbitrary に具体的な型を指定してあげれば良さそうだということがわかってきました。また、arbitraryArbitrary 型クラスのメソッドとなっているため、適切にインスタンスを定義してしまえば、自分で定義した型の値をランダムに生成することも可能です。

実際に QuickCheck のテストを書いてみる

Peals の問題は関数に制約をつけていることが多いため、良い練習になりそうです。今回の制約は次の通り

  • 自然数
  • 重複しない

上記を満たすリストが入力値となります。

素朴に思いつく定義

慣習として property のための関数の接頭辞には prop をつけます。つけなくても問題は無いです。

このコードに対してテストを実行すると次のエラーが表示されます。

Registering PFAD-0.1.0.0...
PFAD-0.1.0.0: test (suite: PFAD-test)


Minfree
  minfree
    書籍の例
  minfree'
    書籍の例
  minfree == minfree'
    minfree == minfree' FAILED [1]

Failures:

  test/MinfreeSpec.hs:17:
  1) Minfree, minfree == minfree', minfree == minfree'
       Falsifiable (after 4 tests and 2 shrinks):
       [-1]

Randomized with seed 1211983148

Finished in 0.0007 seconds
3 examples, 1 failure

PFAD-0.1.0.0: Test suite PFAD-test failed
Completed 2 action(s).
Test suite failure for package PFAD-0.1.0.0
    PFAD-test:  exited with: ExitFailure 1
Logs printed to console

エラーメッセージから、どうやらランダムに生成された値に [-1] が含まれてたようです。まずはこれを改良してみます。

自然数に限定する

やり方は色々とあると思いますが、今回は Positive 型を利用することにします。

この結果、また別のエラーが出るようになりました。

Registering PFAD-0.1.0.0...
PFAD-0.1.0.0: test (suite: PFAD-test)


Minfree
  minfree
    書籍の例
  minfree'
    書籍の例
  minfree == minfree'
    minfree == minfree' FAILED [1]

Failures:

  test/MinfreeSpec.hs:17:
  1) Minfree, minfree == minfree', minfree == minfree'
       Falsifiable (after 4 tests and 1 shrink):
       [Positive {getPositive = 1},Positive {getPositive = 1}]

Randomized with seed 398651692

Finished in 0.0028 seconds
3 examples, 1 failure

PFAD-0.1.0.0: Test suite PFAD-test failed
Completed 2 action(s).
Test suite failure for package PFAD-0.1.0.0
    PFAD-test:  exited with: ExitFailure 1
Logs printed to console

今回のエラーでは [1,1] のような重複した値の場合にテストが失敗しています。これも修正しましょう。

重複をなくす

(==>) を使って minfree に適用する前に事前条件を設定しておくことにします。

これで QuciCheck を記述することができました。

QuickCheck を適用した最終的なコードは以下の通りです。

QuickCheck は本当に優秀で、自分の書いたコードに安心感をあたえてくれます。しかし、導入までのハードルが少し高いと思うので、日本語による実践的なチュートリアルがもう少し増えて欲しいところです。

QuickCheck