先にお知らせ。来週の水曜日からいつも通りの 3 日ペースで記事を書けるようになるかと思います (重大インシデントの終了)。ただ、また次の週の末 (9 月初週末) からお休みします。そして次に 1 週間後ぐらいにまた再開し、9 月末ぐらいまでこんな感じでいきたいな、と。
さてさてー第 2 回です遅くなりました。
概要
- おさらい
- ReturnFrom
- 実践1: OptionBuilder
- まとめ
1. おさらい
前回は簡単な使い方という感じだったのを記憶しています。Bind と Return でしたね。覚えてますか?
前回: 今日から始める F# “Day 7” コンピューテーション式 (1) - モノトーンの伝説日記
type HogeBuilder () = member this.Bind (x, f) = f x member this.Return x = x let hoge = new HogeBuilder()
とね。ここで重要な新たな考えとして、いわゆるコンピューテーション式は「マクロ」*1なのです。難しい言葉で言うと DSL ですね。
ドメイン固有言語(ドメインこゆうげんご、英: domain-specific language、DSL)とは、特定のタスク向けに設計されたコンピュータ言語を意味する。ドメイン特化言語あるいは単にドメイン言語とも呼ばれるが、学術的にはドメイン特化言語と呼ばれることが多い。C言語やJavaのような汎用のプログラミング言語の対照とされる。
DSL は一種類のタスクをうまく実行することに集中したものであり、古くから存在したが、ドメイン固有モデリングの発達と共にDSLという用語も広く知られるようになってきた。
それでは今日の話題に行きましょう。
2. ReturnFrom
return!
で呼びだされる method。定義は:
M<'T> -> M<'T>
ちなみに return
は:
T -> M<'T>
でした。
見てもらえばわかりますが、ReturnFrom はもらった値の型そのままを返す、ということになります。
とりあえず、0 除算の場合未定義なので Option に包んで計算していく、みたいなのをコンピューテーション式を使って書いていきましょう。そのまえに Option を包むためのコンピューテーション式が必要ですが。
3. 実践1: OptionBuilder
OptionBuilder とは option を生成するためにやるもんです。とりあえず次のような式で 0 除算の場合 None を返す、というようなプログラムを考えます。
let div x y = option { let! x = x let! y = y if y <> 0 then return x / y } let div2 x y = div (Some x) (Some y)
div2 は Some でラップして渡す子です。div は完全 return ではない? ああ、unit って知ってますか? C/C++/C# の void みたいな感じですが、void はすべてが値を持ちませんね。この場合、y が 0 出なければ T<'M> だし、そうでなければ unit です。でも、このときそのまま unit が返るんじゃまずいの? って話になりますが、それは後ほど。
3.1. let!
let! は Bind に展開されると前回いましたからちゃっちゃっと書きましょう。
member this.Bind (x, f) = Option.bind f x
ちなみに Option.bind とは None の場合はそのまま None、そうでない場合 f で計算して Some にくるみます。つまり
match x with | None -> None | Some x -> f x
と同意。
くれぐれも、タプルの x と f を反対にしないように。反対にしててコンパイルエラーで???ってなってました。
3.2. return
x と y はコンピューテーション式では T に見えています。だから return を使います。ちなみにこう書けばいいと思う方もいるでしょう?
let x = Some 1 let y = Some 0 let z = x / y
無理です。こんな書き方はできないし、できないのでエレガントなコードになりません。なぜなら int option の除算は未定義です。当たり前ですよね。ですので、仮にやるとしたら
let x = Some 1 let y = Some 0 let z = match x with | None -> None | Some x -> match y with | None -> None | Some y -> if y <> 0 then Some (x / y) else None
という感じでしょうか。きれいじゃないです。
本題に戻りまして、
member this.Return x = Some x
でいいでしょう。ちなみに先ほどの M<'T> で受け取った場合の定義もしておきます。
member this.ReturnFrom x _ option = x
ちなみに _
は型推論が働きます。
3.3. unit
unit のままで返すのはまずいです。そこで Zero というのがあるのでそれを使います。定義は:
unit -> M<'T>
実装。
member this.Zero () = None
捕捉: ちなみに if cond then
で放っておくことはこの Zero の定義が必須です。unit のままで返す、といいましたが、コンピューテーション式上では Zero の定義がないと unit で帰ってきた場合の動作が未定義になるので、式として完結しないことになるので。
完成しました。それでは見ましょう。
type OptionBuilder() = member this.Bind (x, f) = Option.bind f x member this.Return x = Some x member this.ReturnFrom x: _ option = x member this.Zero () = None let option = new OptionBuilder()
これで問題なく動きます。
4. まとめ
コンピューテーション式を使って簡単な OptionBuilder を作っていきました。コンピューテーション式はマクロなので、マクロの動きを知っている必要があると今回のエントリーを通じて分かってもらえばいいんじゃないかと思います。
コードを一字一句間違えずに覚えろ、ということではありませんが、やはり動作を知らないとかけないと思います。たとえば、OptionBuilder も unit が M<'T> になる Zero が定義されているかされていないかで option {| cexpr |}
の書き方も変わるでしょうし、実際それだけ多様性があるのだから、世の中にいろいろな実装が存在します。
逆にいえば、いろいろなことができるすごい強力な機能になります。少し慣れないうちは大変だと思いますが、【マクロ(笑)】という感じで少しずつでもいいから使っていくのが面白いのかもしれません。
次回は「コンピューテーション式 (3) ~非同期とコンピューテーション式」あたり書きたいな、って思ったり。そのあとは C# の相互運用について書こうかと思ってます。それが終わった後のネタはまだ考えてないんだけども、コンピューテーション式についてはまだまだかけると思うので、一度ブレイクを置いた後再び戻ってくる感じで書こうかな、って思ってたり。
本日の記事は以上です! いろいろ詰め込んでいますが、この程度は理解してもらえるとうれしいです。難しいことは書いてないので…
*1:bleis さんのブログより。この考えがとてもわかりやすいと感じたので。詳説コンピュテーション式 - ぐるぐる~