ようこそ。睡眠不足なプログラマのチラ裏です。

判別共用体のケース識別子は直接公開せず、インターフェイスをアクティブパターンで提供するのがスマートっぽいよ

とあるF#er達のつぶやき


わたしのつぶやきが不正確かつ言葉足らずすぎて上手く伝わらなかったのが事の発端です ><。
あまり考えずにつぶやくと、いらぬ誤解を招いたりスルーされたりするから、気を付けよう(まぁ、所詮つぶやきですが)。
イケメンF# MVPの@bleisさんにリツイートされたときは、「えっ」て思っちゃいましたが、たぶん言わんとすることが伝わったということでしょう。流石です!
ということで、最後はのぶひさ先生(id:Nobuhisa)に颯爽と登場していただいたのですが、
執筆中の記事が読めるのはまだ先になりそうということで、わたしが言いたかった事をちょっと説明してみます。


ライブラリ側としては、クライアントに判別共用体のケース識別子を公開したいことってあんまりないよね。

判別共用体のケース識別子の増減または付随する型の変更について、クライアントコードに対する影響範囲を少なくすることを考えています。
ライブラリの判別共用体のケース識別子は、privateまたはinternalで宣言することを検討します。要するにアクセシビリティを意識しましょうということでした。
つまりこれは、ライブラリで完結されるべきことはライブラリ内に隠ぺいしておくのが望ましいとも言っています。
これは、F# MVPいげ太さんのつぶやきの中でも登場した「Draft F# Component Design Guidelines(August 2010) (PDF)」にて推奨されている内容そのものです。



「Draft F# Component Design Guidelines(August 2010)」に出てくるサンプルコードを参考にして解説してみます。
まずは、ライブラリ側にケース識別子を隠ぺいしない判別共用体を宣言してみます。また、評価用の関数evaluateを定義します。

module Module1

type PropLogic =  
  | And of PropLogic * PropLogic  
  | Not of PropLogic 
  | True 

let rec evaluate = function
  | And(x,y) -> (evaluate x) && (evaluate y) 
  | Not x -> not (evaluate x)  
  | True -> true 


クライアント側から利用します。

open System
open Module1

let hoge = And(True,Not(True))
hoge|> evaluate |> printfn "%b" 

type Test = 
  | P of PropLogic 
  | B of bool

let (|Test|) = function
  | P a -> evaluate a
  | B a -> a

let homu = function
  | Test x -> x

homu (Test.P hoge) |> printfn "%b" 
homu (Test.B true) |> printfn "%b" 
Console.ReadLine () |> ignore


実行結果

false
false
true


上記は一見問題なさそうです。しかしこれはクライアント側がPropLogic判別共用体のケース識別子を直接参照していないからにすぎません。
また、ライブラリの判別共用体のケース識別の増減や型の変更などにとても脆いです。
判別共用体からケース識別子が隠ぺいされていないので、クライアント側がとても自由になります。次の実装を見てください。


open System
open Module1

let hoge = And(True,Not(True))
hoge |> evaluate |> printfn "%b" 

type Test = 
  | P of PropLogic   
  | B of bool

let (|Test|) = function
  | P a -> match a with
           | And(x,y) -> (evaluate x) || (evaluate y) 
           | Not x -> not (evaluate x)  
           | True -> false 
  | B a -> a

let homu = function
  | Test x -> x

homu (Test.P hoge) |> printfn "%b" 
homu (Test.B true) |> printfn "%b" 
Console.ReadLine () |> ignore


実行結果

false
true
true


これはひどい。「クライアントが自由に実装できて拡張性が高くてよい」と思いますか?それは大きな間違いです。
面白いことに、これは「自由でありながら不自由である」ということを意味しています。不必要で不適切な拡張はバグの温床となります。
ライブラリ内で完結されるべきことはライブラリ内に隠ぺいしておくのが望ましいということです。



変更に脆いことを確認するために、ライブラリのPropLogic判別共用体のケース識別子に「False」を増やしてみましょう。

type PropLogic =  
  | And of PropLogic * PropLogic  
  | Not of PropLogic 
  | True 
  | False

let rec evaluate = function
  | And(x,y) -> (evaluate x) && (evaluate y) 
  | Not x -> not (evaluate x)  
  | True -> true 
  | False -> false


クライアント側にの実装

open System
open Module1

let hoge = And(True,Not(True))
hoge |> evaluate |> printfn "%b" 

type Test = 
  | P of PropLogic   
  | B of bool

let (|Test|) = function
  | P a -> match a with
           | And(x,y) -> (evaluate x) || (evaluate y) 
           | Not x -> not (evaluate x)  
           | True -> false 
  | B a -> a

let homu = function
  | Test x -> x

homu (Test.P hoge) |> printfn "%b" 
homu (Test.P False) |> printfn "%b" 
homu (Test.B true) |> printfn "%b" 
Console.ReadLine () |> ignore


実行結果

false
true
'Microsoft.FSharp.Core.MatchFailureException' のハンドルされていない例外が ConsoleApplication1.exe で発生しました。


「homu (Test.P False) |> printfn "%b" 」を追加しました。
クライアント側で自由に変更した部分のパターンマッチで「Falseケース識別子」についての判定がないために例外が発生します。
これはコンパイル時に怒ってもらえません*1。非常に残念です。
ライブラリの判別共用体のケース識別子を削除した場合は、クライアント側で削除されたケース識別子を参照している場所すべてに影響がでてしまいます。
クライアント側が必要以上にフリーダムすぎるとカオスが訪れるということです。被害が拡大すると目も当てられません。
クライアントコードに対する影響範囲を少なくすることを考えて、判別共用体のケース識別子は private または internal で宣言することを検討しましょう。
ライブラリで完結されるべきことはライブラリ内に隠ぺいしておきましょう。


