[JAVA] Développement piloté par les tests avec le langage fonctionnel Elm (Chapitre 15-16)

Ceci est une continuation de l'article précédent (https://qiita.com/ababup1192/items/efc8355281a656479f3a). La dernière fois, la mise en œuvre du calcul des taux était un problème, mais grâce au type de dictionnaire, il était étonnamment facile à mettre en œuvre. Ceci est la dernière tranche avec plus d'abstraction et des tests plus complexes. Merci à tous ceux qui ont vu jusqu'ici (le cas échéant)!

Chapitre 15

Enfin, nous implémenterons l'ajout entre d'autres devises que nous voulions à l'origine faire. Parallèlement à cela, nous augmenterons le degré d'abstraction de chaque fonction du type MoneyàExpression```.

Le test d'addition entre d'autres devises à ajouter est le suivant.

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

Par définition de Money, les fonctions dollar '' et franc '' qui retournent le type Money '' sont mordues dans Single '' et '' Monter au type Expression ''. Ensuite, toutes les fonctions écrites en type Money seront élevées au type Expression (c'est une construction assez importante). Dans ce cas, il est difficile de faire une correspondance de motifs avec toutes les fonctions, mais veuillez patienter car il y aura un moyen de le résoudre plus tard. Cependant, la fonction devise '' est Dans le cas de Sum (addition), la mise en œuvre précise que la devise du dernier terme est acquise se produira.

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

La fonction réduire``` de la banque renvoie également le type Money``` qui a été retourné comme type ```Expression```. La fonction montant``` a été migrée depuis Money car elle n'est utilisée que dans Bank (en supposant que le calcul du taux est effectué).

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)

Code entier à ce stade

Les livres n'ont pas fait une abstraction complète jusqu'à présent, mais Elm a une partie qui ne fonctionne pas à moins qu'elle ne soit complètement migrée, donc ça ressemble à ceci. Cette abstraction termine également le contenu du chapitre 16. Cependant, je voulais refactoriser les parties redondantes du code, donc j'aimerais aller un peu plus loin dans le chapitre 15.

J'ai élevé le contenu de Money``` à ```Expression```, donc je pense que déplacer le contenu vers Expression.elm est un module plus propre. De plus, changeons les heures en un opérateur spécial appelé $ * '' pour correspondre à $ + ''.

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)

Réécrivons le test avec un opérateur. On dirait un calcul naturel!

- => (dollar 5 |> times 2)
+ => (dollar 5 $* 2)
- => (dollar 5 |> times 3)
+ => (dollar 5 $* 3)

Concentrons-nous maintenant sur les fonctions d'expression. Vous pouvez voir qu'il existe deux types de fonctions. Une fonction qui Expression renvoie une nouvelle Expression, telle que `($ *)`, et un type qui converge vers un autre type, tel que `` monnaie``` ou `montant```. C'est une fonction.

($*) : Expression -> Int -> Expression

currency : Expression -> Currency
amount : Expression -> Int

De plus, en se concentrant sur la structure interne, elle peut être regroupée sous les deux fonctions suivantes. Ces deux fonctions sont dérivées du type de fonction, mais comme il existe certaines différences par rapport aux propriétés d'origine, une explication détaillée est omise.

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

Si vous utilisez ces deux fonctions, l'implémentation sera réalisée en une seule ligne! J'espère que vous l'apprécierez si vous essayez de l'appliquer aux deux fonctions et de voir comment il se développe.

($*) : Expression -> Int -> Expression
($*) exp multiplier =
map (\(Money amnt c) -> Money (amnt * multiplier) c) exp

La fonction de repli nécessite une valeur initiale, mais comme il n'y a pas de valeur initiale pour la devise, elle est provisoirement supposée être USD.

currency : Expression -> Currency
currency exp =
     fold (\(Money _ c) _ -> c) USD exp
amount exp =
     fold (\(Money amnt _) sum -> sum + amnt) 0 exp

Voyons comment seule la fonction de montant est développée. C'est un modèle très simple, mais il se développe comme suit:

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

C'est la fin du refactoring. À l'avenir, lors de l'extension de ce projet, s'il peut être appliqué au même modèle, vous pourrez écrire le processus simplement en passant une fonction.

Chapitre 16

Comme mentionné dans l'explication du chapitre 15. L'implémentation est déjà terminée, il ne vous reste plus qu'à ajouter un test. Grâce à l'opérateur, je suis heureux que le calcul de l'argent puisse être décrit naturellement.

+         , 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
+            ]

Vive le bon travail. Ce sera Mise en œuvre finale.

Résumé

Nous l'avons élevé du type Money au type Expression et l'avons résolu immédiatement jusqu'à l'implémentation finale. De plus, en faisant attention à la nature du traitement, j'ai pu raccourcir la fonction Expression en définissant des fonctions appelées map and fold. De plus, à travers cette série Tdd Elm, le résumé général et la technique de chute de code orienté objet dans le code Elm sont disponibles dans Elm Advent Calendar 2017. Je voudrais le poster. Si cela ne vous dérange pas, je vous reverrai. Merci beaucoup d'avoir lu l'article jusqu'à présent!

Postscript

Après avoir posté, j'ai reçu un PR de @miyamo_madoka. Il n'y a pas de parties suspectes de l'implémentation, et l'implémentation est très rafraîchissante. La grande erreur dans ma mise en œuvre est que je me soucie du degré d'abstraction.Le dernier type de réduire: Banque-> Devise-> Expression-> Expression de la Banque est `` Expression Il devait rester. Je pense que de nombreux problèmes ont été résolus en revenant au type Money comme Réduire: Banque-> Devise-> Expression-> Argent ''. Par exemple, il est naturel que la devise currency``` ne puisse obtenir que le type Money```, et dans le type abstrait `Expression, il y a des cas d'addition entre d'autres devises. Le problème de ne pas être déterminé de manière unique a été résolu (ainsi que le `` montant). De plus, après avoir reçu le PR, j'ai fait quelques refactories par rapport à mon implémentation et en ai vraiment fait la dernière implémentation. Merci beaucoup d'avoir souligné. Comme c'est un gros problème, j'aimerais terminer avec toutes les implémentations.

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

Développement piloté par les tests avec le langage fonctionnel Elm (Chapitre 15-16)
Développement piloté par les tests avec le langage fonctionnel Elm (Chapitre 5-7)
Développement piloté par les tests avec le langage fonctionnel Elm
Implémenter un test piloté par table dans Java 14