TDD
オブジェクト指向
デザインパターン
Elm
関数型プログラミング
More than 1 year has passed since last update.

前回の記事の続きになります。前回は関数型言語の制約と抽象化の力に付いて書きました。今回は一気に抽象化が進みます。

第8章

この章では、Factory Method パターンを使って貨幣の具体的な実装を隠蔽していきます。

src/Money.src

dollar : Amount -> Money
dollar amount =
    Dollar amount


franc : Amount -> Money
franc amount =
    Franc amount

diff記法をマスターしました。ここでやっと、Money Amountがprivateになります。import先では型としてのMoneyは扱えますが、パターンマッチによるamountの取り出しができなくなります(隠蔽)。

- module Money exposing (Money(..), times)
+ module Money exposing (Money, times, dollar, franc)

Amountの取り出しと同様に直接DollarやFrancの値コンストラクタの呼び出しもできなくなるので、テストコードもFactory Methodを使った形に変更する必要があります。

tests/Tests.elm

- => (Dollar 5 |> times 2)
- === Dollar 10
+ => (dollar 5 |> times 2)
+ === dollar 10

- => (Dollar 5 |> times 3)
- === Dollar 15
+ => (dollar 5 |> times 3)
+ === dollar 15

- => (Franc 5 |> times 2)
- === Franc 10
+ => (franc 5 |> times 2)
+ === franc 10

- => (Franc 5 |> times 3)
- === Franc 15  
+ => (franc 5 |> times 3)
+ === franc 15

これでDollarとFrancが隠蔽できたので、そもそも、この型は必要ないのではということになります。

  • \$5 + 10 CHF=\$10 (レートが2:1の場合)
  • $5 * 2 = 10
  • amountをprivateにする
  • Dollarの副作用をどうする?(最初から)
  • Moneyの丸め処理をどうする?
  • equals()(最初から)
  • hashCode()(最初から)
  • nullと等価性比較
  • 他のオブジェクトと等価比較(最初から)
  • 5 CHF * 2 = 10 CHF
  • DollarとFrancの重複
  • equalsの一般化(最初から)
  • timesの一般化(△)
  • FrancとDollarを比較する(最初から)
  • 通貨の概念

第9章

通貨の概念を導入していきます。単にMoneyの型に文字列が追加されるだけですね。例によってわかりやすくtype alias(Currency)を張ります。ただし、変更箇所は大きいです。currency関数で、MoneyのCurrencyはprivateになります。パターンマッチでは、AmountとCurrencyどちらも取得されますが、必要がない場合にはアンダースコア(_)で省略が可能になります。

src/Money.elm

module Money exposing (Money, times, dollar, franc, currency)


type alias Amount =
    Int


type alias Currency =
    String


type Money
    = Dollar Amount Currency
    | Franc Amount Currency


dollar : Amount -> Money
dollar amount =
    Dollar amount "USD"


franc : Amount -> Money
franc amount =
    Franc amount "CHF"


times : Int -> Money -> Money
times multiplier money =
    case money of
        Dollar amount _ ->
            dollar <| multiplier * amount

        Franc amount _ ->
            franc <| multiplier * amount


amount : Money -> Amount
amount money =
    case money of
        Dollar amount _ ->
            amount

        Franc amount _ ->
            amount


currency : Money -> Currency
currency money =
    case money of
        Dollar _ currency ->
            currency

        Franc _ currency ->
            currency

通貨に関するテストが追加されています。

module Tests exposing (..)

import Test exposing (..)
import TestExp exposing (..)


-- Test target modules

import Money exposing (..)


all : Test
all =
    describe "Money Test"
        [ describe "Dollar"
            [ "Multiplication1"
                => (dollar 5 |> times 2)
                === dollar 10
            , "Multiplication2"
                => (dollar 5 |> times 3)
                === dollar 15
            , "Currency"
                => (currency <| dollar 5)
                === "USD"
            ]
        , describe "Franc"
            [ "Multiplication1"
                => (franc 5 |> times 2)
                === franc 10
            , "Multiplication2"
                => (franc 5 |> times 3)
                === franc 15
            , "Currency"
                => (currency <| franc 5)
                === "CHF"
            ]
        , describe "Equality"
            [ "Equality1"
                => dollar 10
                === dollar 10
            , "Equality2"
                => franc 10
                === franc 10
            , "Equality3"
                => dollar 1
                /== franc 1
            , "Equality4"
                => dollar 1
                /== dollar 2
            , "Equality5"
                => franc 1
                /== franc 2
            ]
        ]
  • \$5 + 10 CHF=\$10 (レートが2:1の場合)
  • $5 * 2 = 10
  • amountをprivateにする
  • Dollarの副作用をどうする?(最初から)
  • Moneyの丸め処理をどうする?
  • equals()(最初から)
  • hashCode()(最初から)
  • nullと等価性比較
  • 他のオブジェクトと等価比較(最初から)
  • 5 CHF * 2 = 10 CHF
  • DollarとFrancの重複
  • equalsの一般化(最初から)
  • timesの一般化(△)
  • FrancとDollarを比較する(最初から)
  • 通貨の概念