判別共用体のケース識別子を直接公開せず、アクティブパターンでインターフェイスを提供するのがスマート


ということで、ライブラリにケース識別子を隠ぺいした判別共用体を宣言します。
また、Evaluateのためのインターフェイスとしてアクティブパターンを定義します。

module Module1

type PropLogic =  
  private 
  | And of PropLogic * PropLogic  
  | Not of PropLogic 
  | True 

let (|Evaluate|) = 
  let rec evaluate = function
    | And(x,y) -> (evaluate x) && (evaluate y) 
    | Not x -> not (evaluate x)  
    | True -> true 
  evaluate

let makeAnd x y= And(x,y)
let makeNot x = Not(x) 
let makeTrue = True 

クライアント側

open System
open Module1

let hoge = makeAnd makeTrue (makeNot(makeTrue))
hoge |> (|Evaluate|) |> printfn "%b" 

type Test = 
  | P of PropLogic 
  | B of bool

let (|Test|) = function
  | P a -> match a with | Evaluate b -> b
  | B a -> a

let homu = function
  | Test x -> x

homu (Test.P hoge) |> printfn "%b" 
homu (Test.B true) |> printfn "%b" 
Console.ReadLine () |> ignore


実行結果

false
false
true


ケース識別子をprivate(あるいはinternal)で宣言することで、ライブラリを参照しているクライアント側からは
And、Not、Trueの3つのケース識別子について参照することができなくなります。ですから、クライアントで自由がききません。
判別共用体に対するEvaluateは、ライブラリから公開されているアクティブパターン「(|Evaluate|)」という1つのインターフェイスのみで完結します。




いげ太さんのつぶやきで気が付いたのですが、「Draft F# Component Design Guidelines(August 2010)」に例がでていました。
真似をして、マルチパラダイムを意識して書き直してみましょう。

type PropLogic =  
  private 
  | And of PropLogic * PropLogic  
  | Not of PropLogic 
  | True 
  /// 主にC#やVB.NETから呼ばれる事を意識
  member x.Evaluate =  
       match x with  
       | And(x,y) -> x.Evaluate && y.Evaluate 
       | Not x -> not x.Evaluate  
       | True -> true 
  /// 主にC#やVB.NETから呼ばれることを意識 
  static member MakeAnd(x,y) = And(x,y)
  static member MakeNot(x) = Not(x)
  static member MakeTrue = True

let (|Evaluate|) x = (x:PropLogic).Evaluate 
let makeAnd = PropLogic.MakeAnd 
let makeNot = PropLogic.MakeNot 
let makeTrue = PropLogic.MakeTrue 


クライアント側

open System
open Module1

let hoge = PropLogic.MakeAnd(makeTrue,makeNot(makeTrue))
hoge.Evaluate |> printfn "%b" 

type Test = 
  | P of PropLogic 
  | B of bool

let (|Test|) = function
  | P a -> match a with | Evaluate b -> b
  | B a -> a

let homu = function
  | Test x -> x

homu (Test.P hoge) |> printfn "%b" 
homu (Test.B true) |> printfn "%b" 
Console.ReadLine () |> ignore


実行結果

false
false
true


最後にケース識別子を隠ぺいした判別共用体にも「Falseケース識別子」を追加して変更に強いことを確認してみましょう。

module Module1

type PropLogic =  
  private 
  | And of PropLogic * PropLogic  
  | Not of PropLogic 
  | True 
  | False 
  /// 主にC#やVB.NETから呼ばれることを意識 
  member x.Evaluate =  
       match x with  
       | And(x,y) -> x.Evaluate && y.Evaluate 
       | Not x -> not x.Evaluate  
       | True -> true 
       | False -> false 
  /// 主にC#やVB.NETから呼ばれることを意識 
  static member MakeAnd(x,y) = And(x,y)
  static member MakeNot(x) = Not(x)
  static member MakeTrue = True
  static member MakeFalse = False

let (|Evaluate|) x = (x:PropLogic).Evaluate 
let makeAnd = PropLogic.MakeAnd 
let makeNot = PropLogic.MakeNot 
let makeTrue = PropLogic.MakeTrue 
let makeFalse = PropLogic.MakeFalse 


クライアント側

open System
open Module1

let hoge = PropLogic.MakeAnd(makeTrue,makeNot(makeTrue))
hoge.Evaluate |> printfn "%b" 

type Test = 
  | P of PropLogic 
  | B of bool

let (|Test|) = function
  | P a -> match a with | Evaluate b -> b
  | B a -> a

let homu = function
  | Test x -> x

homu (Test.P hoge) |> printfn "%b"
homu (Test.P makeFalse) |> printfn "%b" 
homu (Test.B true) |> printfn "%b" 
Console.ReadLine () |> ignore


実行結果

false
false
false
true


OKですね。クライアントに与える影響を最小限におさえてライブラリを変更することができました。
このようにケース識別子を追加する場合はもちろん、ケースの削除あるいは型を変更するような場合にも影響範囲を最小限に抑えることができます。


まとめ

ライブラリで判別共用体を宣言する場合は、ケース識別子をクライアントに対して隠ぺいすることを検討しましょう。
クライアントに与える影響を最小限におさえてバグの混入を防ぎ、変更に強いライブラリを作ることができます。
またその場合、判別共用体に対するインターフェイスをアクティブパターン*2で提供するのがスマートです(必要に応じて、ふつうの関数も提供するのも良い)。
その場合、パーシャルアクティブパターンの利用も検討してみるとよいでしょう。F#で良いコードを書くために、ぜひとも押さえておきたいところです。

*1:コンパイラによる警告は出ます

*2:バナナクリップ