前回の記事の続きになります。前回は苦戦しながらも何とかElmとしての抽象化に成功をしました。今回は通貨の変換をおこなっていきます。大掛かりな実装は、おそらく最後になります。このシリーズも残すところ僅かです。
14章
冒頭で述べた通り、Moneyの変換をおこなっていきましょう。
- $5 + 10 CHF = $10 (レートが2:1の場合)
- $5 + $5 = $10
-
Bank.reduce(Money) - Moneyを変換して換算を行う
- Reduce(Bank, Currency)
追加するテストコードは、以下のようになります。注目するべき点は、新しくBank
という型が登場します。この型は、通貨から通貨へのレートを保存し計算をおこないます。以下の例では、CFHからUSDへのレートを追加したもので、テスト全体として2フランを1ドルに換算するというテストとなります。
+ , describe "Redice Money Dirrerent Currency"
+ [ "CHF => USD 2"
+ => let
+ twoCHF =
+ Money.expression <| franc 2
+ -- addRateは、Bank型を返す。
+ bank =
+ Bank.Bank |> Bank.addRate CHF USD 2
+
+ result =
+ Bank.reduce twoCHF USD bank
+ in
+ dollar 1 === result
+ ]
それではBank
型とaddRate
関数について見ていきましょう。テストを通すだけなので、Bankは単なる仮の型です。Bankは、レートを追加していくので状態を扱うことになります。特にElmのような純粋関数型では、副作用を完全に排除しているので、状態を扱うコードは苦手と思われがちですが、実際はそのようなことはありません。void addRate(String from, String to, int rate)
というメソッドは、換算情報を受け取りBank型を返す関数へと生まれ変わります。・・・しかし、今回は仮実装なので単にbankを受け取ってbankを返すだけの関数です。レート計算の仮実装は、reduce
関数に直に書いて実装をします。Bankは今のところ受け取るだけです。
+ type Bank
+ = Bank
+ addRate : Currency -> Currency -> Float -> Bank -> Bank
+ addRate from to rate bank =
+ bank
- reduce : Expression -> Currency -> Money
- reduce source to =
+ reduce : Expression -> Currency -> Bank -> Money
+ reduce source to bank =
case source of
- Single (Money amount _) ->
- Money amount to
+ Single (Money amount currency) ->
+ let
+ rate =
+ if ( currency, to ) == ( CHF, USD ) then
+ 2
+ else
+ 1
+ in
+ Money (amount // rate) to
Sum exp1 exp2 ->
- Money (sum_ exp1 exp2 to) to
+ Money (sum_ exp1 exp2 to bank) to
- sum_ : Expression -> Expression -> Currency -> Int
- sum_ exp1 exp2 to =
+ sum_ : Expression -> Expression -> Currency -> Bank -> Int
+ sum_ exp1 exp2 to bank =
let
getAmount =
- (\expression -> amount <| reduce expression to)
+ (\expression -> amount <| reduce expression to bank)
in
(getAmount exp1) + (getAmount exp2)
それでは、Money換算の実装をしていきましょう。Bankは、ある通貨からある通貨のペアをキーとし、rateを値として持つHashMapとして持つ、というのがTDD本でのやり方です。Elmでも同じ実装が可能です。通貨のペアを実装するためにJavaでは、Pair
型という新しいクラスを実装する必要がありますが、Elmではタプルという任意の型のペアを作る仕組みが存在しています。HashMapは、Dict
という型で代用することができます。しかし、Dictのキーの型は、comparable(Int, Float, Time, Char, Strin)型とcomparable型のタプルもしくは、リストでなければならないという制約があります。残念ながらCurrency
はUnion型のため、この制約に反しています。今回は、せっかくCurrency型を定義したので、ライブラリの力を借ります。eeue56/elm-all-dictの中にある、EveryDict
という型を使うと任意の型をキーの型として扱うことができます。基本的な使い方は、Dictと変わりありません。もし、EveryDictを使いたくない場合は、(String, String)
をキーの型として使うと良いでしょう。
type alias Bank =
EveryDict ( Currency, Currency ) Int
Dictを利用していることを隠蔽したいので、空の辞書を返す関数を用意します。
bank : Bank
bank =
EveryDict.empty
タプルは、簡単な記号関数を用意することで、通貨を変換しているような、ちょっとしたDSLとして用意することができます。
-- (USD, CHF) === USD ~> CHF
(~>) : a -> b -> ( a, b )
(~>) a b =
( a, b )
次に、addRate
関数は、Dictに新しいrate計算を追加し、更新されたDict型を返す関数として定義してあげれば良いだけです。
addRate : ( Currency, Currency ) -> Int -> Bank -> Bank
addRate fromTo rate =
EveryDict.insert fromTo rate
Elmの辞書型(Dict)は、存在しないキーを取得しようとしたときも安心です。結果がMaybe型で返却されます。ここでは、換算レートが見つから無かったと言う実行時エラーに変換してあげます。また、fromとtoのCurrencyが同じ時には、同レート(1)を返してあげます。
rate : ( Currency, Currency ) -> Bank -> Int
rate (( from, to ) as fromto) bank =
if from == to then
1
else
case EveryDict.get fromto bank of
Just r ->
r
Nothing ->
Debug.crash <| (toString from) ++ " ~> " ++ (toString to) ++ " is not found."
最後に、reduce
は、rate
関数からrateを取り出し、amountを割ってあげるだけで実装完了となります。
reduce : Expression -> Currency -> Bank -> Money
reduce source to bank =
case source of
Single (Money amount currency) ->
let
r =
rate (currency ~> to) bank
in
Money (amount // r) to
Sum exp1 exp2 ->
Money (sum_ exp1 exp2 to bank) to
せっかくなので、先ほどのテストコードにちょっとしたDSLを加えてみましょう。直感的になった気がします。
bank =
Bank.bank |> Bank.addRate (CHF ~> USD) 2
一気にTODOリストが消化されました。残すは足し算時に、今回の換算処理が働けば完了です。
- $5 + 10 CHF = $10 (レートが2:1の場合)
-
$5 + $5 = $10 - $5 + $5 がMoneyを返す
-
Bank.reduce(Money) -
Moneyを変換して換算を行う -
Reduce(Bank, Currency)
まとめ
今回は状態を持ちレートが変動するBank型の実装で関数型では実装が難しいかと思われましたが、実のところ辞書型やタプル型のお陰ですんなり実装できてしまいました。それどころか利点として、辞書に存在しないキーの場合はMaybeでエラー処理を強制することができ、安全に実装を進めることができます。ちょっとしたDSLの導入により、テストの可読性も確保することができました。いい事ずくめの回だったと思います。TDD Elmの残す課題は、あと僅かです。