第10章

10章は、DollarやFrancのtimesメソッドの戻り値をMoneyのインスタンスで返す。という内容とtoString()の実装という内容なのですが、toString()は以下のように最初から実装がされているので(地味に便利ですよね。)省略をさせていただきます。

> toString <| Dollar 1 "USD"
"Dollar 1 \"USD\"" : String

第11章

9章で貨幣の概念が出てきた為、具象型DollarとFrancは要らないだろうということで、消しに掛かります。そうすることでtimesの型分岐(パターンマッチ)は消え、無事一般化されます。elmの良いところとしてUnion Typesで楽に型が生成できるため、9章で文字列だったCurrencyは、正式にCurrency型として定義しました。それでは変更を御覧ください。

src/Money.elm

module Money exposing (Money, Currency(..), times, dollar, franc, currency)


type alias Amount =
    Int


type Currency
    = USD
    | CHF


type Money
    = Money Amount Currency


dollar : Amount -> Money
dollar amount =
    Money amount USD


franc : Amount -> Money
franc amount =
    Money amount CHF


times : Int -> Money -> Money
times multiplier (Money amount currency) =
    Money (multiplier * amount) currency


amount : Money -> Amount
amount (Money amount _) =
    amount


currency : Money -> Currency
currency (Money _ currency) =
    currency

テストはCurrency型の比較になった程度で、他の内容は変わりません。

tests/Tests.elm

module Tests exposing (..)

import Test exposing (..)
import TestExp exposing (..)


-- Test target modules

import Money exposing (..)


all : Test
all =
    describe "Money Test"
        [ describe "Dollar"
            [ "Multiplication1"
                => (dollar 5 |> times 2)
                === dollar 10
            , "Multiplication2"
                => (dollar 5 |> times 3)
                === dollar 15
            , "Currency"
                => (currency <| dollar 5)
                === USD
            ]
        , describe "Franc"
            [ "Multiplication1"
                => (franc 5 |> times 2)
                === franc 10
            , "Multiplication2"
                => (franc 5 |> times 3)
                === franc 15
            , "Currency"
                => (currency <| franc 5)
                === CHF
            ]
        , describe "Equality"
            [ "Equality1"
                => dollar 10
                === dollar 10
            , "Equality2"
                => franc 10
                === franc 10
            , "Equality3"
                => dollar 1
                /== franc 1
            , "Equality4"
                => dollar 1
                /== dollar 2
            , "Equality5"
                => franc 1
                /== franc 2
            ]
        ]

これでtimesは正式に一般化に成功しました。

  • \$5 + 10 CHF=\$10 (レートが2:1の場合)
  • $5 * 2 = 10
  • amountをprivateにする
  • Dollarの副作用をどうする?(最初から)
  • Moneyの丸め処理をどうする?
  • equals()(最初から)
  • hashCode()(最初から)
  • nullと等価性比較
  • 他のオブジェクトと等価比較(最初から)
  • 5 CHF * 2 = 10 CHF
  • DollarとFrancの重複
  • equalsの一般化(最初から)
  • timesの一般化
  • FrancとDollarを比較する(最初から)
  • 通貨の概念

コード全体

まとめ

第11章の時点でのコードをJavaのコードと比較してもらいたいのですが、実のところ表現されている内容の意味はほぼ一致します。書き方に関しても、フィールドはパターンマッチにより取り出す。今まで説明したとおりequalstoStringメソッドは既に定義されている。この二点さえ除けば、本当にJavaと変わらないコードになります。ここで私は、オブジェクト指向プログラミングと関数型プログラミングは、全然別次元の言語ではないんだということを確信しました。現実の問題に対する抽象化に向かうという方向は何ら変わりありません。少し手法が異なるだけで、関数型言語ということで警戒する必要はないんだと強く心に刻み込んだところで続きの実装が楽しみになってきました!次回をお待ち下さい。