たのしいHaskellのツールチェインとC++

田中英行 <tanakh@preferred.jp>

PFIセミナー 2012/07/19

自己紹介

本日の概要

テスト駆動開発

Not enough!

Static typing is not enough!

Contracts are not enough!

XX are not enough!

(オチがそれかよ…)

絶対にバグのないプログラムを書く方法

一般的な話


定理証明系

なぜコードの正しさを証明しないのか?

(こんなことを書きつつ、私はあまり詳しくないです…すみません)

コードの品質のために、我々は何をするべきか?

In my opinion...

テストに「ちょい足し」

ぜひ Generative Test をやりましょう!

だけど…

そういう事なら

       |
   \  __  /
   _ (m) _ピコーン
      |ミ|
    /  `´  \
     (´・_・`) HaskellのQuickCheckでC++コードを
     ノヽノヽ テストすればいいじゃまいか!?
       くく

QuickCheck

QuickCheckについて

Quick start of QuickCheck

  1. Haskell Platform をインストール

  2. テストを含む cabal プロジェクトを作る

  3. テストを書く

  4. テストを実行

  5. 繰り返し

Haskell Platform インストール

ダウンロードして、マウスをホチポチすれば入る(Linux以外)

cabal プロジェクトを作る

> cabal init とタイプして、適当に質問に答える

PS C:\Users\tanakh\Dropbox\project\tmp\qc-test> cabal init
Package name? [default: qc-test]
Package version? [default: 0.1.0.0]
Please choose a license:
 * 1) (none)
   2) GPL-2
   3) GPL-3
...
What does the package build:
   1) Library
   2) Executable
Your choice? 2
Include documentation on what each field means (y/n)? [default: n]
Guessing dependencies...
...
Generating qc-test.cabal...

プロジェクトにテストを追加

<プロジェクト名>.cabal というファイルが生成されたはずなので、それを編集

name:                qc-test
version:             0.1.0.0
-- synopsis:            
-- description:         
license:             BSD3
(略…)
executable qc-test
  main-is:             main.hs
  build-depends:       base ==4.5.*
-- ↓これを追加
test-suite hoge-test
  type:                exitcode-stdio-1.0
  main-is:             test.hs
  build-depends:       base  ==4.5.*
                     , hspec >=1.3

なんかプログラム書く

mySum :: [Int] -> Int
mySum [x] = x
mySum (x:xs) = x + mySum xs

テストを書く

import Test.Hspec

main :: IO ()
main = hspec $ do
  describe "sum function" $ do
    it "is correct" $ do
      mySum [1, 2, 3] `shouldBe` 6
      mySum [1 .. 10] `shouldBe` 55

思いつくままにテストケースを書きます

テストを実行

> cabal build
Building qc-test-0.1.0.0...
Preprocessing executable 'qc-test' for qc-test-0.1.0.0...
[1 of 1] Compiling Main             ( main.hs, dist\build\qc-test\qc-test-tmp\Main.o )
Linking dist\build\qc-test\qc-test.exe ...
Preprocessing test suite 'hoge-test' for qc-test-0.1.0.0...
[1 of 1] Compiling Main             ( test.hs, dist\build\hoge-test\hoge-test-tmp\Main.o )
Linking dist\build\hoge-test\hoge-test.exe ...
> cabal test
Running 1 test suites...
Test suite hoge-test: RUNNING...
Test suite hoge-test: PASS
Test suite logged to: dist\test\qc-test-0.1.0.0-hoge-test.log
1 of 1 test suites (1 of 1 test cases) passed.

パスしたようですね!

QuickCheck のテストを書く

prop_mySum :: [Int] -> Bool
prop_mySum xs =
  mySum xs == sum xs -- sum は標準のリストの総和を求める関数

簡単でしょう?

テストフレームワークに組み込み

import Test.Hspec
import Test.Hspec.HUnit
import Test.Hspec.QuickCheck -- これを追加
-- (略)

main :: IO ()
main = hspec $ do
  describe "sum function" $ do
    it "is correct" $ do
      mySum [1, 2, 3] `shouldBe` 6
      mySum [1 .. 10] `shouldBe` 55

    prop "is equivalent to sum" $ -- これを
      prop_mySum                  -- 追加

別な書き方

import Test.Hspec
import Test.Hspec.HUnit
import Test.Hspec.QuickCheck
-- (略)

main :: IO ()
main = hspec $ do
  describe "sum function" $ do
    it "is correct" $ do
      mySum [1, 2, 3] `shouldBe` 6
      mySum [1 .. 10] `shouldBe` 55

    prop "is equivalent to sum" $ \xs ->
      mySym xs == sum xs

QuickCheck テストの実行

> cabal build && cabal test
Running 1 test suites...
Test suite hoge-test: RUNNING...

sum function
 - is correct
 - is equivalent to sum FAILED [1]

1) sum function is equivalent to sum FAILED
*** Failed! Exception: 'test.hs:(6,1)-(7,27): Non-exhaustive patterns in function mySum' (after 1 test):
[]
...
0 of 1 test suites (0 of 1 test cases) passed.

おや(棒)、テストがコケたみたいですね

テストの結果

   - is equivalent to sum FAILED [1]
> 1) sum function is equivalent to sum FAILED
> *** Failed! Exception: 'test.hs:(6,1)-(7,27): Non-exhaustive patterns in function mySum' (after 1 test):
> []

どうやら、空のリストの時にエラーになっているようです

