読者です 読者をやめる 読者になる 読者になる

TDD in Actionを共同開催しました #tddact

イベント Haskell TDD

7月22日にTDD in Actionというイベントを共同で開催しました。

7月22日 TDD in Action #tddact(東京都)
TDD in Action #tddact - Togetterまとめ

会場抑えてくださったり募集ページを立てたりその他色々とやってくださった共同主催者の@kyon_mmさん、ありがとうございました。言いだしっぺと司会以外やってなくてすみません・・・。

また、オラクルさんには会場をお借りしました、ありがとうございます。

更にTDD Base Campコミュニティの皆様もありがとうございます。
特に本拠地ではないにもかかわらず手伝ってくださった@irofさんに感謝であります。

どんなイベントだったの?

少しでもTDDの要素が入っていればあとは何してもいいゆるふわペアプロ大会でした。
ゆるふわなので朝からビールクズまで行われていました(おすそわけしてくださったJPOUGの皆様に感謝!)。

詳細は参加者の報告記事で垣間見れます。

TDD in Action に参加しました - THE BLUE NOWHERE
TDD in Action - 負けてゐる日誌
[TDD] TDD in Actionに参加してきた - joker1007の日記
TDD in Actionに参加してきた #tddact - くりにっき
TDD in Action #tddact に行ってきた - アル中プログラマの備忘録
mike、mikeなるままに…: TDD in Actionに参加してきた
久々に東京遠征してきた - 日々常々

私はといえば、司会とライブコーディングとHaskellを少ししていました。

ちなみにきょんさんとのライブコーディングですが、30分予定なのに25分近くコードを書かずにドメイン分析っぽいこととテスト戦略っぽいことをやってました。
30分コードを書かずに終わるかと思っていたらそうでもなかったですね。

懇親会は・・・Specs2やり直して少しは反論できるようにしてきますorz

次回あるの?

え、一発ネタのイベントなので可能性は低いかと。
あったとしても私がやる場合は研究で切羽詰ってデータ収集したいときとかそんな感じでしょう。

会場探しがもう少し楽なら考えるのですけどねー・・・。

おまけ

イベント自体については書くことがあまりなかったので、ついでに自販機問題をHaskellでやってみた的なことを書いておきます。
設計や実装はあまり良くないかもなーと思いつつ、何事もさらしておくものだよということでさらしておきます。

自販機問題というのはTDDBC大阪2.0で作成されたTDD練習問題です。
TDD in Actionでもこれを課題として利用しました。

TDD Boot Camp(TDDBC) - TDDBC大阪2.0/課題

環境準備

今回のディレクトリ構成は以下の通りです。

vending-machine-haskell
|- Setup.hs
|- vending-machine-haskell.cabal
|- src
   |- VendingMachine.hs
   |- MoneyStack.hs
|- dist
   |- ビルドやテストでの生成物置き場
|- test
   |- doctests.hs
   |- (Spec.hs)

cabal init コマンドを実行して指示にあわせて入力するとと vending-machine-haskell.cabalと Setup.hsが生成されるはずです。

次に、cabalファイルを修正していきます。
今回は最小構成にしてみました。

vending-machine-haskell.cabal

name:               vending-machine-haskell
version:            0.1.0
author:              pocketberserker
maintainer:       ****@gmail.com
build-type:        Simple
cabal-version:  >=1.0

library
  hs-source-dirs:  src
  ghc-options:     -Wall
  build-depends:
        base
      , mtl
  exposed-modules: VendingMachine

test-suite doctests
  type:            exitcode-stdio-1.0
  hs-source-dirs:  test
  main-is:         doctests.hs
  ghc-options:     -Wall -threaded
  build-depends:
        base
      , doctest >= 0.7

doctests.hsの作成

doctest用のtest-suiteを作成しましょう。

-- doctests.hs
module Main where

import Test.DocTest

main :: IO ()
main = doctest ["--optghc=-isrc", "src/VendingMachine.hs"]

optghcはdoctestのオプションで、=以降に記述したオプションをGHCに転送するコマンドらしいです。
ここでは-iオプションを使って指定したディレクトリ下(今回はsrcを指定しました)のファイルを探索しています。

