業務で使う関数型言語 (F# 編) 〜 レコードで値オブジェクトを簡単に作る

業務で使う関数型言語の第一回は、レコードについてです。
この記念すべき第一回は、F# Advent Calendar 2011 の1日目の参加エントリーにもなっています。


レコードとオブジェクト指向プログラミング言語におけるクラスを比べながら、どういう場面でレコードが使えるのか見ていきましょう。

クラスのおさらい

まずは、クラスについてのおさらいです。
JavaC# などの言語に見られるクラスには、以下のような特徴があります。

  • いくつかのデータをまとめることができる
  • メンバに対して可視性 (もしくは accessibility) を設けることができる
  • 他のクラスを継承することができる
  • 継承先で振る舞いを変更することができる
  • インターフェイスを実装することができる

他にもあるでしょうけど、とりあえずこのくらいで。
次はレコードです。

レコードの特徴

レコードを、クラスの特徴と同じような観点から見た場合、

  • いくつかのデータをまとめることができる
  • データ毎に可視性を設定することはできず、全て公開される
  • 他の型を継承することはできない
  • インターフェイスを実装することはできない

これだけ見ると、レコードにはオブジェクト指向プログラマにとっては受け入れがたい制限があるように思えます。
なぜこんな制限を受け入れることができるのでしょうか?

レコードはクラスではない

レコードはクラスと似ている部分もありますが、クラスそのものではなく、クラスが担っている一部の機能を提供するものと考えてください。
レコードは、オブジェクト指向プログラミングにおける値オブジェクトのためのものなのです。


値オブジェクトは業務でも頻繁に使用しますが、クラスで値オブジェクトを作るのは結構面倒です。
そのため、本来値オブジェクトを作るべき場面で値オブジェクトを作らず、プリミティブ型や文字列型などを使ってしまうこともよくあると思います。
例えば、従業員の名前と電話番号を扱うために Employee クラスを導入すべき場面で、2 つの String で済ませてしまったりとか、そんな経験ありませんか?


それに対してレコードは値オブジェクトにフォーカスを当て、特化していますので、簡単に作ることができます。
以下では値オブジェクトという観点からレコードを見ていきます*1

値オブジェクトとしてのレコード

例えば Java で同値比較を行えるようにするためには、equals メソッドを適切にオーバーライドする必要があります。
更に、ハッシュを用いるコレクションのキーとして使えるようにするために、hashCode も適切にオーバーライドしなければなりません。


テストやデバッグを考えると、toString もオーバーライドしておいた方が何かと便利でしょう。
これらをすべて満たす値オブジェクトを Java のクラスを用いて作ろうとすると、以下のようになります。

// Javaの値オブジェクトの例
public final class Employee {
    public final String name;
    public final PhoneNumber phoneNum;
    public Employee(String name, PhoneNumber phoneNum) {
        this.name = name;
        this.phoneNum = phoneNum;
    }
    @Override public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return prime * result + ((phoneNum == null) ? 0 : phoneNum.hashCode());
    }
    @Override public boolean equals(Object obj) {
        if (obj == null || !(obj instanceof Employee))
            return false;
        Employee other = (Employee) obj;

        if (name == null)
            if (other.name != null) return false;
        else if (!name.equals(other.name))
            return false;

        if (phoneNum == null)
            if (other.phoneNum != null) return false;
        else if (!phoneNum.equals(other.phoneNum))
            return false;

        return true;
    }
    @Override public String toString() {
        return "Employee [name=" + name + ", phoneNum=" + phoneNum + "]";
    }
}

Eclipse が出力するコードを参考に書いてみましたが、正直うまく動くか自信はありません。


Java と同様の例を F# で書いた場合、以下のようになります。

// F#のレコードを使った値オブジェクトの例
type Employee = {
  Name: string
  PhoneNum: PhoneNumber
}

驚くほどシンプルで分かりやすいですよね。
F# では、同値比較可能な型のみで構成されたレコードであれば = 演算子によって同値比較が可能です。
同値比較可能な型には、int や string 型以外にも、タプルやレコード、判別共用体が含まれます。
他にもありますが、F# でよく使う型 / よく作る型は大体これらの組み合わせになります。
そのため、F# のレコードは基本的には何もしなくても同値比較できることになります。


また、%A という書式指定子を用いることで、何もしなくてもいい感じに文字列化できるので、ToString をオーバーライドする機会もそれほど多くはありません。


上記のコードに余分なノイズは一切ありませんし、バグの入り込む余地もありません。


「これくらいなら IDE で自動生成できるよ!」という意見もあるでしょう。
しかし、例えば Employee に他の情報、例えば役職が追加されたことを考えてみてください。
F# の場合、単にフィールドを追加するだけですが、Java の場合は自動生成をやり直す必要があります。
これを忘れると、原因究明が困難なバグの原因となってしまいます。

制限することによるパワフルさ

レコードに限った話ではないですが、関数型言語は手続型言語に比べて色々とできないことが多いです。
例えば、

  • 変数への再代入は基本的にできない
  • つまり繰り返しのために C 言語のような for 文はまず使えない
    • そもそも C 言語のような for 文がない

などです。
なぜできないのか、に対する一つの答えとしては、「それらを捨ててまで得たいものがあるから」というものです。
これらを捨てる代わりに、別のパワフルなものを得ているわけです。
それをここで述べることはしませんが*2、そういうものだと思ってください。
わざわざ制限されまくりの環境でプログラミングをしたいという人は・・・いなくはないですが、業務では・・・ないとは言いませんが、したくないですよね?
それと同じように、制限(に思える何か)には大抵理由があります。理由なしにそうなっていることはないと言っていいでしょう。


クラスからみると制限の多いように見えたレコードも、値クラスという「得たいもの」のために特化しているだけでした。
クラスは色々と出来すぎるのです。
オブジェクト指向では、クラスは単一の責任のみを持つこと、という原則(SRP:単一責任原則)があります。
しかし、皮肉なことにクラス自身が複数の責任を持っていると見ることもできるわけです。

レコードは、オブジェクト指向プログラミングにおける値オブジェクトのためのものなのです。

と書きましたが、逆に考えることもできます。つまり、
値オブジェクトは、関数型言語におけるレコードを実現しようとしているのです。
と。
どちらの方が正しいとかではなく、単に見方の問題だとは思いますが、これだけは言えます。


業務で値オブジェクトを使用するような場面では、関数型言語は有用である。


そして、F# はクラスやインターフェイスも持っています。
例えば、DDD の文脈での値オブジェクトの対ともいえるエンティティはクラスやインターフェイスを用いて実装し、値オブジェクトをレコードで実装する、という設計も可能です*3
これだけで F# を業務アプリケーションに適用できそうな可能性を感じてきませんか?


今回はここまでです。
次回はレコードの対である、判別共用体のお話です。

F# Advent Calender 2011 としては、変態 F#er として有名な @gab_km さんです。
楽しみですね!

*1:値オブジェクトという観点から、と書いたように、実際にはレコードを用いて値オブジェクト以外のものを表すことも可能です。しかし、レコードの使用目的のほとんどは値オブジェクトなので、ここでは無視します。

*2:変数への再代入が出来ないという制限をすることによる嬉しさについては、別の連載を考えていて、その中で話す予定

*3:サービスは普通に関数をモジュールでまとめればいいでしょう。