mySum :: [Int] -> Int
mySum [x] = x    ←   あっ(´・_・`)
mySum (x:xs) = x + mySum xs

修正&再実行

mySum [] = 0
mySum (x:xs) = x + mySum xs

直して、再実行

> cabal test
Running 1 test suites...
Test suite hoge-test: RUNNING...
Test suite hoge-test: PASS
Test suite logged to: dist\test\qc-test-0.1.0.0-hoge-test.log
1 of 1 test suites (1 of 1 test cases) passed.

ふう、事なきを得ました。

C++のコードをテストする

HaskellからC++コードを呼び出す

int add(int x, int y)
{
    return x * y; // わざと間違えてます!!
}

.cabal ファイルを編集

test-suite hoge-test
  type:                exitcode-stdio-1.0
  main-is:             test.hs
  build-depends:       base ==4.5.*
                     , hspec
                     , QuickCheck
  -- ↓ C++ファイルを c-sources に列挙して外部ライブラリにstdc++を追加
  c-sources:           hoge.cpp
  extra-libraries:     stdc++

C++を呼び出すコードを書く

foreign import ccallcdecl 呼び出し規約の関数を呼び出せる

{-# LANGUAGE ForeignFunctionInterface #-}
import Test.Hspec
import Test.Hspec.HUnit
import Test.Hspec.QuickCheck
import Test.QuickCheck
import Test.QuickCheck.Property

main :: IO ()
main = hspec $ do
  describe "add function" $ do
    prop "it corrects" $ \x y -> do
      morallyDubiousIOProperty $ do -- テスト中でIOをやるための関数
        z <- add x y
        return $ z == x + y

foreign import ccall add :: Int -> Int -> IO Int

テスト実行

> cabal test
Running 1 test suites...
Test suite hoge-test: RUNNING...

add function
 - it corrects FAILED [1]

1) add function it corrects FAILED
*** Failed! Falsifiable (after 1 test and 2 shrinks):
0
1


Finished in 0.0120 seconds, used 0.0156 seconds of CPU time

1 example, 1 failure
Test suite hoge-test: FAIL
Test suite logged to: dist\test\qc-test-0.1.0.0-hoge-test.log
0 of 1 test suites (0 of 1 test cases) passed.

C++コードをQuickCheck

#include <vector>
using namespace std;

int vsum(const vector<int> &v) {
  int ret = 0;
  for (size_t i = 0; i < v.size(); ++i)
    ret += v[i];
  return ret;
}

これをテストしたいが、Haskellのデータ構造とのマーシャリングが面倒なので、 ラッパ関数を書く

extern "C" int c_vsum(int *p, int n) {
  return vsum(vector<int>(p, p+n));
}

テストコード

{-# LANGUAGE ForeignFunctionInterface #-}
import Test.Hspec
import Test.Hspec.HUnit
import Test.Hspec.QuickCheck
import Test.QuickCheck
import Test.QuickCheck.Property
import Foreign
import Foreign.C

main :: IO ()
main = hspec $ do
  describe "add function" $ do
    prop "it corrects" $ \xs -> do
      morallyDubiousIOProperty $ do
        withArrayLen (map fromIntegral xs) $ \len ptr -> do
          ret <- c_vsum ptr (fromIntegral len)
          return $ fromIntegral ret == (sum xs :: Int)

foreign import ccall c_vsum :: Ptr CInt -> CInt -> IO CInt

テスト実行

> cabal test
Running 1 test suites...
Test suite hoge-test: RUNNING...
Test suite hoge-test: FAIL
Test suite logged to: dist\test\qc-test-0.1.0.0-hoge-test.log
0 of 1 test suites (0 of 1 test cases) passed.

C++のテストがQuickCheckできました(´^_^`)!

ベンチマーク

Criterion

Criterionの特徴

C++でも使いたい!

C++をCriterion

とりあえず適当にソートプログラム書いてみた

#include <vector>
#include <algorithm>
using namespace std;

extern "C"
void vsort(int *p, int n)
{
  vector v(p, p+n);
  sort(v.begin(), v.end());
}

ベンチマークを cabalファイルに追加

benchmark hoge-bench
  type:                exitcode-stdio-1.0
  main-is:             bench.hs
  build-depends:       base ==4.5.*
                     , criterion
                     , random
  c-sources:           hoge.cpp
  extra-libraries:     stdc++

Criterion書く

{-# LANGUAGE ForeignFunctionInterface #-}
import Control.Monad
import Data.List
import Foreign
import Foreign.C
import System.Random

import Criterion.Main

main = do
  let len = 1000
  xs <- replicateM len $ randomRIO (0, 1000)
  defaultMain
    [ bgroup "sort"
      [ bench "c++"     $ whnfIO $ withArrayLen (map fromIntegral xs) $ \len ptr ->
         vsort ptr (fromIntegral len)
      , bench "haskell" $ nf sort (xs :: [Int])
      ]
    ]

foreign import ccall vsort :: Ptr CInt -> CInt -> IO ()

ベンチマーク実行

> cabal bench
Running 1 benchmarks...
Benchmark hoge-bench: RUNNING...
warming up
estimating clock resolution...
...

benchmarking sort/c++
mean: 54.11847 us, lb 53.89714 us, ub 54.40775 us, ci 0.950
std dev: 1.288492 us, lb 1.016472 us, ub 1.923825 us, ci 0.950
found 5 outliers among 100 samples (5.0%)
  4 (4.0%) high mild
  1 (1.0%) high severe
variance introduced by outliers: 17.107%
variance is moderately inflated by outliers

benchmarking sort/haskell
mean: 363.3632 us, lb 361.8362 us, ub 365.6127 us, ci 0.950
std dev: 9.413442 us, lb 7.040128 us, ub 14.36999 us, ci 0.950
found 4 outliers among 100 samples (4.0%)
  3 (3.0%) high mild
  1 (1.0%) high severe
variance introduced by outliers: 19.973%
variance is moderately inflated by outliers
Benchmark hoge-bench: FINISH

結果

結果

まとめ

まとめ