初期はVendingMachine.hsのみdoctestで実行するようにしました。
モジュールが増えてテスト対象を追加する必要が出てきたらリストにテストしたいファイル名を追加していきます。

何が必要か考える

それでは開発開始です。

まずステップ2あたりまでを眺め、何が必要そうか確認してみます。

  • 受け付けるお金(acceptableMoney)
  • 想定外のなお金
  • 自販機(VendingMachine)
  • 自販機へお金投入(insert)
  • お金は複数回投入可能
  • 投入金額の合計(TotalInsertAmount)
  • 払い戻し操作(payback)
  • 想定外のお金はユーザに払い戻す
  • ジュース(drink)
    • ジュース名(name)
    • 値段(price)
  • ジュースの在庫(stock)
  • 格納されている(購入可能な)ジュース(purchasableDrink)

これだけだと投入したお金の状態やジュースラックが存在しないので追加しておきます。

  • 投入されたお金の状態(MoneyStack)
  • ジュースラック(drinkRack)

もう少しきちんとドメイン考えなよという話ですが、一人では思いつかないのでひらめいたときにでも名称変更します・・・。

MoneyStackの作成

MoneyStackは投入されたお金の数を状態として保存するデータ構造として考えます。
データ構造に不安はないので一気に作ってしまいましょう。
ここではMoneyStackモジュールを作成してからレコードを定義しました。

-- MoneyStack.hs
module MoneyStack where

data MoneyStack =
  MoneyStack { ten :: Int
             , fifty :: Int
             , hundred :: Int
             , fiveHundred :: Int
             , thousand :: Int} deriving (Show)

各お金が何回投入されたかを記録する形にしました。

出来上がったのでビルドとテストを実行します。

cabal configure --enable-test && cabal build && cabal test doctests

テストがまだ一切記述されていないので、ビルドだけでも良かったりもします。

MoneyStack.init の作成

さて、投入資金管理用のデータ構造が出来上がったわけですが、これを毎回初期化するのは面倒くさいのでinit関数を作っておきます。

まずテストを書きます(これ以降コードは変更部分のみ記述します)。

-- MoneyStack.hs
{-| function 'init'.

>>> init
MoneyStack {ten = 0, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0}
-}
init = undefined

更に、doctestでテストを実行するように指定します。

-- doctests.hs
main = doctest ["--optghc=-isrc", "src/VendingMachine.hs", "src/MoneyStack.hs"]

テストを実行すると失敗します。undefinedだからそれもそうですね、実装します。

-- MoneyStack.hs
init =   MoneyStack { ten = 0
                    , fifty = 0
                    , hundred = 0
                   , fiveHundred = 0
                    , thousand = 0}

通りました。これで状態の初期化は楽になりましたね。

お金を投入できる

お金を投入できるようにしましょう。

ぱっと思いつく簡単なテストは、お金が一切投入されていない状態で10円が投入されたら10円が1枚存在することを確認することです。
お金はとりあえずIntで表現し、投入資金記録はStateモナドを使ってみます。

-- VendingMachine.hs
module VendingMachine where

{-| function 'insert'.

>>> runState (insert 10) MoneyStack.init
(Nothing,MoneyStack {ten = 1, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0})
-}
insert = undefined

テストを実行すると関数未定義で失敗するので、簡単な実装をします。

-- VendingMachine.hs
import Control.Monad.State
import MoneyStack

insert 10 = state $ \s -> (Nothing, s { ten = 1})

テストに成功しました、ほっと一息。

お金を複数回投入できる

でも、お金は複数回投入できないといけないようです。テストを追加します。

-- VendingMachine.hs
{-| function 'insert'.

  省略

>>> runState (insert 10 >> insert 10) MoneyStack.init
(Nothing,MoneyStack {ten = 2, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0})
-}

実行すると・・・レッドになりました。

先ほどまでの実装ではどう頑張っても10円が1回投入された状態しか返せないので、直前までに投入された枚数+1の状態を返せるように変更しましょう。

-- VendingMachine.hs
insert 10 = state $ \s -> (Nothing, s { ten = ten s + 1})

テストに通りました。

50,100,500,1000円も受け付けるようにする

でもこの実装、他のお金(50,100,500,1000円)にまだ対応していないので、実装を追加する必要があります。

