C#に型クラスを入れる実装の話
この記事はC# Advent Calendar 2016の12日目のものです。 昨日(今日)書いた、F# Advent Calendar 2016 11目のC#版です。
今日のリポジトリはこちら。
実は、F#版だけじゃなくてC#版の実装もあります。 ということで、そのざっくりした紹介です。
型クラス?コンセプト?
F#版では「型クラス(type class)」と呼んでいますが、C#版では「コンセプト(concept)」と呼んでいるようです。 で、コンセプトがあると何がうれしいかですが、例えばC#には現在3つの2要素タプルがあります。
System.Collections.KeyValuePair<TKey, TValue>
System.Tuple<T1, T2>
(T1, T2)
これらの型すべてに対応するためには、現在のC#ではオーバーロードを3つ書く必要があります。
例えば、「2要素タプルの IEnumerable
から1番目の要素を取り出した IEnumerable
にしたい」という場合を考えてみましょう。
public static IEnumerable<T1> FirstAll<T1, T2>(IEnumerable<KeyValuePair<T1, T2>> xs) => xs.Select(kvp => kvp.Key); public static IEnumerable<T1> FirstAll<T1, T2>(IEnumerable<Tuple<T1, T2>> xs) => xs.Select(t => t.Item1); public static IEnumerable<T1> FirstAll<T1, T2>(IEnumerable<(T1, T2)> xs) => xs.Select(t => t.Item1);
面倒ですね。 ここで、提案されているコンセプトを使った場合にどうなるか見てみましょう。
// 新しいキーワードconceptを使ってコンセプトを定義 public concept Tuple2<Tpl, [AssociatedType] T1, [AssociatedType] T2> { T1 First(Tpl t); T2 Second(Tpl t); Tpl Make(T1 item1, T2 item2); } // 新しいキーワードinstanceを使ってコンセプトのインスタンスを定義 // ここではKeyValuePairをTuple2のインスタンスにしている public instance KeyValuePairTuple2<T1, T2> : Tuple2<KeyValuePair<T1, T2>, T1, T2> { T1 First(KeyValuePair<T1, T2> t) => t.Key; T2 Second(KeyValuePair<T1, T2> t) => t.Value; KeyValuePair<T1, T2> Make(T1 item1, T2 item2) => new KeyValuePair<T1, T2>(item1, item2); } // System.TupleをTuple2のインスタンスにする public instance TupleTuple2<T1, T2> : Tuple2<Tuple<T1, T2>, T1, T2> { T1 First(Tuple<T1, T2> t) => t.Item1; T2 Second(Tuple<T1, T2> t) => t.Item2; Tuple<T1, T2> Make(T1 item1, T2 item2) => Tuple.Create(item1, item2); } // System.ValueTupleをTuple2のインスタンスにする public instance ValueTupleTuple2<T1, T2> : Tuple2<(T1, T2), T1, T2> { T1 First((T1, T2) t) => t.Item1; T2 Second((T1, T2) t) => t.Item2; (T1, T2) Make(T1 item1, T2 item2) => (item1, item2); }
こういう定義をしておけば、あとは一つだけ実装を書くだけです。
// 型パラメータにimplicitを付けて、その型パラメータがTuple2でなければならないことを制約で書く public static IEnumerable<T1> FirstAll<T1, T2, implicit Tpl2>(IEnumerable<Tpl2> xs) where Tpl2 : Tuple2<T1, T2> => xs.Select(x => First(x)); // 本体部分では何の修飾もなしにメソッドを呼び出す // 当然、SecondAllも同様に定義可能 public static IEnumerable<T2> SecondAll<T1, T2, implicit Tpl2>(IEnumerable<Tpl2> xs) where Tpl2 : Tuple2<T1, T2> => xs.Select(x => Second(x));
これで、定義した FirstAll
や SecondAll
には IEnumerable<KeyValuePair<TKey, TValue>>
も IEnumerable<Tuple<T1, T2>>
も IEnumerable<(T1, T2)>
も渡せます。
このように、既存の型に対して後付けで新たな抽象を追加できるのがコンセプトの便利なところの一つです。
ここからは未確認ですが、おそらく戻り値オーバーロードのようなこともできるようです。
public static IEnumerable<Tpl2> Singleton<T1, T2, implicit Tpl2>(T1 x, T2 y) where Tpl2 : Tuple2<T1, T2> => Enumerable.Repeat(Make(x, y), 1); IEnumerable<KeyValuePair<string, int>> res1 = Singleton("aaa", 0); IEnumerable<Tuple<int, int>> res2 = Singleton(10, 20); IEnumerable<(string, string)> res3 = Singleton("hoge", "piyo");
他にも、例えば今は Enumerable.SequentialEqual
で IEnumerable<T>
どうしの比較をしていますが、比較不可能なもの((Equals
をオーバーライドしていないとかとか))でもコンパイルが通ってしまいますが、コンセプトが導入されれば Eq
コンセプトの要素を持つ場合のみに有効な比較演算子みたいなものも定義出来てうれしい、とかがあったりします。
この実装方法の利点・欠点
この実装方法は、既存のランタイムに全く手を加える必要がないのが利点です。
欠点は、この実装方法でどこまでやるかという話になりますが、例えば ==
演算子を Eq
コンセプトで置き換えるとなると、互換性を犠牲にする必要が出てきてしまう点です。
全部作り直してしまえるタイミングはとうの昔に過ぎ去っているので、別の演算子を導入するとか何らかのスイッチで切り替えられるようにしておくとかしないといけません(そんなの知るか、全部作り直しじゃー!ってのも面白いんですけどまずないでしょう)。
C#にコンセプトはいつ乗るの?
この実装が乗ることはまずないです。 ですが、こういう「今ここにない機能」が実際に動作するコードとともに公開されているというのは、いい時代になったものです。 コンセプト(≒型クラス)は、Haskellはもちろん似たような機能がSwiftやRust、Scalaといった今をときめく言語たちに乗っていますので、この実装そのままではなくても、いつかはC#にも乗ったりする日が来るかもしれませんね。
F#に型クラスを入れる実装の話
この記事はF# Advent Calendar 2016の11日目のものです。ちょっと遅れてしまいました。。。
ICFP 2016(と併催されたML workshop?)で気になる内容があったので、ちょっとまとめてみました。
Classes for the Masses - ICFP 2016
ざっくり、F#に型クラスを導入してみたぜ、って内容です。
型クラスとは
JavaやC#での interface
みたいなものですが、interface
は侵入的なのに対して、型クラスは非侵入的という違いがあります。
侵入的というのは、型の定義にその interface
を実装しますよ、ということを書く必要があることを意味します。
// C# interface Eq<A> { bool Equal(A a, A b); } // intefaceは型に侵入する class SomeClass : Eq<SomeClass> { public bool Equal(A a, A b) { ... } }
それに対して型クラスは非侵入的であり、型の定義にその型クラスを実装することは書きません。
-- haskell class Eq a where (==) :: a -> a -> Bool -- 型クラスは型の定義に書かなくていい data SomeType = ... -- SomeTypeをEq型クラスのインスタンスにする(型定義と分かれている) instance Eq SomeType where x == y = ...
これの何が嬉しいかというと、ひとつは、型の定義をその型に対して可能な操作と分離できることです*1。
これによって、例えば標準ライブラリの型に対しても、後付けで型クラスのインスタンスにできるようになります。 抽象を後付けできるとでも表現すればいいでしょうか。
この型クラスをF#に導入してみた、というのが今回紹介する内容です。
F#への型クラスの実装方法
実際に動作するコードは下記のリポジトリで公開されています。
先に示した Eq
型クラスはこの実装を使うと、
// Eq型クラスの実装(interfaceとしてコンパイルされる) [<Trait>] type Eq<'a> = abstract equal: 'a -> 'a -> bool // SomeTypeをEq型クラスのインスタンスにする(structとしてコンパイルされる) // Haskellと違い、インスタンスの定義に名前(ここではEqSomeType)が必要 [<Witness>] type EqSomeType = interface Eq<SomeType> with member equal x y = ...
と書きます。
この Eq
型クラスを使うには、
let (==) a b = Eq.equal a b
のように、型クラス名.メンバー 引数リスト ...
のように書くようです。
型クラスは struct
にコンパイルされるため、デフォルト値を介して型クラスのメンバーにアクセスできます。
この関数は、下記のようにコンパイルされます。
// C#モドキ public static bool operator ==<A, EqA>(A a, A b) where EqA : struct, Main.Eq<A> => default(EqA).equal(a, b);
struct
を使うことで、追加の引数を不要にしています。
また、Eq
型クラスを要素に持つリストを Eq
型クラスのインスタンスにする(それ以外のリストは Eq
型クラスにしない)こともできます。
[<Witness>] type EqList<'a, 'EqA when 'EqA :> Eq<'a>> = interface Eq<'a list> with member equal a b = match a, b with | x::xs, y::ys -> Eq.equal a b && Eq.equal xs ys | [], [] -> true | _, _ -> false
互換性及び他の.NET言語との連携
ここまででみたように、この実装ではあくまで.NETの型でそのまま表現できる形になっています。 ランタイムに手を加える必要がないため、互換性を崩すことなく採用できるように実装されている、ということです。
また型クラスを使った関数は、型クラスに対応しない既存の.NET言語からは型パラメータを明示的に渡せば使えます(使いやすいとは言っていない)。
この方法の問題点
この方法はランタイムに手を加えないため、(例えば Monad
のような)高階型クラスがサポートできません。
ううむ、残念・・・
あ、それと、このリポジトリをcloneしてbuild.cmdを管理者権限で実行するとF#の環境がぶっ壊れた(VSでビルドできなくなった)ので、やるなら仮想環境で試してみることをお勧めします。
続・そろそろPower Assertについてひとこと言っておくか
3年前にこんな記事をあげました。
3行でまとめると、
- Power Assertはユニットテストのためにほしかったものではない
- 欲しいのは結果の差分
- 誰か作って!
というエントリでした。 そしたら id:pocketberserker が作ってくれました!
PowerAssertより強そうな名前でいい感じです。
Power Assertは時代遅れ、今はMuscle Assertだ!的な話かな?
— 裸のWPF/MVVMを書く男(マン) (@gab_km) 2016年6月1日
MuscleAssertの使い方
このライブラリは、PersimmonというF#用のテスティングフレームワークを拡張するライブラリとして作られています。 ただ、ざっくり概要をつかむだけであればどちらも知らなくても問題ありません。 このライブラリでできることはほぼ1つだけです。
open Persimmon open Persimmon.Syntax.UseTestNameByReflection open Persimmon.MuscleAssert let add x y = x + y let ``add 2 3が5を返す`` () = test { do! add 2 3 === 5 }
以上。簡単。 これを実行しても成功してしまって面白みがないので、わざと間違ってみましょう。
open Persimmon open Persimmon.Syntax.UseTestNameByReflection open Persimmon.MuscleAssert let add x y = x + x // ミス! let ``add 2 3が5を返す`` () = test { do! add 2 3 === 5 }
これをPersimmon.Consoleで実行すると、
Assertion Violated: add 2 3が5を返す 1. . left 4 right 5
こんなエラーが出てきました。 普通ですね。
では、例えばこんなJSONがあったとしましょう。
{"widget": { "debug": "on", "window": { "title": "Sample Konfabulator Widget", "name": "main_window", "width": 500, "height": 500 }, "image": { "src": "Images/Sun.png", "name": "sun1", "hOffset": 250, "vOffset": 250, "alignment": "center" }, "text": { "data": "Click Here", "size": 36, "style": "bold", "name": "text1", "hOffset": 250, "vOffset": 100, "alignment": "center", "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" } }}
これを読み込む関数を定義したとして、その関数をテストしたいですよね。
let expected = let ``JSONが読み込める`` () = test { do! read json === expected }
read
関数の実装にミスがあり、text
の vOffset
に hOffset
の値を使ってしまったとしましょう。
このテストを実行すると、下記のようなエラーメッセージが表示されます。
Assertion Violated: JSONが読み込める 1. .text.vOffset left 250 right 100
text
の vOffset
の値が左は 250
だったけど、右は 100
だった、ということが一目瞭然です。
MuscleAssert VS PowerAssert
MuscleAssertとPowerAssertの目的ははっきりと分かれています。 MuscleAssertが最初からテスティングフレームワークのアサーションを書くために特化しているのに対して、PowerAssertは(テストではなく)表明に使うことを前提にデザインされています。
表明手段
表明手段としてのPowerAssertはとても便利です。
言語内蔵の assert
は、条件式が false
の場合に何やらメッセージを出しますが、「どこで表明が false
と評価された」くらいの情報しか持っていません。
メッセージをカスタマイズすることはできますが、文字列で指定する必要があるため「どうなったか」を埋め込むのは大変です。
PowerAssertは、言語内蔵の assert
をそのままに表示されるメッセージをリッチにしてくれます。
表明として埋め込んだ式の「部分式の値」がメッセージとして表示されるため、「どの式の評価値が想定と違うのか」を調べるための情報をコーディングのコストを払わずに得られるようになるのです。
対してMuscleAssertはそもそも、Persimmon.MuscleAssertはPersimmon用のライブラリとして作られているため、Persimmonに依存しており単体で使えるものではありません。 表明に使えたとしても、MuscleAssertは式全体の評価結果の差分を出すため、ほしい情報である「どの式の評価値が想定と違うのか」を調べるための情報はそこに乗っていないでしょう。
表明手段としては、PowerAssertの圧勝です。
ユニットテスト用アサーション
しかし、MuscleAssertがやりたかったのは表明ではありません。 ユニットテストのアサーションとして使いたかったのです。
MuscleAssertが例えばJSONのようなネストした構造に対するテストに強そうだ、というのは先ほど紹介した例で分かると思います。 XMLやJSONやYAMLは当然として、そもそもクラス自体が何かを内部に持っているネスト構造をしているため、ネストした構造をそのまま比較してもわかりやすいメッセージが出力されるMuscleAssertは便利です。
対してPowerAssertはこの例には貧弱です。
let ``JSONが読み込める`` () = test { do! read json === expected }
このテストが失敗するとして、PowerAssertで表示されるのは
json
変数の中身read json
の結果expected
の中身read json === expected
がfalse
になったということ
ですかね。 どれもドバドバと大量の出力をするわりに、本当に欲しい「どこがどう違うのか?」という情報はそこから得るのは容易ではありません。 diffツールを使って外部でdiffとるとかしたことある人も多いんじゃないでしょうか?
そもそも、テストで actual
側に部分式が出てうれしいほど何かを書くことって多いのか?というのも疑問です。
このテストのように、多くのテストでは期待値との一点比較ができればいいのではないでしょうか?
ちなみに、MuscleAssertでは一度に複数の箇所の間違いを出してくれますので、小さいテストをまとめるのも容易です。
1. .image.hOffset left 500 right 250 .image.vOffset left 500 right 250 .text.vOffset left 250 right 100 .text.alignment left centre right center @@ -1,6 +1,6 @@ cent -re +er
MuscleAssertの弱点
MuscleAssertの弱点は、一点比較しかできないところです。 そのため、浮動小数点数を含むデータ構造を、浮動小数点数の一致範囲を指定して比較、ということは現状ではできません。 また、大小比較などもサポートしていません。
現状でこれらをテストしたい場合は、MuscleAssertを使わずにテストするしかありません。 今のところ、これで困ったことはありません(そういうテストが必要なドメインで仕事をしていない)。
まとめ
まとめも3行で。
- MuscleAssert便利
- テストのためのアサーションライブラリとしてはPowerAssertよりも便利
- 弱点はある。でも自分が困っていないから放置
みなさんも自分が使っている言語でMuscleAssertを実装してみてはいかがでしょう?便利ですよ。
F#でWPFやるときのTipsとか(その2)
F#でWPFやるときのTipsとか(その1)の続編です。
添付プロパティの作り方
F#で添付プロパティを作るには、添付プロパティの本体はプロパティではなくフィールドに保持する必要があるようです。
// ダメな例 type Sample private () = static member SomeValueProperty = DependencyProperty.RegisterAttached("SomeValue", typeof<string>, typeof<Sample>, FrameworkPropertyMetaData("", Sample.OnSomeValueChanged)) static member GetSomeValue(obj: DependencyObject) = obj.GetValue(Sample.SomeValueProperty) :?> string static member SetSomeValue(obj: DependencyObject, value: string) = obj.SetValue(Sample.SomeValueProperty, value) static member OnSomeValueChanged = PropertyChangedCallback(fun sender e -> // プロパティが変更されたときの処理 )
このコードはコンパイルは通りますが、この添付プロパティにXAML内でBindingしようとすると、
'Button' コレクション内で 'Binding' を使用することはできません。'Binding' は、DependencyObject の DependencyProperty でのみ設定できます。
というエラーになってしまいます。
プロパティではなくフィールドを使うとうまくいきます。
type Sample private () = // 一旦staticなフィールドに保持しておいて、 static let someValueProperty = DependencyProperty.RegisterAttached("SomeValue", typeof<string>, typeof<Sample>, FrameworkPropertyMetaData("", Sample.OnSomeValueChanged)) // プロパティの値として保持したフィールドを設定 static member SomeValueProperty = someValueProperty static member GetSomeValue(obj: DependencyObject) = obj.GetValue(Sample.SomeValueProperty) :?> string static member SetSomeValue(obj: DependencyObject, value: string) = obj.SetValue(Sample.SomeValueProperty, value) static member OnSomeValueChanged = PropertyChangedCallback(fun sender e -> // プロパティが変更されたときの処理 )
添付プロパティがF#で書けるため、WPFのかなりの部分がF#のみで完結できると思われます。 Full F#でWPFがかなり現実味を帯びてきました。 足りないのは各種ユーティリティなので、その辺の再実装が苦でない人であれば、十分選択肢に入ってくる環境はすでに整ったと言えるでしょう。
別Windowの開き方
今のところ、一番手軽に別Windowを開くには、XAML Type Providerを使うのがいいでしょう。
まずはXAMLを作る必要がありますが、F#のプロジェクトではXAMLのアイテムテンプレートがないため、「General」の「XMLファイル」を選んでファイルの拡張子をxamlに変更します。 注意点として、この方法で追加したファイルは「ビルドアクション」が「None」になっているので、「Resource」に変更しておく必要があります。
「F# Empty Windows App (WPF)」テンプレートでプロジェクトを作った場合、
type OtherView = XAML<"OtherWindow.xaml">
としてViewを表す型を作っておいて、何らかのコマンド内でこの型のオブジェクトを生成して Show
(もしくは ShowDialog
)を呼び出します。
member this.OnClick = this.Factory.CommandSync (fun () -> let view = OtherView() view.Root.Show() )
ちなみに、「F# Empty Windows App (WPF)」テンプレートで導入されるFsXaml.Wpfは古い(0.9.9)ため、パッケージを更新(現時点では2.1.0)するとビルドが通らなくなります。
ビルドを通すためには、Root
プロパティへのアクセスを消してください。
member this.OnClick = this.Factory.CommandSync (fun () -> let view = OtherView() view.Show() )
App.fsもコンパイルエラーになるので、そちらの Root
も削除しましょう。
[<STAThread>] [<EntryPoint>] let main argv = App().Run()
F#でWPFやるときのTipsとか(その1)
最近、Full F#でWPFしてるので、Tipsてきなものをまとめようと思います。 その2はTipsがたまればあるかもしれませんが、過度な期待はしないでください。
プロジェクトの作り方
基本的には、Pure F# WPF GUIアプリ開発に向けてに書いてある通りです。 細かい注意点があるので書いておきます。
.NET Frameworkの選択に関する注意点
.NET Frameworkを4.5.1や4.5.2などのような3桁のものを選ぼうとすると、プロジェクト作成に失敗します。 また、4.6を選んでも4.5として作られるので、その点にも注意しましょう。
初回以外での作成
言うまでもないことかもしれませんが一応。 一回でも「F# Empty Windows App (WPF)」を使ってプロジェクトを作った場合、それ以降は「オンライン」の方ではなく、「インストール済み」のテンプレートを選ぶことになります。
MainView.xaml.fs
「F# Empty Windows App (WPF)」というテンプレートでプロジェクトを作ると、下記の内容でMainView.xaml.fsというファイルが生成されます。
namespace ViewModels open System open System.Windows open FSharp.ViewModule open FSharp.ViewModule.Validation open FsXaml type MainView = XAML<"MainWindow.xaml", true> type MainViewModel() as self = inherit ViewModelBase()
不要な型
実際にこのテンプレートで作る際、MVVMで作る場合は MainView
は不要です。
こいつはC#でのコードビハインドで作る場合に使うものに相当するため、MVVMで行く場合は消してしまって大丈夫です。
当然、C#でのコードビハインドで作る場合は MainViewModel
の方が不要なので、消してしまいましょう。
ただし、生成されるコードは完全にMVVMでやること前提なコードになっているので、正直お勧めしません。
細かいことですが、MainViewModel
の定義の =
の後ろに1つ、その下の行に4つ、空白文字が紛れ込んでいるのも注意しましょう。
気になる人は消してしまうといいでしょう。
App.fs
MainView.xaml.fs同様にテンプレートによって作られるファイルです。
module main open System open FsXaml open System.Windows type App = XAML<"App.xaml"> [<STAThread>] [<EntryPoint>] let main argv = App().Root.Run()
モジュール名
なぜか小文字で main
というモジュールになっています。
細かい部分ですが、Main
とか App
とかに変えたほうがいいでしょう。
XAML Type Providerについて
テンプレートで作られる構成がMVVMスタイルで作ることを意識したものになっていますので、あまりXAML Type Providerの出番はないのですが、Type Providerの使い方の例として触れておきます。
コード生成の代替としてのType Provider
C#でのコードビハインドではdesigner.csファイルが自動生成されますが、F#はXAML Type Providerの力によりそういったファイルは生成されません。
その代わりに、type HogeView = XAML<"xamlファイル名", true>
のようにしてビュー用の型をXAMLから生成することになります。
テンプレートによって生成されたプロジェクトを弄って、ちょっと使って見ましょう。
不要なファイルを消す
XAML Type Providerを使う場合は次のファイルは不要です。消してしまいましょう。
MainWindow.xamlを書き換える
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:ViewModels;assembly=FsEmptyWindowsApp3" xmlns:fsxaml="http://github.com/fsprojects/FsXaml" Title="MVVM and XAML Type provider" Height="200" Width="400"> <Grid> <Label x:Name="Message"/> </Grid> </Window>
Message
という名前を持った Label
を追加しました。
App.fsを書き換える
App.fsを書き換え、先ほど追加したラベルにテキストを設定してみます。
module App open System open FsXaml open System.Windows type MainView = XAML<"MainWindow.xaml", true> [<STAThread>] [<EntryPoint>] let main argv = // XAML Type Providerによって生成された型のインスタンスを生成し、 let view = MainView() // Messageというプロパティにアクセスし、Contentに文字列を設定 view.Message.Content <- "Hello XAML Type Provider!" // Application.RunにXAMLのRoot要素であるWindowオブジェクトを渡して、 // アプリケーションを起動 let app = Application() app.Run(view.Root)
これでビルドして実行すると、「Hello XAML Type Provider!」と表示されたウィンドウが開きます。
XAML Type Providerの力によって、自動生成コードなしでビューにアクセスできました。
イベントの登録
これだけだと、全部XAMLに書けばいいじゃん、という話になってしまうので、ボタンを追加してボタンにイベントを登録してみましょう。
MainWindow.xamlの内容を変更します。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:ViewModels;assembly=FsEmptyWindowsApp3" xmlns:fsxaml="http://github.com/fsprojects/FsXaml" Title="MVVM and XAML Type provider" Height="200" Width="400"> <StackPanel> <Label x:Name="Message"/> <Button x:Name="Btn">Please Click!</Button> </StackPanel> </Window>
Grid
は面倒なので StackPanel
に変え、Label
の下に Btn
という名前でボタンを追加しました。
そして、app.Run(view.Root)
の前にイベントを追加するコードを書きます。
view.Btn.Click.Add(fun x -> view.Message.Content <- "Clicked!")
これをビルドして実行すると、先ほど作った画面にボタンが追加されたウィンドウが開きます。 そして、ボタンをクリックするとラベルの内容が変わります。
こんな感じで、画面の要素に名前を付けておけば、かなり簡単に画面が作れることが分かります。 まぁ、ちょっとした画面であればXAML Type Providerも選択肢に入れてもいいかな、という感じでしょうか。
Optionに見るコンピュテーション式のつくり方
またの名を入門コンピュテーション式(嘘
この記事は、「コンピュテーション式ってどうやって作ればいいの?」に対する自分なりの回答です。
matchのネスト
option
を返す3つの関数f
, g
, h
があったとします。
で、このような処理がしたいとしましょう。
let niceFunction (arg1, arg2, arg3) = match f arg1 with | Some x -> match g arg2 with | Some y -> match h arg3 with | Some z -> Some (x, y, z) | None -> None | None -> None | None -> None
この関数は、3つの関数すべてが成功したときだけ、その結果をまとめて成功として返しています。 それ以外は何もせずに失敗として返しています。
このように、「すべて成功したときだけ計算したい」という状況はよく起こります。
例えば、f
がDBから何か取得する関数、g
がファイルシステムからファイルを取得する関数、h
がネットワークから何か取得する関数だとして、
これらすべてが成功したらそれらの情報を使って何か処理がしたい、というケースが考えられます。
これを毎回書くのはだるいですし、計算のもとになるソースが増えれば増えるほど、match
がネストしていきます。
どうにかできないでしょうか?
コードの「形」の共通部分
ここで注目してほしいのは、このコードの構造が再帰構造になっている点です。
match <expr> with | Some <v> -> +----------------+ | 全体と似た構造 | +----------------+ | None -> None
このように、Some
の場合の処理に、全体の構造に似た形が再び現れることが分かります。
まずは、この部分をカスタマイズできるように関数の引数として渡せるようにしてみましょう。
コードの再帰構造の関数化
<expr>
の部分と、Some
の場合に行う処理を引数に取ればよさそうです。
また、Some
の場合に行う処理では、Some
が持っている値も必要になるため、関数の引数として渡すことにします。
let matchSome target procForSome = match target with | Some v -> procForSome v | None -> None
この関数は、procForSome
に渡す関数の中で再びmatchSome
関数が呼び出されることを想定しています。
これを使うと、最初のコードはこう書けます。
let niceFunction (arg1, arg2, arg3) = matchSome (f arg1) (fun x -> matchSome (g arg2) (fun y -> matchSome (h arg3) (fun z -> Some (x, y, z) )))
無名関数のネストはありますが、疑似的にフラットに出来ました。
g
とh
の間に何か挟まってきても、
let niceFunction (arg1, arg2, arg3) = matchSome (f arg1) (fun x -> matchSome (g arg2) (fun y -> matchSome (hoge (arg1, arg2, arg3)) (fun w -> matchSome (h arg3) (fun z -> Some (x, y, w, z) ))))
なんとか対処できます。 ただ、末尾の閉じカッコはどんどん増えていきます。 どうにかならないでしょうか・・・
無名関数によるletの除去
さて、ちょっと話題を変えて、let
を除去する方法を考えてみましょう。
let x = 2 let y = "aaa" let z = [0..x] printfn "%A" (x, y, z)
F#で変数を導入したい場合に真っ先に思い浮かぶのがlet
です。
しかし、他にも変数が導入できるものがあります。
関数の引数です*1。
let x = 42 printfn "%A" x
このコードを無名関数を使って書き直すと、
(fun x -> printfn "%A" x) 42
となります。 これを参考に、最初のコードを書き替えてみます。
(fun x -> (fun y -> (fun z -> printfn "%A" (x, y, z)) [0..x]) "aaa") 2
これでは読めないので、|>
を使ってさらに変形します。
2 |> (fun x -> "aaa" |> (fun y -> [0..x] |> (fun z -> printfn "%A" (x, y, z))))
2
をx
に入れ、"aaa"
をy
に入れ、[0..x]
をz
に入れ、本体を実行しているように見えませんか?
コンピュテーション式の導入
さて、話を元に戻しましょう。
let niceFunction (arg1, arg2, arg3) = matchSome (f arg1) (fun x -> matchSome (g arg2) (fun y -> matchSome (hoge (arg1, arg2, arg3)) (fun w -> matchSome (h arg3) (fun z -> Some (x, y, w, z) ))))
このコードの末尾部分のカッコをどうにかしたいのでした。
そして、let
は無名関数で除去できる、ということを見ました。
では、無名関数をlet
で除去できないでしょうか・・・?
これは残念ながらできません。
ですが、コンピュテーション式を使えばlet!
という構文を使うことで可能になります。
let!
は、簡単にはコンピュテーションビルダーのBind
メソッド呼び出しに変形されます。
builder { let! v = expr ... }
このコードは、
builder.Bind(expr, (fun v -> ...))
このように変形されます。
ここで、matchSome
関数を思い出してください。
let matchSome target procForSome = match target with | Some v -> procForSome v | None -> None
この関数、Bind
が要求する形に似ていますね。
実は、matchSome
関数は、ほとんどそのままBind
として使えます。
では、コンピュテーションビルダーを定義してみましょう。
type OptionBuilder() = member __.Bind(x, f) = matchSome x f member __.Return(x) = Some x let option = OptionBuilder()
これを使えば、元のコード
let niceFunction (arg1, arg2, arg3) = matchSome (f arg1) (fun x -> matchSome (g arg2) (fun y -> matchSome (h arg3) (fun z -> Some (x, y, z) )))
は、こう書き直せます。
let niceFunction (arg1, arg2, arg3) = option { let! x = f arg1 let! y = g arg2 let! z = h arg3 return (x, y, z) }
フラットになりました!
このように、コンピュテーション式はある種のネスト構造をフラットに(読みやすく、かつ編集しやすく)書けるようにする機能を持ちます*2。
コンピュテーション式を作るには
自分でコンピュテーション式を作ろうとする場合、その「同じような構造がネストしている」ことを発見できねばなりません。 というか順番が逆で、「同じような構造がネストしている」のがだるいからコンピュテーション式でフラットにするのであって、コンピュテーション式が作りたいからそのような構造を見つけるのではないです。
で、先人はいくつも「同じような構造がネストしている」パターンを見つけてくれており、それぞれに名前まで付けてくれています。
例えば、上で例にした'a option
を対象にしたものは、MaybeモナドやOptionモナドとして広く知られています。
コンピュテーション式を作れるようになるための近道、それは色々なモナドを理解し、そのコンピュテーション式を実際に作ってみることです。
余談
モナドさえ理解できればコンピュテーション式は作れるようになるか、というとそういうわけではありません。 コンピュテーション式はモナド以上のことができてしまうため、モナドだけわかってもすべての機能の実装はできません(し、間違った実装を提供してしまいます)。
また、全然モナドじゃないコンピュテーション式も作れます。 作れますが、それが実用的になることはそうそうないでしょう。 コンピュテーション式の基本、それはモナドです。
Combine Deep Dives
この記事はF# Advent Calendar 2015の17日目の記事です。
今日はコンピュテーション式の Combine
について取り上げます。
詳説コンピュテーション式をある程度理解していると分かりやすいかもしれません。
内容を簡単にまとめると、
Delay
の中で受け取った関数を実行する場合、副作用を考慮したときに問題が起こらないか考えること- ゼロ値がある型で
Combine
を実装するときは、Delay
の中で受け取った関数を実行せずに、Combine
の中で実行すること - ゼロ値がない型で
Combine
を実装するときは、Combine
の実装はBind
に流し、Zero
はM<unit>
を返すように実装すること
です。
Combineの目的
Combine
は、コンピュテーション式の2つの式を繋ぐために使います。
コンピュテーション式中の変換対象となる式を ce
プレフィックスで表す場合、ce1; ce2
という式*1は Combine
を使って下記のように変換されます。
(* bはビルダークラスのインスタンス *) b.Combine(ce1の変換結果, b.Delay(fun () -> ce2の変換結果))
あれあれ、Delay
というメソッドが出てきました。
このように、Combine
を使うためには Delay
を実装する必要があります。
Delayの実装
Delay
をどうするかは、2通りの方法があります。
まずは、単純な方法から見てみます。
Delayの実装方法その1
Combine
の引数としては、ce1
の変換結果と ce2
の変換結果がそのまま渡されるのがとりあえずわかりやすい気がしませんか?
そういうことにしておくと、Delay
の実装はこう決まります。
(* 引数の関数を実行するだけ *) member __.Delay(f) = f ()
こうすることで、Combine
には ce1
の変換結果と ce2
の変換結果がそのまま渡されます。
この実装方針を取った場合、Combine
のシグネチャはMSDNのコンピュテーション式のページにあるように、
M<'T> * M<'T> -> M<'T>
または
M<unit> * M<'T> -> M<'T>
となるでしょう。 実際に具体例でみてみます。
listの場合
'a list
の Combine
を考えてみます。
list { yield 10 yield 20 }
とあったとき、望む結果が [10; 20]
だとすると、Combine
が意味するのはリスト同士の結合、つまり List.append
です。
実装してみましょう。
type ListBuilder () = member __.Yield(x) = [x] member __.Delay(f) = f () member __.Combine(xs, ys) = List.append xs ys
この場合、Combine
のシグネチャは 'a list * 'a list -> 'a list
になります。
optionの場合
'a option
の Combine
を考えてみます。
option { if cond then return 10 return 20 }
とあり、cond
によって
cond の値 |
結果 |
---|---|
true |
Some 10 |
false |
Some 20 |
となってほしいとします。
この場合、Combine
が意味するのは match
による分岐です。
else
の伴わない if
には Zero
も必要なので、実装します。
type OptionBuilder () = member __.Return(x) = Some x member __.Zero() = None member __.Delay(f) = f () member __.Combine(x, y) = match x with | Some x -> Some x | None -> y
この場合、Combine
のシグネチャは 'a option * 'a option -> 'a option
になります。
Delayの実装方法その2
上の Delay
の実装、無名関数でくるんだものをそのまま実行しており、Delay
の存在意義が分かりません。
無名関数でくるんだ結果を Delay
に渡すことなどせずに、直接 Combine
に渡してくれ、と思ってしまっても仕方ありません。
では、なぜ Delay
なんてものが Combine
の変換に出てくるのでしょうか?
上で実装した OptionBuilder
を使って、上の Delay
の実装には問題があることを見てみます。
let option = OptionBuilder() option { if true then return 10 printfn "hello" return 20 }
このコードは Some 10
を返しますが、「hello」も表示されてしまいます。
コンピュテーション式の部分を変換してみると、次のようになります*2。
let b = option b.Combine( (if true then b.Return(10) else b.Zero()), b.Delay(fun () -> printfn "hello"; b.Return(20)))
Delay
は受け取ったラムダ式をそのまま実行するように実装しましたので、Combine
を呼び出す前にラムダ式の中の式が実行されてしまうのです。
これを避けるためには、Delay
に渡ってきた関数は実際に必要になるまで実行を遅延する必要があります。
この方針で実装した OptionBuilder
は下記のとおりです。
type OptionBuilder () = member __.Return(x) = Some x member __.Zero() = None member __.Delay(f) = f (* ここでは実行せず、渡された関数をそのまま返す *) member __.Combine(x, rest) = match x with | Some x -> Some x | None -> rest () (* xがNoneのときのみ、渡された関数を実行する *)
この場合、Combine
のシグネチャは 'a option -> (unit -> 'a option) -> 'a option
となり、MSDNに書いてあるシグネチャとは異なるものになります。
まぁ、通常のシグネチャと言っている通り、別に必ずその通りにしなければいけないわけではないので、そういうものだと思ってください。
横道にそれますが、別に Combine
の実装の結果の型を 'a list option
にしてしまってもいいのです。変換された結果がコンパイル可能であれば、どんなシグネチャにしても構いません(ただしそういう実装にすると、Combine
をネスト出来なくなり、とても使いにくくなりますが)。
さぁではこれで実行してみましょう!
let option = OptionBuilder() let res = option { if true then return 10 printfn "hello" return 20 } printfn "%A" res
実行結果:
<fun:res@41>
!?!?
res
の型が関数になっちゃってますね。
これは、Delay
を実装するとコンピュテーション式全体も Delay
でくるまれるように変換されるのが原因です。
上の方でコンピュテーション式の変換結果をこう書きましたが、
let b = option b.Combine( (if true then b.Return(10) else b.Zero()), b.Delay(fun () -> printfn "hello"; b.Return(20)))
正しくはこうです。
let b = option (* 一番外側もDelayされる *) b.Delay(fun () -> b.Combine( (if true then b.Return(10) else b.Zero()), b.Delay(fun () -> printfn "hello"; b.Return(20))))
最初の Delay
の実装では渡された関数を Delay
の中で実行していたので問題になりませんでしたが、今回の Delay
の実装は渡された関数をそのまま返すため、最終的な結果が関数になってしまうのです。
さて困った・・・
Runの実装
この問題は、コンピュテーションビルダーに Run
を実装することで解決できます。
コンピュテーションビルダーに Run
が実装されていると、一番外側の Delay
のさらに外側に Run
メソッド呼び出しが挟まれます。
つまり、このように変換されることになります。
let b = option b.Run( b.Delay(fun () -> b.Combine( (if true then b.Return(10) else b.Zero()), b.Delay(fun () -> printfn "hello"; b.Return(20)))))
Run
には Delay
の結果が渡されることから、Run
の実装をこうすればいいでしょう。
member __.Run(f) = f ()
これで、望みの動きをする OptionBuilder
が手に入りました。
いい感じのOptionBuilder
type OptionBuilder () = member __.Return(x) = Some x member __.Zero() = None member __.Delay(f) = f member __.Combine(x, rest) = match x with | Some x -> Some x | None -> rest () member __.Run(f) = f ()
あとは Bind
や ReturnFrom
などを提供していきましょう。
ListBuilder再考
ListBuilder
の Combine
は OptionBuilder
のような考慮は不要なのでしょうか?
考えてみましょう。例えば、以下のようなコードはどうなるべきでしょうか?
let xs = list { if false then printfn "hello" yield 10 yield 20 }
「hello」とは表示されずに、[20]
が返ってきてほしいですよね。
このコンピュテーション式の変換結果を見てみましょう。
let b = list b.Delay(fun () -> b.Combine( (if false then printfn "hello"; b.Yield(10) else b.Zero()), b.Delay(fun () -> b.Yield(20))))
このように、printfn
は if
式の中にあるため、単純な Delay
の実装で何も問題ありません。
let xs = list { yield 10 printfn "hello" yield 20 }
この例では、[10; 20]
が返ってきてほしいため、やはり printfn
も実行されるべきでしょう。
これらのことから、ListBuilder
は最初の実装で十分、ということになります。
seq
を再実装したい場合は最初の実装では不十分ですが、これがなぜかを考えるのは読者への課題としておきましょう。
もう一つのCombine
Combine
の通常のシグネチャは、
M<'T> * M<'T> -> M<'T>
または
M<unit> * M<'T> -> M<'T>
でした。
しかし、今まで見てきたものはすべて前者の派生形であり、後者は出てきませんでした。
後者の第一引数側が unit
になるような Combine
はどういうときに出てくるのでしょうか?
今までの例の共通点
今まで見てきたのは、list
と option
でした。
この2つの共通点はいくつかありますが、ここではゼロ値を持つ点が重要です。
list
の場合は []
(空リスト)が、option
の場合は None
がゼロ値です。
型の定義を見てみると分かりやすいです。
type 'a list = | [] (* ゼロ値 *) | (::) of 'a * 'a list type 'a option = | None (* ゼロ値 *) | Some of 'a
このように、ゼロ値とそれ以外の場合でデータコンストラクタが別になっているのが分かります。
これらゼロ値は、'a
がなんであろうが使えます。
さて、ではこのような「ゼロ値」がないような型を考えてみます。
Async
F#で非同期計算を表す型である Async<'T>
を見てみます。
この型は list
や option
と違って、データコンストラクタが1つしかありません*3。
(* https://github.com/Microsoft/visualfsharp/blob/2d413fb940aa1677688454c50b8ec05cd3b6f78f/src/fsharp/FSharp.Core/control.fs#L584より *) [<NoEquality; NoComparison>] [<CompiledName("FSharpAsync`1")>] type Async<'T> = P of (AsyncParams<'T> -> FakeUnitValue)
そのため、ゼロ値はありません。
(ちなみに、FakeUnitValue
は unit
がIL的には void
に落ちてしまうため末尾最適化の対象にならない(tail.
プレフィックスが発行されない)問題を回避するために導入された型であり、unit
と思ってもらって構いません)
しかし、AsyncBuilder
は Zero
メソッドを次のシグネチャで提供しています。
member Zero : unit -> Async<unit>
Async<unit>
型の値はゼロ値ではありません。
例えば 'a option
の None
は実際の型が int option
だとしても string option
だとしても使えます。
ある意味、ジェネリックな値として振る舞うのです。
それに対して、AsyncBuilder
の Zero
メソッドは Async<'a>
ではなく Async<unit>
を返します。
この Zero
メソッドの定義に意味はあるのでしょうか?
Zero
単体ではわかりにくいので、Combine
も見てみます。
let sequentialA p1 p2 = bindA p1 (fun () -> p2) (* snip *) member b.Combine(p1, p2) = sequentialA p1 p2
引数の順番に注意する必要がありますが、なるほど Bind
に落ちるんですね。
bindA
の第二引数の関数が受け取る型が unit
になっている点に注目してください。
つまり、p1
の型は Async<unit>
である必要があります。
Combine
の型が Async<unit> * Async<'T> -> Async<'T>
になりました!
さらに、Zero
メソッドの戻り値の型と Combine
の第一引数の型が一致していることから、両者を組み合わせて使えることがわかります。
このように、ゼロ値が用意されていない(できない)型の場合に、M<unit> * M<'T> -> M<'T>
というバージョンの Combine
を提供すると、便利な場合があります。
Combine
の第一引数側を無視して、第二引数側を常に返すようなイメージですね。
また、その場合は Zero
メソッドは M<unit>
型の値を返すように定義します。
ということで、AsyncBuilder
ではこういうコードがコンパイルできます。
async { if cond1 then printfn "if1" if cond2 then printfn "if2" return "str" }
おぉ、便利っぽい!
ZeroとCombineの罠
ただ、注意点として、このようなコードもコンパイルできてしまいます。
async { if true then return () return "str" }
「え、型はどうなってるの?」と思った方、return
という単語のイメージに引きずられています。
F#の return
は別にコンピュテーション式全体の型を決めるわけではありません。
AsyncBuilder
での Combine
は、第一引数側を無視して、第二引数側を常に返すようなイメージでした。
第一引数側は Async<unit>
になっているため、Combine
によって無視(厳密には無視しているわけではないが・・・)されて、第二引数側が返されます。
直観と反している気はしますが、そういうものです。
展開結果を見ればもう少し納得しやすいかもしれません。
let b = async b.Delay(fun () -> b.Combine( (if true then b.Return() else b.Zero()), (* 第一引数側の結果にかかわらず *) b.Delay(fun () -> b.Return("str")))) (* 第二引数側の結果が使われる *)
こういうビルダーを使うときは、return ()
と書かないほうが無難でしょう。
unit
を受け取る Return
をオーバーロードして、Obsolete
属性でコンパイルエラーにする、とかできるかもしれませんのでそういうビルダーが作りたくなった際に参考にしてください。
returnできない罠
型の問題ではなく、「え、return
したのにその後ろのコードが実行されるの?」と思った方、return
という単語のイメージに引きずられています。
F#の return
は別にその時点で結果を返すようなものではありません。
単にビルダーの Return
メソッドが呼び出されるだけであり、Return
自体は実行の流れを制御できません。
AsyncBuilder
の Zero
メソッドが返す値はゼロ値ではありませんでした。
また、Combine
の第一引数が Async<unit>
に固定されているため、
async { if true then return "str1" (* ここにstringは置けない。unitである必要がある *) return "str2" }
とは書けません。コンパイルエラーになります。
このあたりを解決するために、Stateを使ったり継続を使ったりできるかもしれませんが、Async
では未検証です。
気になった方は以下のリンク群をどうぞ。
- コンピュテーション式の実装にStateを用いる
- コンピュテーション式におけるreturnとyield
- 継続を使ってOptionコンピュテーション式を実装する
- 継続渡しスタイルを使ってListコンピュテーション式を実装する
- F#のコンピュテーション式
- yieldとreturnの話
F#のコンピュテーション式を提供するライブラリ事情
yieldとreturnの話でも調べたのですが、現在の状況を調べてみました。 調べたのは下記のコードです。
- FSharpx.ExtrasのMonad.fsのMaybeBuilder
- ExtCoreのcontrol.fsのMaybeBuilder
- Visual F# PowerToolsのUtils.fsのMaybeBuilder
- Basis.CoreのComputationExpr.fsのOptionBuilder
FSharpx.Extras
まず、Delay
の実装ですが、これは受け取った関数を実行せずにそのまま返しています。
そのため、Combine
の中で第二引数を実行することになります。
しかし、これを Option.bind
にそのまま渡しているため、Combine
の第一引数の型が unit option
に固定化されてしまっています。
せっかく Zero
メソッドが 'a option
のゼロ値である None
を返しているにもかかわらず、これでは宝の持ち腐れです。
ということで、このようなコードのコンパイルが通ってしまいます。
maybe { if true then return () (* ??? *) return 10 }
さらに、Combine
の実装が第一引数として unit option
を要求するため、下記のコードはコンパイルが通りません。
maybe { if true then return 10 (* compile error *) return 20 }
これでは Zero
メソッドの戻り値を None
にしている意味が全くありません。
ExtCore
前回調査時は Zero
が返す値が Some ()
でしたが、そこは None
に修正されていました。
しかし、Combine
がオーバーロードされているうえ、シグネチャもおかしい・・・
member Delay: (unit -> 'T option) -> (unit -> 'T option) member Combine: unit option * 'T option -> 'T option (* 使われないオーバーロード *) member Combine: unit option * (unit -> 'T option) -> 'T option (* 実際はunit optionではなく'Tではないジェネリック型になっているけど、 Delayの呼び出しが第二引数に渡されるため、実質unitになる *)
Delay
の実装が受け取った関数をそのまま返す実装になっているため、最初の Combine
のオーバーロードは使われません。
しかも、FSharpx.Extras同様に Combine
の実装が Bind
を呼び出しているため、やはり下記コードはコンパイルできません。
maybe { if true then return 10 (* compile error *) return 20 }
Zero
の実装も意味がなく、やはり下記コードはコンパイルが通ってしまいます。
maybe { if true then return () (* ??? *) return 10 }
なかなかの迷走ぶりです。
Visual F# PowerTools
Visual F# PowerToolsが持っている MaybeBuilder
は、過去のバージョンのExtCoreからコピーしてきたものです。
そのため、Zero
が Some ()
になっており、Delay
は渡された関数を実行しており、Combine
は unit option * 'T option -> 'T option
版になっています。
Zero
と Combine
の整合性は取れていますが、ゼロ値を使っていないのが微妙です。
また、Delay
が実行を遅延しないバージョンなので、副作用と一緒には使えません。
まぁ、この MaybeBuilder
はVFPTの内部のみでしか使われない想定のため、それほど問題ではないでしょう。
Basis.Core
最近全然更新してませんが、このライブラリはそもそも巷のライブラリのコンピュテーションビルダーがことごとくダメ実装だったから作ったライブラリなので、これまで見てきたような問題はありません。
(* コンパイルエラー *) option { if true then return () return 10 }
(* Some 10 *) option { if true then return 10 return 20 }
ただし、while
の中での return
できるようにStateを使った実装をしているため、Zero
も Combine
もこれまで見てきたものとは全然違うシグネチャおよび実装になっています。
member this.Zero() = None, Continue member this.Combine((x: _ option, cont), rest: unit -> _ option * FlowControl) = match cont with | Break -> x, Break | Continue -> if x.IsSome then x, Break else rest ()
この実装についての話は、(再掲になりますが)下記のURLをどうぞ。
まとめ
Combine
を中心にいろいろなことを見ました。
Combine
の目的Combine
実装に絡むDelay
の実装2通り- 単純な
Delay
の実装の罠 - もう一つの
Delay
の実装とRun
- 単純な
- 単純な
Delay
の実装で十分なケース (ListBuilder
) - 第一引数として
M<unit>
を取るCombine
について- ゼロ値について
- ゼロ値がない場合(
Async<'T>
)のCombine
とその罠return ()
出来てしまうreturn x
出来ない
- コンピュテーション式を提供するライブラリの
Combine
- 大体のライブラリが何かしら問題を抱えている
皆さんがコンピュテーションビルダーを書く場合で、Combine
を提供したくなったときにこのエントリを思い出していただければありがたいです。
いやぁ、コンピュテーション式は楽しいなぁ。