読者です 読者をやめる 読者になる 読者になる

C#に型クラスを入れる実装の話

C#

この記事はC# Advent Calendar 2016の12日目のものです。 昨日(今日)書いた、F# Advent Calendar 2016 11目C#版です。

今日のリポジトリはこちら。

github.com

実は、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));

これで、定義した FirstAllSecondAll には 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.SequentialEqualIEnumerable<T> どうしの比較をしていますが、比較不可能なもの((Equals をオーバーライドしていないとかとか))でもコンパイルが通ってしまいますが、コンセプトが導入されれば Eq コンセプトの要素を持つ場合のみに有効な比較演算子みたいなものも定義出来てうれしい、とかがあったりします。

この実装方法の利点・欠点

この実装方法は、既存のランタイムに全く手を加える必要がないのが利点です。

欠点は、この実装方法でどこまでやるかという話になりますが、例えば == 演算子Eq コンセプトで置き換えるとなると、互換性を犠牲にする必要が出てきてしまう点です。 全部作り直してしまえるタイミングはとうの昔に過ぎ去っているので、別の演算子を導入するとか何らかのスイッチで切り替えられるようにしておくとかしないといけません(そんなの知るか、全部作り直しじゃー!ってのも面白いんですけどまずないでしょう)。

C#にコンセプトはいつ乗るの?

この実装が乗ることはまずないです。 ですが、こういう「今ここにない機能」が実際に動作するコードとともに公開されているというのは、いい時代になったものです。 コンセプト(≒型クラス)は、Haskellはもちろん似たような機能がSwiftやRust、Scalaといった今をときめく言語たちに乗っていますので、この実装そのままではなくても、いつかはC#にも乗ったりする日が来るかもしれませんね。

F#に型クラスを入れる実装の話

F#

この記事はF# Advent Calendar 2016の11日目のものです。ちょっと遅れてしまいました。。。

ICFP 2016(と併催されたML workshop?)で気になる内容があったので、ちょっとまとめてみました。

Classes for the Masses - ICFP 2016

ざっくり、F#に型クラスを導入してみたぜ、って内容です。

型クラスとは

JavaC#での 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#への型クラスの実装方法

実際に動作するコードは下記のリポジトリで公開されています。

github.com

先に示した 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でビルドできなくなった)ので、やるなら仮想環境で試してみることをお勧めします。

*1:オブジェクト指向プログラミングのよくある説明の一つに「データと操作をひとまとまりにできる」というものがありますが、それとはある意味正反対の特徴ですね。まぁ、この「データと操作をひとまとまりにできる」という説明には言いたいことがあるんですが、それは別の機会にでも

続・そろそろPower Assertについてひとこと言っておくか

F# Test

3年前にこんな記事をあげました。

bleis-tift.hatenablog.com

3行でまとめると、

  • Power Assertはユニットテストのためにほしかったものではない
  • 欲しいのは結果の差分
  • 誰か作って!

というエントリでした。 そしたら id:pocketberserker が作ってくれました!

github.com

PowerAssertより強そうな名前でいい感じです。

MuscleAssertの使い方

このライブラリは、PersimmonというF#用のテスティングフレームワークを拡張するライブラリとして作られています。 ただ、ざっくり概要をつかむだけであればどちらも知らなくても問題ありません。 このライブラリでできることはほぼ1つだけです。

open Persimmon
open Persimmon.Syntax.UseTestNameByReflection
open Persimmon.MuscleAssert

let add x y = x + y

let ``add 2 35を返す`` () = test {
  do! add 2 3 === 5
}

以上。簡単。 これを実行しても成功してしまって面白みがないので、わざと間違ってみましょう。

open Persimmon
open Persimmon.Syntax.UseTestNameByReflection
open Persimmon.MuscleAssert

let add x y = x + x // ミス!

let ``add 2 35を返す`` () = 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 関数の実装にミスがあり、textvOffsethOffset の値を使ってしまったとしましょう。 このテストを実行すると、下記のようなエラーメッセージが表示されます。

 Assertion Violated: JSONが読み込める
 1. .text.vOffset
      left  250
      right 100

textvOffset の値が左は 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のようなネストした構造に対するテストに強そうだ、というのは先ほど紹介した例で分かると思います。 XMLJSONYAMLは当然として、そもそもクラス自体が何かを内部に持っているネスト構造をしているため、ネストした構造をそのまま比較してもわかりやすいメッセージが出力されるMuscleAssertは便利です。

対してPowerAssertはこの例には貧弱です。

let ``JSONが読み込める`` () = test {
  do! read json === expected
}

このテストが失敗するとして、PowerAssertで表示されるのは

  • json 変数の中身
  • read json の結果
  • expected の中身
  • read json === expectedfalse になったということ

ですかね。 どれもドバドバと大量の出力をするわりに、本当に欲しい「どこがどう違うのか?」という情報はそこから得るのは容易ではありません。 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

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)

F# WPF

最近、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に見るコンピュテーション式のつくり方

F#

またの名を入門コンピュテーション式(嘘

この記事は、「コンピュテーション式ってどうやって作ればいいの?」に対する自分なりの回答です。

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)
  )))

