Test Driven Development in Functional Language Elm (Chapter 15-16)

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)!

Chapter 15

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 toExpression```.

-[] ** \ $ 5 + 10 CHF = \ $ 10 (when rate is 2: 1) **

The test of addition between other currencies to be added is as follows.

tests/Tests.elm

+ 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.

src/Money/Money.elm

- 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. Theamount``` function has been migrated from Money because it is used only in Bank (assuming rate calculation is done).

src/Bank.elm

- 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)

Whole code at this point

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 $ +.

src/Expression.elm

+ ($+) : 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 `` `currencyandamount``` 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.

Chapter 16

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.

Summary

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!

Postscript

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.

src/Money/Model.elm

module Money.Model exposing (Money(..), Amount, Currency(..))


type alias Amount =
    Int


type Currency
    = USD
    | CHF


type Money
    = Money Amount Currency

src/Money/Money.elm

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

src/Bank.elm

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

src/Expression.elm

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 $*

tests/Tests.elm

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

Test Driven Development in Functional Language Elm (Chapter 15-16)
Test Driven Development in Functional Language Elm (Chapter 5-7)
Test-driven development in the functional language Elm
Implement Table Driven Test in Java 14
Ruby Study Memo (Test Driven Development)
[Ruby on Rails Tutorial] Error in the test in Chapter 3