7月のTDDBC東京のときのお題、飲み物自動販売機 Ver 2.0をお借りして、実践しつつTDDってこうやってやるんだよー的なことを説明してから、2人でコード書いてもらった。
2人とも、別に俺が隅から隅まで教えなくても勝手にどうやったらいいか考えてやってくれるってのはわかってた。で、予想通り後半暇になってきたので、俺ももう1回改めて同じ課題をやってみた。
やっぱりStateモナドが正しかった
7月のとき、最初にお題見た時から「これStateじゃね?」とは思っていたんだけど、僕は初対面の人とペアプロやっていきなり「Stateモナドで行きましょう」とか言い出すこわい人じゃないので、さすがにやらなかったんですね(@razonさんにそれを言っても問題なかったかもしれないけど)。
というわけで、今日書いたコード全貌はこちら。
https://gist.github.com/nisshiee/6578338
ある瞬間のスナップショットとしてのVendingMachine型を用意するところまでは7月と一緒。で、前回はここから「VendingMachineを引数に受け取って、次の状態を表すVendingMachineを返す関数」を実装していったわけだが、今回は「State[VendingMachine, _]」を実装していく。あ、言い忘れたけどStateはScalaz使う。
Stateを作るのは簡単で、Stateオブジェクトのapplyメソッドを使えば良い。
引数fには、「処理前の状態Sを受け取り、処理後の状態S'と処理に伴って出力される値Aのタプルを返す」関数を渡してやれば良い。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def apply[S, A](f: (S) ⇒ (S, A)): State[S, A] |
例えば、現在の投入金額合計を取得する処理は、状態を表すVendingMachineには変化を与えず、金額だけをIntとして取得するStateとなるので、以下の様になる。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val total = State[VendingMachine, Int] { vm => (vm, vm.total) } |
コインを投入する処理の場合は、VendingMachineの投入金額合計を加算して、処理に伴う出力は特に無いStateとなる。ただし、投入するコインの種類によって加算する額を変えたいので、一段高階にする。つまりStateそのものをvalで定義するのではなく、「Money => State」を定義する。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
val insert: UsableMoney => State[VendingMachine, Unit] = m => State { vm => | |
(vm.copy(total = vm.total + m.value), ()) | |
} |
で、用意したStateを実行(?)するときは、まずいつもどおりモナドをfor式なりmapなりflatMapなりで結合して、最後にapply(処理後のSとAを両方取得)なり、eval(処理後のAを取得)なり、exec(処理後のSを取得)なりのメソッドに"初期状態"を渡してやれば良い。
「100円入れて、10円入れて、そのあとの投入金額合計を取得」はこうなる。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(insert(Money100) >> insert(Money10) >> total) eval VendingMachine.init // = 110 | |
// もしくはfor式で書くならこう | |
(for { | |
_ <- insert(Money100) | |
_ <- insert(Money10) | |
t <- total | |
} yield t) eval VendingMachine.init // = 110 |
さいごに
この自動販売機のお題、オブジェクト指向で順番にやっていくと、内部状態が関わる関係でいろいろややこしい状況に陥る面白いお題になっている(やってみるとわかる)。
でもStateモナドを使ったらどうなるだろうか。僕が書いたテストコード、最初から最後まで一貫してStateモナドの枠の中で収まっている(実装しておいたStateを、1そのまま、2演算子で結合、3for式で結合のどれかして、evalかけているだけ。全テストケースとも。)
モナド、面白いね!
0 件のコメント:
コメントを投稿