無名関数のネストはありますが、疑似的にフラットに出来ました。 ghの間に何か挟まってきても、

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))))

2xに入れ、"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モナドとして広く知られています。 コンピュテーション式を作れるようになるための近道、それは色々なモナドを理解し、そのコンピュテーション式を実際に作ってみることです。

余談

モナドさえ理解できればコンピュテーション式は作れるようになるか、というとそういうわけではありません。 コンピュテーション式はモナド以上のことができてしまうため、モナドだけわかってもすべての機能の実装はできません(し、間違った実装を提供してしまいます)。

また、全然モナドじゃないコンピュテーション式も作れます。 作れますが、それが実用的になることはそうそうないでしょう。 コンピュテーション式の基本、それはモナドです。

*1:forやmatchも変数は導入できますが、ここではこれらを使っても意味がないので無視します

*2:それだけではないんですが、基本はこれです

Combine Deep Dives

F#

この記事はF# Advent Calendar 2015の17日目の記事です。

今日はコンピュテーション式の Combine について取り上げます。

詳説コンピュテーション式をある程度理解していると分かりやすいかもしれません。

内容を簡単にまとめると、

  • Delay の中で受け取った関数を実行する場合、副作用を考慮したときに問題が起こらないか考えること
  • ゼロ値がある型で Combine を実装するときは、Delay の中で受け取った関数を実行せずに、Combine の中で実行すること
  • ゼロ値がない型で Combine を実装するときは、Combine の実装は Bind に流し、ZeroM<unit> を返すように実装すること

です。

Combineの目的

Combine は、コンピュテーション式の2つの式を繋ぐために使います。 コンピュテーション式中の変換対象となる式を ce プレフィックスで表す場合、ce1; ce2 という式*1Combine を使って下記のように変換されます。

(* 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 listCombine を考えてみます。

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 optionCombine を考えてみます。

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 ()

あとは BindReturnFrom などを提供していきましょう。

ListBuilder再考

ListBuilderCombineOptionBuilder のような考慮は不要なのでしょうか? 考えてみましょう。例えば、以下のようなコードはどうなるべきでしょうか?

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))))

このように、printfnif 式の中にあるため、単純な 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 はどういうときに出てくるのでしょうか?

今までの例の共通点

今まで見てきたのは、listoption でした。 この2つの共通点はいくつかありますが、ここではゼロ値を持つ点が重要です。 list の場合は [] (空リスト)が、option の場合は None がゼロ値です。 型の定義を見てみると分かりやすいです。

type 'a list =
  | []         (* ゼロ値 *)
  | (::) of 'a * 'a list

type 'a option =
  | None       (* ゼロ値 *)
  | Some of 'a

このように、ゼロ値とそれ以外の場合でデータコンストラクタが別になっているのが分かります。 これらゼロ値は、'a がなんであろうが使えます。

さて、ではこのような「ゼロ値」がないような型を考えてみます。

Async

F#で非同期計算を表す型である Async<'T> を見てみます。 この型は listoption と違って、データコンストラクタが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)

そのため、ゼロ値はありません。 (ちなみに、FakeUnitValueunit がIL的には void に落ちてしまうため末尾最適化の対象にならない(tail. プレフィックスが発行されない)問題を回避するために導入された型であり、unit と思ってもらって構いません)

しかし、AsyncBuilderZero メソッドを次のシグネチャで提供しています。

member Zero : unit -> Async<unit>

Async<unit> 型の値はゼロ値ではありません。 例えば 'a optionNone は実際の型が int option だとしても string option だとしても使えます。 ある意味、ジェネリックな値として振る舞うのです。

それに対して、AsyncBuilderZero メソッド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 自体は実行の流れを制御できません。

AsyncBuilderZero メソッドが返す値はゼロ値ではありませんでした。 また、Combine の第一引数が Async<unit> に固定されているため、

async {
  if true then
    return "str1" (* ここにstringは置けない。unitである必要がある *)
  return "str2"
}

とは書けません。コンパイルエラーになります。 このあたりを解決するために、Stateを使ったり継続を使ったりできるかもしれませんが、Async では未検証です。 気になった方は以下のリンク群をどうぞ。

F#のコンピュテーション式を提供するライブラリ事情

yieldとreturnの話でも調べたのですが、現在の状況を調べてみました。 調べたのは下記のコードです。

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からコピーしてきたものです。 そのため、ZeroSome () になっており、Delay は渡された関数を実行しており、Combineunit option * 'T option -> 'T option 版になっています。 ZeroCombine の整合性は取れていますが、ゼロ値を使っていないのが微妙です。 また、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を使った実装をしているため、ZeroCombine もこれまで見てきたものとは全然違うシグネチャおよび実装になっています。

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 を提供したくなったときにこのエントリを思い出していただければありがたいです。 いやぁ、コンピュテーション式は楽しいなぁ。

*1:セミコロンは改行とインデントで置き換え可能

*2:一部意図的に間違っていますが、そのあたりは後述します

*3:さらに、シグネチャファイルによってただ一つのデータコンストラクタも外部には隠ぺいされている