幸い、10円と同様の実装で大丈夫そうなので不安はありません。
一気に実装してしまいましょう(テストは後付で作りました)。

-- VendingMachine.hs
{-| function 'insert'.

  省略

>>> runState (insert 50) MoneyStack.init
(Nothing,MoneyStack {ten = 0, fifty = 1, hundred = 0, fiveHundred = 0, thousand = 0})

>>> runState (insert 100) MoneyStack.init
(Nothing,MoneyStack {ten = 0, fifty = 0, hundred = 1, fiveHundred = 0, thousand = 0})

>>> runState (insert 500) MoneyStack.init
(Nothing,MoneyStack {ten = 0, fifty = 0, hundred = 0, fiveHundred = 1, thousand = 0})

>>> runState (insert 1000) MoneyStack.init
(Nothing,MoneyStack {ten = 0, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 1})
-}
insert 50 = state $ \s -> (Nothing, s { fifty = fifty s + 1})
insert 100 = state $ \s -> (Nothing, s { hundred = hundred s + 1})
insert 500 = state $ \s -> (Nothing, s { fiveHundred = fiveHundred s + 1})
insert 1000 = state $ \s -> (Nothing, s { thousand = thousand s + 1})

想定外のお金はユーザに払い戻す

想定外のお金はユーザに払い戻す必要があります。

-- VendingMachine.hs
{-| function 'insert'.

  省略

>>> runState (insert 10 >> insert 1) MoneyStack.init
(Just 1,MoneyStack {ten = 1, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0})
-}

想定外のお金は計算結果として返すようにしてみました。
でもこの設計はあまりよろしくない気もしますね・・・何かいい方法ないかなぁ。

テストに失敗したので実装に移ります。

-- VendingMachine.hs
insert other = state $ \s -> (Just other, s)

グリーンになりました。
これでお金投入関係は終わりですかね。

投入金額の合計算出

投入金額の合計値を計算する関数、totalを作成しましょう。
まずは10円が1枚投入されているときのテストから。

-- MoneyStack.hs
{-| function 'total'.

>>> total MoneyStack {ten = 1, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0}
10
-}
total = undefined

簡単なので、思いつきで一気に実装します。

-- MoneyStack.hs
total s =
  ten s * 10 + fifty s * 50 + hundred  s * 100 + fiveHundred s * 500 + thousand s * 1000

テストには通りました。

うーん、そのままでもいいですけど、試しにフリーポイントスタイルにリファクタリングしましょうかね。

-- MoneyStack.hs
import Control.Monad.List ()

total = sum . sequence 
  [(*10) . ten, (*50) . fifty, (*100) . hundred, (*500) . fiveHundred, (*1000) . thousand]

逆に読みにくいかも、と感じた人はバージョン管理システムで一つ前の状態に戻しましょう。

払い戻し操作

払い戻し操作は、戻ってくるお金のリストとお金が全く投入されていない状態が返ってきてほしい感じなので以下のような形でテストを書きました。

-- VendingMachine.hs
{-| function 'payback'.

>>> runState payback $ execState (insert 10) MoneyStack.init
([10],MoneyStack {ten = 0, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0})
-}
payback :: State MoneyStack [Int]
payback = undefined

テストはもちろん失敗します。
型が推論では解決できそうにないので先に記述しました。

-- VendingMachine.hs
payback = state $ \s -> (take (ten s) $ repeat 10, MoneyStack.init)

あまりTDDになっている気がしないですが・・・まぁ明白な実装ということで。
これをリファクタリングします。

-- VendingMachine.hs
payback = state $ \s -> (convert s (ten, 10), MoneyStack.init)
  where convert s (f, m) = take (f s) $ repeat m

さらにリファクタリングします。

-- VendingMachine.hs
payback = state $ \s -> (calc s, MoneyStack.init)
  where convert s (f, m) = take (f s) $ repeat m
             calc s = convert s (ten, 10)

少しきれいになったような、そうでもないような感じですね。関数名は目を瞑ることにします。

他のお金が投入されている場合も戻ってくるかテストしましょう。

-- VendingMachine.hs
{-| function 'payback'.

  省略

>>> runState payback $ execState (insert 10 >> insert 1000) MoneyStack.init
([10,1000],MoneyStack {ten = 0, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0})
-}

