[JAVA] Testgetriebene Entwicklung mit der Funktionssprache Elm (Kapitel 15-16)

Dies ist eine Fortsetzung des vorherigen Artikels (https://qiita.com/ababup1192/items/efc8355281a656479f3a). Beim letzten Mal war die Implementierung der Ratenberechnung ein Problem, aber dank des Wörterbuchtyps war die Implementierung unerwartet einfach. Dies ist die letzte Rate mit weiterer Abstraktion und komplexeren Tests. Vielen Dank an alle, die dies bisher gesehen haben (falls vorhanden)!

Kapitel 15

Schließlich werden wir die Addition zwischen anderen Währungen implementieren, die wir ursprünglich machen wollten. Gleichzeitig werden wir den Abstraktionsgrad jeder Funktion vom Typ "Geld" auf "Ausdruck" erhöhen.

Der Test der Addition zwischen anderen hinzuzufügenden Währungen ist wie folgt.

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

Per Definition von Geld werden die Funktionen "Dollar" und "Franken", die den Typ "Geld" zurückgeben, in die "Single" gebissen Auf Ausdruck erhöhen. Dann werden alle im Money-Typ geschriebenen Funktionen auf den Expression-Typ angehoben (dies ist eine ziemlich große Konstruktion). In diesem Fall ist es schwierig, einen Mustervergleich mit allen Funktionen durchzuführen. Warten Sie jedoch, da es später eine Möglichkeit gibt, das Problem zu lösen. Die Funktion `` `Währung ist jedoch Im Fall von Summe (Addition) werden die Implementierungsdetails, dass die Währung der letzteren Laufzeit erworben wird, auftreten.

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

Die `Redu``` `Funktion der Bank gibt auch das zurück, was den` Money Typ als `` `Expression Typ zurückgegeben hat. Die Funktion `` `Betrag``` wurde von Money migriert, da sie nur in der Bank verwendet wird (vorausgesetzt, die Zinsberechnung wird durchgeführt).

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)

Vollständiger Code zu diesem Zeitpunkt

In dem Buch habe ich noch keine vollständige Abstraktion durchgeführt, aber in Elm gab es einige Teile, die nicht funktionieren würden, wenn ich nicht in einer vollständigen Form migriert wäre, also wurde es so. Diese Abstraktion beendet auch den Inhalt von Kapitel 16. Ich wollte jedoch die redundanten Teile des Codes umgestalten, daher möchte ich in Kapitel 15 noch etwas weiter gehen.

Da ich den Inhalt von "Geld" auf "Ausdruck" erhöht habe, halte ich das Verschieben des Inhalts nach Expression.elm für ein saubereres Modul. Ändern wir auch die Zeiten in einen speziellen Operator namens `$ *`, um mit `$ +` übereinzustimmen.

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)

Schreiben wir den Test mit einem Operator neu. Sieht aus wie eine natürliche Berechnung!

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

Konzentrieren wir uns nun auf Ausdrucksfunktionen. Sie können sehen, dass es zwei Arten von Funktionen gibt. Eine Funktion, bei der Expression einen neuen Ausdruck zurückgibt, z. B. `($ *)`, und einen Typ, der zu einem anderen Typ konvergiert, z. B. `Währung``` oder` Betrag``` Es ist eine Funktion.

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

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

Wenn man sich auf die interne Struktur konzentriert, kann man sie in die folgenden zwei Funktionen einteilen. Diese beiden Funktionen werden vom Funktionstyp abgeleitet. Da jedoch einige Unterschiede zu den ursprünglichen Eigenschaften bestehen, wird auf eine ausführliche Erläuterung verzichtet.

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

Wenn Sie diese beiden Funktionen verwenden, wird die Implementierung in nur einer Zeile abgeschlossen! Ich hoffe, es wird Ihnen gefallen, wenn Sie versuchen, es auf die beiden Funktionen anzuwenden und zu sehen, wie es sich erweitert.

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

Die Fold-Funktion erfordert einen Anfangswert, aber da es keinen Anfangswert für die Währung gibt, wird vorläufig angenommen, dass es sich um USD handelt.

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

Mal sehen, wie nur die Betragsfunktion erweitert wird. Es ist ein sehr einfaches Muster, aber es erweitert sich wie folgt:

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

Dies ist das Ende des Refactorings. Wenn Sie dieses Projekt in Zukunft auf dasselbe Muster anwenden können, können Sie den Prozess in Zukunft einfach durch Übergeben einer Funktion schreiben.

Kapitel 16

Wie in der Erklärung in Kapitel 15 erwähnt. Die Implementierung ist bereits abgeschlossen. Sie müssen also nur einen Test hinzufügen. Dank des Betreibers bin ich froh, dass die Berechnung des Geldes natürlich beschrieben werden kann.

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

Ist Prost auf gute Arbeit. Es wird [Endgültige Implementierung] sein (https://github.com/ababup1192/elm-tdd/tree/session15).

Zusammenfassung

Wir haben es vom Typ "Geld" zum Ausdruck "Typ" angehoben und es bis zur endgültigen Implementierung sofort gelöst. Unter Berücksichtigung der Art der Verarbeitung konnte ich die Ausdrucksfunktion verkürzen, indem ich Funktionen definierte, die als Map and Fold bezeichnet wurden. Durch diese Tdd Elm-Reihe sind die Gesamtübersicht und die Technik zum Ablegen von objektorientiertem Code in Elm-Code in Elm Advent Calendar 2017 verfügbar. Ich würde es gerne posten. Wenn es Ihnen nichts ausmacht, sehen wir uns dort wieder. Vielen Dank, dass Sie den Artikel bisher gelesen haben!

Nachtrag

Nach dem Posten erhielt ich eine PR von @miyamo_madoka. Es gibt keine verdächtigen Teile der Implementierung und die Implementierung ist sehr erfrischend. Der große Fehler bei meiner Implementierung ist, dass mir der Abstraktionsgrad am Herzen liegt. Die letzte Art der Reduzierung der Bank: Bank-> Währung-> Ausdruck-> Ausdruck ist `` Ausdruck Es sollte bleiben. Ich denke, dass viele Probleme gelöst wurden, indem man zum Geldtyp wie "Reduzieren: Bank-> Währung-> Ausdruck-> Geld" zurückkehrte. Zum Beispiel ist es natürlich, dass die Währung `` Währung``` nur den Typ Geld``` erwerben kann, und im abstrahierten Typ `Ausdruck``` gibt es Fälle, in denen andere Währungen addiert werden. Das Problem, nicht eindeutig bestimmt zu werden, wurde gelöst (sowie "Betrag"). Außerdem habe ich nach Erhalt der PR einige Refactories im Vergleich zu meiner Implementierung erstellt und es wirklich zur letzten Implementierung gemacht. Vielen Dank für den Hinweis. Da es eine große Sache ist, möchte ich mit allen Implementierungen fertig werden.

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

Testgetriebene Entwicklung mit der Funktionssprache Elm (Kapitel 15-16)
Testgetriebene Entwicklung mit der Funktionssprache Elm (Kapitel 5-7)
Testgetriebene Entwicklung mit der Funktionssprache Elm
Implementieren Sie einen tabellengesteuerten Test in Java 14