前回の記事の続きになります。前回は関数型言語の制約と抽象化の力に付いて書きました。今回は一気に抽象化が進みます。
第8章
この章では、Factory Method パターンを使って貨幣の具体的な実装を隠蔽していきます。
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を使った形に変更する必要があります。
- => (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どちらも取得されますが、必要がない場合にはアンダースコア(_)で省略が可能になります。
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型として定義しました。それでは変更を御覧ください。
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型の比較になった程度で、他の内容は変わりません。
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のコードと比較してもらいたいのですが、実のところ表現されている内容の意味はほぼ一致します。書き方に関しても、フィールドはパターンマッチにより取り出す。今まで説明したとおりequals
やtoString
メソッドは既に定義されている。この二点さえ除けば、本当にJavaと変わらないコードになります。ここで私は、オブジェクト指向プログラミングと関数型プログラミングは、全然別次元の言語ではないんだということを確信しました。現実の問題に対する抽象化に向かうという方向は何ら変わりありません。少し手法が異なるだけで、関数型言語ということで警戒する必要はないんだと強く心に刻み込んだところで続きの実装が楽しみになってきました!次回をお待ち下さい。