1000は含まれないということで落ちました。
実装に移りましょう。

-- VendingMachine.hs
payback = state $ \s -> (calc s, MoneyStack.init)
  where convert s (f, m) = take (f s) $ repeat m
             calc s = concat $ map (convert s) [(ten, 10), (thousand, 1000)]

テストに通りました。
後は50,100,500円の実装ですが、関数と値の組を追加すればいいだけなのでさっさと追加してしまいます。

-- VendingMachine.hs
payback = state $ \s -> (calc s, MoneyStack.init)
  where convert s (f, m) = take (f s) $ repeat m
             calc s = concat $ map (convert s)
               [(ten, 10), (fifty, 50), (hundred, 100), (fiveHundred, 500), (thousand, 1000)]

さて、これでステップ1までが完了した形でしょうか。

全体像

ここまでの経過を貼り付けておきます*1

-- MoneyStack.hs
module MoneyStack where

import Control.Monad.List ()

data MoneyStack =
  MoneyStack { ten :: Int
             , fifty :: Int
             , hundred :: Int
             , fiveHundred :: Int
             , thousand :: Int} deriving (Show)

{-| function 'init'.

>>> init
MoneyStack {ten = 0, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0}
-}
init =   MoneyStack { ten = 0
                    , fifty = 0
                    , hundred = 0
                    , fiveHundred = 0
                    , thousand = 0}

{-| function 'total'.

>>> total MoneyStack {ten = 1, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0}
10
-}
total = sum . sequence 
  [(*10) . ten, (*50) . fifty, (*100) . hundred, (*500) . fiveHundred, (*1000) . thousand]
-- VendingMachine.hs
module VendingMachine where

import Control.Monad.State
import MoneyStack

-- VendingMachine.hs
{-| function 'insert'.

>>> runState (insert 10) MoneyStack.init
(Nothing,MoneyStack {ten = 1, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0})

>>> runState (insert 50) MoneyStack.init
(Nothing,MoneyStack {ten = 0, fifty = 1, hundred = 0, fiveHundred = 0, thousand = 0})

>>> runState (insert 100) MoneyStack.init
(Nothing,MoneyStack {ten = 0, fifty = 0, hundred = 1, fiveHundred = 0, thousand = 0})

>>> runState (insert 500) MoneyStack.init
(Nothing,MoneyStack {ten = 0, fifty = 0, hundred = 0, fiveHundred = 1, thousand = 0})

>>> runState (insert 1000) MoneyStack.init
(Nothing,MoneyStack {ten = 0, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 1})

>>> runState (insert 10 >> insert 10) MoneyStack.init
(Nothing,MoneyStack {ten = 2, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0})

>>> runState (insert 10 >> insert 1) MoneyStack.init
(Just 1,MoneyStack {ten = 1, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0})
-}
insert 10 = state $ \s -> (Nothing, s { ten = ten s + 1})
insert 50 = state $ \s -> (Nothing, s { fifty = fifty s + 1})
insert 100 = state $ \s -> (Nothing, s { hundred = hundred s + 1})
insert 500 = state $ \s -> (Nothing, s { fiveHundred = fiveHundred s + 1})
insert 1000 = state $ \s -> (Nothing, s { thousand = thousand s + 1})
insert other = state $ \s -> (Just other, s)

{-| function 'payback'.

>>> runState payback $ execState (insert 10) MoneyStack.init
([10],MoneyStack {ten = 0, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0})

>>> runState payback $ execState (insert 10 >> insert 1000) MoneyStack.init
([10,1000],MoneyStack {ten = 0, fifty = 0, hundred = 0, fiveHundred = 0, thousand = 0})
-}
payback :: State MoneyStack [Int]
payback = state $ \s -> (calc s, MoneyStack.init)
  where convert s (f, m) = take (f s) $ repeat m
             calc s = concat $ map (convert s)
               [(ten, 10), (fifty, 50), (hundred, 100), (fiveHundred, 500), (thousand, 1000)]

この設計はひどいとか改善案とか随時募集しています。

続きは気が向いたらか、もしくはTDDBCごとに更新とかでもいいかもしれないです。

*1:全部出来上がったらGitHubに公開します