4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

関数型言語Elmでテスト駆動開発(14章)

Posted at

前回の記事の続きになります。前回は苦戦しながらも何とかElmとしての抽象化に成功をしました。今回は通貨の変換をおこなっていきます。大掛かりな実装は、おそらく最後になります。このシリーズも残すところ僅かです。

14章

冒頭で述べた通り、Moneyの変換をおこなっていきましょう。

  • $5 + 10 CHF = $10 (レートが2:1の場合)
  • $5 + $5 = $10
  • Bank.reduce(Money)
  • Moneyを変換して換算を行う
  • Reduce(Bank, Currency)

追加するテストコードは、以下のようになります。注目するべき点は、新しくBankという型が登場します。この型は、通貨から通貨へのレートを保存し計算をおこないます。以下の例では、CFHからUSDへのレートを追加したもので、テスト全体として2フランを1ドルに換算するというテストとなります。

tests/Tests.elm

+ , 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は今のところ受け取るだけです。

src/Bank.elm

+ 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の残す課題は、あと僅かです。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?