Java
TDD
テスト
Elm
関数型プログラミング

関数型言語Elmでテスト駆動開発(第5~7章)

More than 1 year has passed since last update.

前回の記事の続きになります。前回はElmのデータ型の強力さ(特に等価比較)を確認しました。今回の内容ではどうでしょうか。見ていきましょう。

第5章

今まで扱える通貨はUSドルだけでしたが、フラン(Franc)を追加しましょう。また前回コメントで指摘を受けた部分で修正があります。module Dollar exposing (..)と記述した場合には、実はAmountを書き換えることはできませんが取得はできてしまうため、privateにはなっていません。module Dollar exposing (Dollar)と記述した場合にはprivateにすることができるのですが、そうした場合Dollar 5のような値コンストラクタも記述できなくなってしまいます。回避するには、dollar : Int -> Dollarいわゆるファクトリパターンな関数を公開してあげる必要があります。本書を読み進めたところファクトリパターンは後の章で出てくる予定なので、そこまでpublic(書き換えは不可)なamountで行かせてもらいます。

  • \$5 + 10 CHF=\$10 (レートが2:1の場合)
  • $5 * 2 = 10
  • amountをprivateにする
  • Dollarの副作用をどうする?(最初から)
  • Moneyの丸め処理をどうする?
  • equals()(最初から)
  • hashCode()(最初から)
  • nullと等価性比較
  • 他のオブジェクトと等価比較(最初から)
  • 5 CHF * 2 = 10 CHF

まずは、テストだけを書きましょう。当然コンパイルエラーです。この時点で単純な通貨の比較は前回確認したとおり、言語で保証されているのでテストケースからは外しています。timesは名前が被ってしまうので、DollarとFrancモジュールで使い分けています。

tests/Test.elm

all : Test
all =
    describe "Money Test"
        [ describe "Dollar"
            [ "Multiplication1"
                => (Dollar 5 |> Dollar.times 2)
                === Dollar 10
            , "Multiplication2"
                => (Dollar 5 |> Dollar.times 3)
                === Dollar 15
            ]
        , describe "Franc"
            [ "Multiplication1"
                => (Franc 5 |> Franc.times 2)
                === Franc 10
            , "Multiplication2"
                => (Franc 5 |> Franc.times 3)
                === Franc 15
            ]
        ]

本書の通り非常に心苦しいですがDollarの定義をコピーアンドペーストをして、フランを実装しましょう。単純な置換で済みます。

src/Franc.elm

module Franc exposing (Franc(..), times)


type alias Amount =
    Int


type Franc
    = Franc Amount


times : Int -> Franc -> Franc
times multiplier (Franc amount) =
    Franc <| multiplier * amount


amount : Franc -> Amount
amount (Franc amount) =
    amount

無事テストが通ったのでTODOが以下のようになります。

  • \$5 + 10 CHF=\$10 (レートが2:1の場合)
  • $5 * 2 = 10
  • amountをprivateにする
  • Dollarの副作用をどうする?(最初から)
  • Moneyの丸め処理をどうする?
  • equals()(最初から)
  • hashCode()(最初から)
  • nullと等価性比較
  • 他のオブジェクトと等価比較(最初から)
  • 5 CHF * 2 = 10 CHF
  • DollarとFrancの重複
  • equalsの一般化
  • timesの一般化

第6章

この章の書籍では、DollarとFrancの重複を消すために継承をおこないます。elmでは直接継承という概念はありませんが、どのように表現していくか、そしてどういう効果があるか個人的におもしろい結果になったと思います。

書籍のMoneyクラスをより正確に移植するには、以下のような定義になると思います。

type Dollar = Dollar Amount
type Franc = Franc Amount
type Money = Dolalr | Franc

しかし、OOPの継承を再現するときに、elmではわざわざ上のようなコードは書かないと思いますので、以下のようにUnion Typesの形で書かせてもらいました。Javaではクラスを分けて、それぞれの定義を記述すると思います。しかし、今回の場合にはtimesamount関数を見てもらうと分かる通り、Money型として引数を受け取ったり、返したりし、パターンマッチで分岐していくことで、それぞれの分岐した型(正確には値)に対しての処理を記述していくことになります。Javaに慣れている方は、instanceofやキャストによる分岐処理の嫌な気配を感じ取るかもしれません。しかし安心してください。コンパイラによりMoney型以外の分岐型は当然エラーを吐いてくれますし、パターンマッチ時にamountも取得することができるのでキャスト(とそれに伴う実行時エラー)が発生することもありません。

