p_tan's blog

勉強日記です。ツッコミ大歓迎

レコードの不変条件を強制する方法 #fsharp

この記事は F# Advent Calendar 2017 - Qiita 10日目の記事です。

F#でレコードを使う場合に、型の不変条件を強制したい場合があったので調べました。

例 : Triangle 型

例えば、以下のような辺の長さ a, b , c を持つ三角形を表すレコードがあったとします。

module Shape = 
    type Triangle = { a : float; b : float; c : float }

三角形の辺の長さ a, b, c の不変条件として、以下をすべて満たしている必要があります。

  • a + b > c
  • b + c > a
  • c + a > b

さて、今のままでは不変条件を無視してTriangle の値を生成できてしまいます。

open Shape
let tri = { Triangle.a = 1.; b = 2.; c = -1. } // 不正な値での生成 

クラスにしてコンストラクタやファクトリメソッドでチェックするという手もありますが、レコードや判別共用体には、クラスにない以下のメリットがあります。

  • デフォルトでの値による等価性/比較判定
  • パターンマッチによる値の取り出し、分解

上記のメリットを残したまま、不変条件を強制する方法はないでしょうか。

方法1. フィールドの private 定義とアクティブパターンを組み合わせる

なぜかMSDNには書いていないのですが、レコードや判別共用体は型自体のアクセス指定とは別に、フィールドのprivate/internal定義が可能になっています。フィールドをprivateにすると、モジュール外からは生成できなくなります。ただ、パターンマッチもできなくなってしまうので、アクティブパターンを用いてパターンマッチできるようにします。

module Shape = 
    // フィールドのみprivate定義. 型はpublicのまま
    type Triangle = private { a : float; b : float; c : float }
    // ファクトリ関数
    let createTriangle a b c : Triangle option = 
        // 不変条件チェック
        let isValid a b c = a + b > c && b + c > a && c + a > b
        if isValid a b c then Some { Triangle.a = a; b = b; c = c }
        else None
    // アクティブパターンでパターンマッチでの分解を可能にする
    let (|Triangle|) {Triangle.a = a; b = b; c = c} = (a, b, c) 

上記のように定義すると、以下のように使えます。

open Shape
// { Triangle.a = 1.; b = 2.; c = -1. } // privateにより直接生成不可
createTriangle 1. 2. -1. // -> 不変条件に違反しているのでNoneを返す.
let tri = createTriangle 2. 3. 4. |> Option.get
let (Triangle(a,b,c)) = tri // パターンマッチで分解.

かなりいい感じなのですが、わざわざアクティブパターンを書かなければならないのと、一部のフィールドのみを用いたパターンマッチができなくなることが欠点です。

let { Triangle.a = a } = tri // 通常のレコードなら可能だが・・・

方法2. クラスのvalメンバとStructuralEquality, StructuralComparison 属性を組み合わせる

もう一つの方法は、クラスのvalフィールドと属性を組み合わせるものです。(すでにレコードじゃなくなっていますが)

module Shape = 
    [<StructuralEquality>]
    [<StructuralComparison>]
    [<Struct>] // 現状、上記2つの属性は Struct でないとつけられない
    type Triangle = 
        val a : float
        val b : float
        val c : float
        private new(a, b, c) = { a = a; b = b; c = c }
        // ファクトリ関数
        static member Create a b c : Triangle option = 
            // 不変条件チェック
            let isValid a b c = a + b > c && b + c > a && c + a > b
            if isValid a b c then Some(Triangle(a, b, c))
            else None

クラスのvalフィールドはレコードのフィールドのようにパターンマッチができます。

open Shape
let tri = Triangle.Create 3. 4. 5. |> Option.get
let {Triangle.a = a} = tri // フィールドの一部をパターンマッチできる

また、StructuralEquality, StructuralComparison属性をつけることで、値での等価性/比較判定ができるようになります。 ただし、現状ではStructクラスにしか上記属性はつけられないようです。Structはデフォルトコンストラクタですべてのフィールドを0初期化できてしまうので、不変条件を強制するという今回の趣旨からみると残念な感じです。

なお、Structではない通常のクラスにも、StructuralEquality, StructuralComparison属性をつけられるようにしてほしいというIssueが立っているようです[6]。

まとめ

フィールドのアクセス指定とコンストラクタのアクセス指定は別々にしたい。

参考サイト