前回の記事の続きになります。前回はtimesが一般化されスッキリしたコードになりました。今回から遂に為替に関する実装を開始していきます。
12章
ここで一旦TODOリストを綺麗に整理します。そして為替の第一段階としてUSドル同士の足し算を新たなタスクとして追加します。
- $ + 10 CHF = $10 (レートが2:1の場合)
- $5 + $5 = $10
以下が追加される対象のテストです。5ドル同士をplus
関数に渡すことで足し算をおこないます。そのままそれが答えになるわけではなく、足し算の計算結果は、為替を考慮してBank.reduce
の関数に通貨を渡すことで最終的な結果に変化します。
++ describe "Simple Addition"
++ [ "addition1"
++ => let
++ five =
++ dollar 5
++
++ sum =
++ plus five five
++
++ reduced =
++ Bank.reduce sum USD
++ in
++ dollar 10
++ === reduced
++ ]
plusとreduce
の詳細を見ていきましょう。本当であればplus関数の戻り値の型はMoneyではなく、計算を示すExpression
と言う型であるべきです。しかし、一旦テストを通したいので仮実装となります。2つのMoney型の値を受け取って、Money型の値を返します。パターンマッチをしてamount同士を足し合わせます。もう皆さん慣れましたね?
++ plus : Money -> Money -> Money
++ plus (Money amount1 currency) (Money ++ amount2 _) =
++ Money (amount1 + amount2) currency
reduceの実装は本当に仮のものです。単に10ドルの値を返します。
[src/Bank.elm]
++ module Bank exposing (..)
++ import Money exposing (..)
++ reduce : Money -> Currency -> Money
++ reduce source to =
++ Money.dollar 10
13章
13章は非常に難しい章でした。Javaの内容をそのまま移植したのですが、しっくりせずElm使いのフォロワーさん達のお力添えもあり何とか納得できる形になりました。この場を借りてお礼を言いたいと思います。実際後で確認したところ、後の章の抽象化まで進んでしまいましたが、とりあえずそれも含めて成果とします。初期のダメダメな設計も含めて包み隠さず順を追って説明していきたいと思います(これぞTDD・・・?)。
TODOの項目は以下のように増えます。(正直あまり項目について理解していません・・・。)
- $ + 10 CHF = $10 (レートが2:1の場合)
- $5 + $5 = $10
- $5 + $5 がMoneyを返す
- Moneyを変換して換算を行う
- Reduce(Bank, String)
Javaでは、Expression
と言うインターフェースが定義してありました。このインターフェースは、reduce
というメソッドを持ち、通貨を与えられると、Moneyを返すというメソッドを持ちます。reduceは計算の簡約を意味します。Moneyと足し算を表すSumと言うクラスがこのインターフェースを実装します。まずはじめに、Expression
について考えました。Elmは言語をなるべくシンプルに保つために関数型におけるインターフェースの役割をする型クラスを持ちません。とりあえず愚直にreduceメソッドを表すために、Expressionの定義をCurrency -> Money
としました。
src/Expression.elm
++ module Expression exposing (Expression)
++ import Money.Model exposing (Currency, Money)
++ type alias Expression =
++ Currency -> Money
続いて足し算を表す型Sumを考えました。Union Typesを使い、2つのMoneyを受け取り足し算を表す型を表現します。SumはExpressionを実装しなければならないので、reduce関数を定義します。Sumを受け取り、Expressionを返します。これでSum型のreduce
関数を表せますね。ちょうど第二引数がCurrency
を受け取るので、最終的に足し算を終えた新しいMoneyを返す関数、Expression型が完成します。SumはExpressionとして表現されなければならないので、expressionというリフト(持ち上げ)する関数も用意しました。しかし、ここではreduce関数を呼び出さなければならず、SumをExpressionとして持ち上げると簡約が行われてしまうという問題が発生しました。
src/Sum.elm
module Sum exposing (..)
import Money.Model exposing (Money(..), Currency)
import Expression exposing (..)
type Sum
= Sum Money Money
reduce : Sum -> Expression
reduce (Sum (Money augend _) (Money addend _)) to =
let
amount =
augend + addend
in
Money amount to
expression : Sum -> Expression
expression sum =
reduce sum
Sumと同様にMoneyもExpressionとして実装しなおしましょう。expressionは、通貨を渡したらMoneyを返す関数とします。MoneyをExpressionとして扱うだけなのでレートは考慮せず、そのままMoneyを返します。ちょっと怪しい実装な気がしますね?plus
関数は、Moneyを返していましたが、足し算を表す式(Sum)として返すようにしています。足し算はSumに委譲します。Money単体のときは為替をしなければなりませんが、とりあえずExpressionにリフトしてそのまま返す実装にしています。
src/Money/Money.elm
++ expression : Money -> Expression
++ expression money =
++ (\_ -> money)
-- plus : Money -> Money -> Money
++ plus : Money -> Money -> Sum
-- plus (Money amount1 currency) (Money amount2 _) =
-- Money (amount1 + amount2) currency
++ plus money1 money2 =
++ Sum money1 money2
++ reduce : Money -> Currency -> Expression
++ reduce money to =
++ expression money
BankはExpressionと変換先の通貨を受け取り最終結果のMoneyを返します。Expressionの型がCurrency -> Money
という型だったことを思い出してください。
module Bank exposing (..)
import Money.Model exposing (Money, Currency)
import Expression exposing (Expression)
reduce : Expression -> Currency -> Money
reduce source to =
source to
テストがいくつか増えて長くなったので変更や増えた部分のテストを列挙しておきます。
- Simple Addition
- 足し算結果をSumとBankでreduceし最終結果のMoneyを比較
- Plus Returns Sum
- plusだけを行い、足し算を表すSum型であることを確認
- Reduce Sum
- USドルからではなく、Sumを直接reduceに渡して想定した結果になるか確認
- Reduce Money
- MoneyをExpressionとしreduceし、そのままMoneyとなるか確認
describe "Simple Addition"
[ "addition1"
=> let
five =
dollar 5
sum =
Sum.reduce <| plus five five
reduced =
Bank.reduce sum USD
in
dollar 10
=== reduced
]
, describe "Plus Returns Sum"
[ "addition1"
=> let
five =
dollar 5
result =
plus five five
in
result
=== Sum five five
]
, describe "Reduce Sum"
[ "addition1"
=> let
sum =
Sum.reduce <| Sum (dollar 3) (dollar 4)
result =
Bank.reduce sum USD
in
dollar 7
=== result
]
, describe "Reduce Money"
[ "reduce1"
=> let
money =
Money.expression <| dollar 1
in
dollar 1
=== Bank.reduce money USD
]
いろいろツッコミどころは多そうですが、とりあえずの問題点としては、MoneyとSumがExpression型と言うのは結構無理がありそうです。また、SumとBankの二重のreduceが結構不自然ですね。
抽象レベルを引き上げる
Javaスタイルに合わせるのをやめて、Elmらしくリファクタリングしていきます。
まず、Expressionの型としてMoneyとSumを含めてしまいます。
src/Expression.elm
- type alias Expression =
- Currency -> Money
+ type Expression
+ = Expression (Currency -> Money)
+ | ExpressionMoney Money
+ | ExpressionSum Sum
plus関数は、SumをExpressionにリフトして返します。
src/Money/Money.elm
- plus : Money -> Money -> Sum
+ plus : Money -> Money -> Expression
plus money1 money2 =
- Sum money1 money2
+ ExpressionSum <| Sum money1 money2
Bankの関数はExpression型の分岐が増えたので、パターンマッチをおこない、それぞれの計算を行います。
src/Bank.elm
reduce : Expression -> Currency -> Money
reduce source to =
- source to
+ case source of
+ Expression expression ->
+ expression to
+
+ ExpressionMoney money ->
+ money
+
+ ExpressionSum sum ->
+ Sum.reduce sum to
だいぶ素直な実装になってきましたが、もう少し何とかなりそうです。例えば、Expression (Currency -> Money)
は、どこでも使われていません。reduceが既にその役割をになっています。Expresion Sum
は、Sum型を持ちますが、素直にSumをそっくり持ってきたほうがしっくり来そうな気がします。最後にもうひと頑張りしてみましょう。
最終段階
フォロワーさんに先ほどの実装を見せたところ、Expressionを非常にしっくり来る型として修正していただきました。早速見ていきましょう。
実に美しい型です。Moneyだけの場合はSingle Money
となり、Sumの場合には、2つの式(Expression)を受け取る再帰的データ構造として表すのが、かなりしっくりきました。ここの型は後の章で表されていました。現段階では、まだテストでMoneyを直接渡したいので、Sumの形で返してくれるヘルパー関数sum
を用意しておきました。単にmoneyをSingle Money
にリフトするだけですね。
type Expression
= Single Money
| Sum Expression Expression
sum : Money -> Money -> Expression
sum money1 money2 =
Sum (Single money1) (Single money2)
plus関数は計算として、より自然に見せるために$+
という演算子として定義しました。記号を使った関数で中置演算の形で書けるのはElmの利点と言えますね。ScalaはJVM言語ですが、同様に記号でメソッドを定義することが可能なのでオススメです。$+
関数は先ほどのヘルパー関数を呼び出してあげるだけでOKです。
($+) : Money -> Money -> Expression
($+) money1 money2 =
Expression.sum money1 money2
Money同士の足し算が自然にExpressionとして、表すことができたのでBank.reduce
に移りましょう。ポイントはSumが再帰的データ構造で表されていた点です。つまり、reduceも再帰的に解決されます。Singleの場合は、単にMoneyに変換してあげるだけで済みます。Sumの場合は、sum_というprivateなヘルパー関数に、exp1, exp2という名前で式を渡してあげます。式は再帰的にreduce関数に渡すことで最終的にはMoney型で返ることが保証されています。あとは、Money.amount
関数でamountを取得します。あとは式から抽出したamount同士を返してあげれば足し算されたMoneyの完成です。少し複雑ですが、再帰を利用したきれいな形で書けたので非常に嬉しくなりました。
reduce : Expression -> Currency -> Money
reduce source to =
case source of
Single (Money amount _) ->
Money amount to
Sum exp1 exp2 ->
Money (sum_ exp1 exp2 to) to
sum_ : Expression -> Expression -> Currency -> Int
sum_ exp1 exp2 to =
let
getAmount =
(\expression -> Money.amount <| reduce expression to)
in
(getAmount exp1) + (getAmount exp2)
最後にテスト全体を張ります。最初より格段に見やすいテストになったと思います。
module Tests exposing (..)
import Test exposing (..)
import TestExp exposing (..)
-- Test target modules
import Money.Money as Money exposing (..)
import Money.Model exposing (..)
import Bank exposing (..)
import Expression 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
]
, describe "Simple Addition"
[ "addition1"
=> let
five =
dollar 5
sum =
five $+ five
reduced =
Bank.reduce sum USD
in
dollar 10
=== reduced
]
, describe "Plus Returns Sum"
[ "addition1"
=> let
five =
dollar 5
result =
five $+ five
in
result
=== Expression.sum five five
]
, describe "Reduce Sum"
[ "addition1"
=> let
sum =
Expression.sum (dollar 3) (dollar 4)
result =
Bank.reduce sum USD
in
dollar 7
=== result
]
, describe "Reduce Money"
[ "reduce1"
=> let
money =
Money.expression <| dollar 1
in
dollar 1
=== Bank.reduce money USD
]
]
まとめ
前回の最後では、JavaとElmのコードが同じような形で表現できたので今回も同じような形でいけるかと移植を試みましたが、結果として汚い形になってしまいました。個人的な結論としては、中途半端な抽象化コードをElmで表すと、かなり厳しいコードになってしまうのではないかと思いました。思い返してみれば今までのElmコードも具象型が混じってしまうと表現が厳しいので抽象化された型になっている箇所がいくつか見受けられました。そこが関数型の融通の効かない部分と見ることもできますし、しっかり抽象化されて型が保証されていると見ることもできると思います。ただ、今回のElmの抽象化もまだ中途半端で、$+
関数がExpression型ではなくMoney
型で受け取っていたり改善の余地はまだ多く残されています。TDDシリーズも佳境に近づいてきましたが、これからどのように表せるか私自身もかなり楽しみです(上手くコード化できるか不安でもありますが)。