判別共用体のケース識別子は直接公開せず、インターフェイスをアクティブパターンで提供するのがスマートっぽいよ
とあるF#er達のつぶやき
判別共用体は直接公開してどうのこうのしたりするよりかは、内部的な実装は判別共用体でやって、アクティブパターンでインターフェイスを提供するというのがスマートっぽいよ。#fsharp
@zecl それって、多言語を意識したインターフェイスを提供する場合、という前提が付いたりしませんかね? #fsharp
2011-04-20 16:14:00 via Chromed Bird to @zecl
@zecl あー。どっちかっていうと s/多言語/マルチ パラダイム/ です。 #fsharp
2011-04-20 16:15:37 via Chromed Bird to @zecl
まずは F# Component Design Guidelines の翻訳が必要なのかもしれず。
26ページだから、さほどしんどくはなさそうには思える。
@igeta F#で利用するライブラリを作る場合に有効かと思っています。判別共用体ではなく、アクティブパターンを公開することで、クライアントのコードへの影響をおさえながらライブラリが変更しやすくなるので。マルチパラダイムは意識していませんでした。 #fshap
2011-04-20 16:33:16 via web to @igeta
いげ太さんが26ページくらいなら翻訳してくれると聞いて RT @igeta まずは F# Component Design Guidelines の翻訳が必要なのかもしれず。
2011-04-20 16:34:23 via web to @igeta
@zecl シチュエーション的にどういうのを想像されていますか? 判別共用体のケースが増えた場合にとか、減った場合にとか、付随する型が変わった場合にとか。
2011-04-20 16:58:31 via Chromed Bird to @zecl
アクティブパターンによるインターフェースの提供や抽象化については、僕が今書いている記事に少し言及が・・・! #fsharp
@nobuhisa_k のぶひさ先生が颯爽と登場!
@igeta 恐れ多いです!(゚_゚) まぁ出版は数ヶ月先だと思いますが。。。
2011-04-20 17:35:59 via Keitai Web to @igeta
わたしのつぶやきが不正確かつ言葉足らずすぎて上手く伝わらなかったのが事の発端です ><。
あまり考えずにつぶやくと、いらぬ誤解を招いたりスルーされたりするから、気を付けよう(まぁ、所詮つぶやきですが)。
イケメンF# MVPの@bleisさんにリツイートされたときは、「えっ」て思っちゃいましたが、たぶん言わんとすることが伝わったということでしょう。流石です!
ということで、最後はのぶひさ先生(id:Nobuhisa)に颯爽と登場していただいたのですが、
執筆中の記事が読めるのはまだ先になりそうということで、わたしが言いたかった事をちょっと説明してみます。
ライブラリ側としては、クライアントに判別共用体のケース識別子を公開したいことってあんまりないよね。
@zecl シチュエーション的にどういうのを想像されていますか? 判別共用体のケースが増えた場合にとか、減った場合にとか、付随する型が変わった場合にとか。
2011-04-20 16:58:31 via Chromed Bird to @zecl
判別共用体のケース識別子の増減または付随する型の変更について、クライアントコードに対する影響範囲を少なくすることを考えています。
ライブラリの判別共用体のケース識別子は、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つのインターフェイスのみで完結します。
@zecl それって、多言語を意識したインターフェイスを提供する場合、という前提が付いたりしませんかね? #fsharp
2011-04-20 16:14:00 via Chromed Bird to @zecl
@zecl あー。どっちかっていうと s/多言語/マルチ パラダイム/ です。 #fsharp
2011-04-20 16:15:37 via Chromed Bird to @zecl
いげ太さんのつぶやきで気が付いたのですが、「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#で良いコードを書くために、ぜひとも押さえておきたいところです。
アレンジや使い方一つでイメージが変わるバナナクリップ。かー