src/Money.elm

type alias Amount =
    Int


type Money
    = Dollar Amount
    | Franc Amount


times : Int -> Money -> Money
times multiplier money =
    case money of
        Dollar amount ->
            Dollar <| multiplier * amount

        Franc amount ->
            Franc <| multiplier * amount


amount : Money -> Amount
amount money =
    case money of
        Dollar amount ->
            amount

        Franc amount ->
            amount

今回私がElmの強力さを感じた点は、timesの型Int -> Money -> Moneyです。Javaで実装をおこなった場合は、以下のようなシグネチャになるはずです。つまり、それぞれのクラスで定義され、それぞれの具象型として返してしまうのです。つまりMoney型にamountフィールドを移し、Money型として値を返さない限りはtimesの重複を消すことはできないのです。この実装は10章での話となってしまいます。現状DollarとFrancの型の制約上完全にはtimesの重複を消していることにはなりませんが、10章に近い実装を既に6章の実装のついでに終えてしまっているのは、すごいパワーを感じます。

// Dollar.java
Dollar times(int multiplier);

// Franc.java
Franc times(int multiplier);

前回に引き続き強力なのは、等価比較です。書籍のJavaでのequalsの実装は以下のようになります。Money型に無理やりキャストし、amount同士の比較をしています。当然DollarとFranc同士の比較が成功してしまいます。また、本質ではありませんが、インスタンスの型をしっかり比較しなければ、Money型以外の型が入りこんだ時にキャスト失敗の実行時エラーが発生します。type Money = Dollar Amount | Franc Amountの書き方では、DollarとFrancの比較は絶対成功しませんし、別な型は比較演算子に渡した途端コンパイルエラーになります。型に守られていることは非常に嬉しいですね。

// Money.java
public boolean equals(Object object) {
    Money money = (Money) object;
    return amount == money.amount;
}

最後にテストケースは、このように変わります。Moneyモジュールのみを読み込めば良くなり、timesは一般化されているのがわかります。

module Tests exposing (..)

import Test exposing (..)
import TestExp exposing (..)


-- Test target modules

import Money exposing (..)


all : Test
all =
    describe "Money Test"
        [ describe "Dollar"
            [ "Multiplication1"
                => (Dollar 5 |> times 2)
                === Dollar 10
            , "Multiplication2"
                => (Dollar 5 |> times 3)
                === Dollar 15
            ]
        , describe "Franc"
            [ "Multiplication1"
                => (Franc 5 |> times 2)
                === Franc 10
            , "Multiplication2"
                => (Franc 5 |> times 3)
                === Franc 15
            ]
        ]

今回のTODOリストは以下のようになります。内容が進めば進むほど、関数型の力が如実に現れていきます(打ち消し線が書いてある項目すべて)。つまり、安全性を確かめるテストや直感的にそうなって欲しい挙動を言語が勝手に補ってくれています

  • \$5 + 10 CHF=\$10 (レートが2:1の場合)
  • $5 * 2 = 10
  • amountをprivateにする
  • Dollarの副作用をどうする?(最初から)
  • Moneyの丸め処理をどうする?
  • equals()(最初から)
  • hashCode()(最初から)
  • nullと等価性比較
  • 他のオブジェクトと等価比較(最初から)
  • 5 CHF * 2 = 10 CHF
  • DollarとFrancの重複
  • equalsの一般化(最初から)
  • timesの一般化(△)
  • FrancとDollarを比較する(最初から)

最終的なコード全体

第7章

すみません。追記情報です。既にUSドルとフランの比較は6章時点で終わっていました。

まとめ

6章の最後で触れましたが、関数型はJavaなどのOOPや手続き型に比べて制約が多いことが特徴として挙げられます。しかしその制約とは、決して自由を奪うための制約ではありません。キャストではなくコンパイラが型をしっかりチェックしてくれることで、抽象度は引き上げられ安全に分岐処理が進められます。さらに、しなくてもよい冗長なコードやそれに対するテストコードを書く必要がありません。これはロジックコードに集中し、アプリケーションの質を向上することにも繋がります。書籍「テスト駆動開発」をまだ買われていない方は是非購入しJavaとの比較を是非してみてください。Elmの魅力にきっと気づくはずです!