レコードの不変条件を強制する方法 #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]。
まとめ
フィールドのアクセス指定とコンストラクタのアクセス指定は別々にしたい。
参考サイト
- [1] 三角形の成立条件とその証明 | 高校数学の美しい物語
- [2] f# - Is it possible to enforce that a Record respects some invariants? - Stack Overflow
- [3] constructor - How to do argument validation of F# records - Stack Overflow
- [4] Designing with types: Single case union types | F# for fun and profit
- [5] New type: constrained type · Issue #553 · fsharp/fslang-suggestions · GitHub
- [6] Implement [<StructuralEquality>] and [<StructuralComparison>] for simple class types · Issue #554 · fsharp/fslang-suggestions · GitHub