This is a continuation of the previous article (https://qiita.com/ababup1192/items/efc8355281a656479f3a). Last time, the implementation of rate calculation was an issue, but thanks to the dictionary type, it was unexpectedly easy to implement. This is the final installment by further abstraction and more complex tests. Thank you to everyone who has seen this far (if any)!
Finally, we will implement the addition between other currencies that we originally wanted to do. Along with that, we will raise the level of abstraction of each function from `` `Moneytype to
Expression```.
-[] ** \ $ 5 + 10 CHF = \ $ 10 (when rate is 2: 1) **
The test of addition between other currencies to be added is as follows.
+ describe "Mixed Addition"
+ [ "CHF ~> USD 2"
+ => let
+ fiveBucks =
+ dollar 5
+
+ tenFrancs =
+ franc 10
+
+ bank =
+ Bank.bank |> Bank.addRate (CHF ~> USD) 2
+
+ result =
+ Bank.reduce (fiveBucks $+ tenFrancs) USD bank
+ in
+ dollar 10 === result
+ ]
By definition of Money, the `dollar``` and
franc``` functions that returned the ``
Money type are bitten into the `` `Single
and `` ` Raise to Expressiontype. Then, all the functions written in Money type will be raised to Expression type (it is quite a big construction). In that case, it is troublesome to do pattern matching with all functions, but please wait as there will be a way to solve it later. However, the
currency``` function is
In the case of Sum (addition), the implementation details that the currency of the latter term is acquired will occur.
- dollar : Amount -> Money
+ dollar : Amount -> Expression
dollar amount =
- Money amount USD
+ Single <| Money amount USD
- franc : Amount -> Money
+ franc : Amount -> Expression
franc amount =
- Money amount CHF
+ Single <| Money amount CHF
- times : Int -> Money -> Money
- times multiplier (Money amount currency) =
- Money (multiplier * amount) currency
+ times : Expression -> Int -> Expression
+ times exp multiplier =
+ case exp of
+ Single (Money amnt crncy) ->
+ Single <| Money (amnt * multiplier) crncy
+
+ Sum exp1 exp2 ->
+ let
+ mlp_ e =
+ times e multiplier
+ in
+ Sum (mlp_ exp1) (mlp_ exp2)
- ($+) : Money -> Money -> Expression
+ ($+) : Expression -> Expression -> Expression
- ($+) money1 money2 =
- Expression.sum money1 money2
+ ($+) exp1 exp2 =
+ Sum exp1 exp2
- amount : Money -> Amount
- amount (Money amount _) =
- amount
- currency : Money -> Currency
+ currency : Expression -> Currency
- currency (Money _ currency) =
- currency
+ currency expression =
+ case expression of
+ Single (Money _ currency) ->
+ currency
+ Sum _ exp2 ->
+ currency exp2
Bank's `reduce
function also returns the `Money``` type that was returned as the
`Expressiontype. The
amount``` function has been migrated from Money because it is used only in Bank (assuming rate calculation is done).
- reduce : Expression -> Currency -> Bank -> Money
+ reduce : Expression -> Currency -> Bank -> Expression
reduce source to bank =
case source of
- Single (Money amount currency) ->
+ Single (Money amnt currency) ->
let
r =
rate (currency ~> to) bank
in
- Money (amount // r) to
+ Single <| Money (amnt // r) to
Sum exp1 exp2 ->
- Money (sum_ exp1 exp2 to bank) to
+ Single <| Money (sum_ exp1 exp2 to bank) to
+
+
+ amount : Expression -> Int
+ amount expression =
+ case expression of
+ Single (Money amnt _) ->
+ amnt
+
+ Sum exp1 exp2 ->
+ (amount exp1) + (amount exp2)
In the book, I haven't done a complete abstraction so far, but in Elm, there were some parts that didn't work unless I completely migrated, so it became like this. This abstraction also ends the content of Chapter 16. However, I wanted to refactor the redundant parts of the code, so I'd like to go a little further in Chapter 15.
I've raised the content from `Money``` to ```Expression``` so far, so I felt that moving the content to Expression.elm would make it cleaner as a module. Also, change times to a special operator called ``` $ *`
to match $ +
.
+ ($+) : Expression -> Expression -> Expression
+ ($+) exp1 exp2 =
+ Sum exp1 exp2
+
+
+ ($*) : Expression -> Int -> Expression
+ ($*) exp multiplier =
+ case exp of
+ Single (Money amnt crncy) ->
+ Single <| Money (amnt * multiplier) crncy
+
+ Sum exp1 exp2 ->
+ let
+ mlp_ e =
+ e $* multiplier
+ in
+ Sum (mlp_ exp1) (mlp_ exp2)
+
+
+ currency : Expression -> Currency
+ currency expression =
+ case expression of
+ Single (Money _ currency) ->
+ currency
+
+ Sum _ exp2 ->
+ currency exp2
+
+
+ amount : Expression -> Int
+ amount expression =
+ case expression of
+ Single (Money amnt _) ->
+ amnt
+
+ Sum exp1 exp2 ->
+ (amount exp1) + (amount exp2)
Let's rewrite the test with an operator. Looks like a natural calculation!
- => (dollar 5 |> times 2)
+ => (dollar 5 $* 2)
- => (dollar 5 |> times 3)
+ => (dollar 5 $* 3)
Now let's focus on Expression functions. You can see that there are two types of functions. Functions such as `($ *)`
that Expression returns a new Expression, and types that converge to different types such as `` `currencyand
amount``` It is a function.
($*) : Expression -> Int -> Expression
currency : Expression -> Currency
amount : Expression -> Int
Furthermore, paying attention to the internal structure, it can be summarized as the following two functions. These two functions are derived from functional types, but since there are some differences from their original properties, detailed explanations are omitted.
map : (Money -> Money) -> Expression -> Expression
map f exp =
case exp of
Single money ->
Single <| f money
Sum exp1 exp2 ->
Sum (map f exp1) (map f exp2)
fold : (Money -> a -> a) -> a -> Expression -> a
fold f init exp =
case exp of
Single money ->
f money init
Sum exp1 exp2 ->
(amount exp1) + (amount exp2)
fold f (fold f init exp1) exp2
If you use these two functions, the implementation will be completed in just one line! I hope you can enjoy it by applying it to the two functions and seeing how it expands.
($*) : Expression -> Int -> Expression
($*) exp multiplier =
map (\(Money amnt c) -> Money (amnt * multiplier) c) exp
The fold function requires an initial value, but since there is no initial value for currency, we have assumed it to be USD.
currency : Expression -> Currency
currency exp =
fold (\(Money _ c) _ -> c) USD exp
amount exp =
fold (\(Money amnt _) sum -> sum + amnt) 0 exp
Let's see how only the amount function is expanded. It's a very simple pattern, but it expands as follows:
amount Single(Money 10 USD)
= fold (\(Money amnt _) sum -> sum + amnt) 0 Single (Money 10 USD)
= (\(Money amnt _) sum -> sum + amnt) (Money 10 USD) 0
= (\(Money 10 _) 0 -> 0 + 10)
= 0 + 10
= 10
This is the end of the refactoring. In the future, when extending this project, if it can be applied to the same pattern, you can write the process just by passing a function.
As mentioned in the explanation in Chapter 15. The implementation is already done, so all you have to do is add a test. Thanks to the operators, I'm happy that the calculation of money can be described naturally.
+ , describe "Sum Plus Money"
+ [ "($5 + 10 CHF) + $5"
+ => let
+ fiveBucks =
+ dollar 5
+
+ tenFrancs =
+ franc 10
+
+ bank =
+ Bank.bank |> Bank.addRate (CHF ~> USD) 2
+
+ sum =
+ Bank.reduce ((fiveBucks $+ tenFrancs) $+ fiveBucks) USD bank
+
+ result =
+ Bank.reduce sum USD bank
+ in
+ dollar 15 === result
+ ]
+ , describe "Sum Times"
+ [ "($5 + 10 CHF) * 2"
+ => let
+ fiveBucks =
+ dollar 5
+
+ tenFrancs =
+ franc 10
+
+ bank =
+ Bank.bank |> Bank.addRate (CHF ~> USD) 2
+
+ sum =
+ Bank.reduce ((fiveBucks $+ tenFrancs) $* 2) USD bank
+
+ result =
+ Bank.reduce sum USD bank
+ in
+ dollar 20 === result
+ ]
thank you for your hard work. It will be Final Implementation.
I raised it from Money to Expression type and solved it at once until the final implementation. Also, paying attention to the nature of processing, I was able to shorten the Expression function by defining the functions map and fold. Also, through this Tdd Elm series, the overall summary and the technique of dropping object-oriented code into Elm code are available in Elm Advent Calendar 2017. I would like to post it. If you don't mind, I'll see you there again. Thank you so much for reading the article so far!
After posting, I received a PR from @miyamo_madoka. There are no suspicious parts of the implementation, and the implementation is very refreshing. The big mistake in my implementation is that I care about the degree of abstraction. The last type of Bank's `reduce: Bank-> Currency-> Expression-> Expression``` is ```Expression```. It was to remain. I think that many problems have been solved by returning to the Money type like
reduce: Bank-> Currency-> Expression-> Money```. For example, it is natural that the currency ``
currency can only get the `` `Money
type, and in the abstracted Expression
type, there are cases of addition between other currencies. The problem of not being uniquely determined has been resolved (as well as amount
). In addition, after receiving the PR, I made some refactorings compared to my implementation and made it really the last implementation. Thank you very much for pointing out. Since it's a big deal, I'd like to finish with all the implementations.
module Money.Model exposing (Money(..), Amount, Currency(..))
type alias Amount =
Int
type Currency
= USD
| CHF
type Money
= Money Amount Currency
module Money.Money exposing (dollar, franc, currency, amount)
import Money.Model exposing (Money(..), Amount, Currency(..))
dollar : Amount -> Money
dollar amount =
Money amount USD
franc : Amount -> Money
franc amount =
Money amount CHF
currency : Money -> Currency
currency (Money _ c) =
c
amount : Money -> Amount
amount (Money a _) =
a
module Bank exposing (bank, rate, Bank, addRate, (~>), reduce)
import Money.Model exposing (Currency, Money(..))
import Money.Money as Money
import EveryDict exposing (EveryDict)
import Expression exposing (Expression(..))
type alias Bank =
EveryDict ( Currency, Currency ) Int
bank : Bank
bank =
EveryDict.empty
(~>) : a -> b -> ( a, b )
(~>) a b =
( a, b )
addRate : ( Currency, Currency ) -> Int -> Bank -> Bank
addRate fromTo rate =
EveryDict.insert fromTo rate
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 : Bank -> Currency -> Expression -> Money
reduce bank to exp =
case exp of
Single (Money amnt source) ->
let
r =
rate (source ~> to) bank
in
Money (amnt // r) to
Sum exp1 exp2 ->
let
amnt_ e =
Money.amount <| reduce bank to e
a1 =
amnt_ exp1
a2 =
amnt_ exp2
in
Money (a1 + a2) to
module Expression exposing (Expression(..), single, ($+), ($*))
import Money.Model exposing (Money(..), Currency)
type Expression
= Single Money
| Sum Expression Expression
single : Money -> Expression
single =
Single
($+) : Expression -> Expression -> Expression
($+) =
Sum
($*) : Expression -> Int -> Expression
($*) exp multiplier =
map (\(Money amnt c) -> Money (amnt * multiplier) c) exp
map : (Money -> Money) -> Expression -> Expression
map f exp =
case exp of
Single money ->
Single <| f money
Sum exp1 exp2 ->
Sum (map f exp1) (map f exp2)
infixl 6 $+
infixl 7 $*
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"
=> ((single <| dollar 5) $* 2)
=== (single <| dollar 10)
, "Multiplication2"
=> ((single <| dollar 5) $* 3)
=== (single <| dollar 15)
, "Currency"
=> (currency <| dollar 5)
=== USD
]
, describe "Franc"
[ "Multiplication1"
=> ((single <| franc 5) $* 2)
=== (single <| franc 10)
, "Multiplication2"
=> ((single <| franc 5) $* 3)
=== (single <| 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 =
single <| dollar 5
sum =
five $+ five
reduced =
Bank.reduce bank USD sum
in
dollar 10
=== reduced
]
, describe "Reduce Sum"
[ "addition1"
=> let
sum =
(single <| dollar 3) $+ (single <| dollar 4)
bank =
Bank.bank |> Bank.addRate (CHF ~> USD) 2
result =
Bank.reduce bank USD sum
in
dollar 7
=== result
]
, describe "Reduce Money"
[ "reduce1"
=> let
single_ =
single <| dollar 1
bank =
Bank.bank |> Bank.addRate (CHF ~> USD) 2
in
Bank.reduce bank USD single_ === dollar 1
]
, describe "Reduce Bank with Different Currency"
[ "CHF ~> USD 2"
=> let
twoCHF =
single <| franc 2
bank =
Bank.bank |> Bank.addRate (CHF ~> USD) 2
result =
Bank.reduce bank USD twoCHF
in
result === dollar 1
]
, describe "Identity rate"
[ "USD ~> USD 1"
=> let
bank =
Bank.bank |> Bank.addRate (CHF ~> USD) 2
in
Bank.rate (USD ~> USD) bank
=== 1
]
, describe "Mixed Addition"
[ "CHF ~> USD 2"
=> let
fiveBucks =
single <| dollar 5
tenFrancs =
single <| franc 10
bank =
Bank.bank |> Bank.addRate (CHF ~> USD) 2
result =
(fiveBucks $+ tenFrancs)
|> Bank.reduce bank USD
in
dollar 10 === result
]
, describe "Sum Plus Money"
[ "($5 + 10 CHF) + $5"
=> let
fiveBucks =
single <| dollar 5
tenFrancs =
single <| franc 10
bank =
Bank.bank |> Bank.addRate (CHF ~> USD) 2
result =
((fiveBucks $+ tenFrancs) $+ fiveBucks)
|> Bank.reduce bank USD
in
dollar 15 === result
]
, describe "Sum Times"
[ "($5 + 10 CHF) * 2"
=> let
fiveBucks =
single <| dollar 5
tenFrancs =
single <| franc 10
bank =
Bank.bank |> Bank.addRate (CHF ~> USD) 2
result =
((fiveBucks $+ tenFrancs) $* 2)
|> Bank.reduce bank USD
in
dollar 20 === result
]
]
Recommended Posts