ステップアップでわかるコンピュテーション式。TryWith や TryFinally などの実装にぜひ活用したい Delayと Run
全国1億2千万人の F# ファンの皆様いかがお過ごしでしょうか。理解できるわけもないとわかっていながらも調子に乗って「型システム入門 プログラミング言語と型の理論」を買ってしまった系の痛いおじさんです。10年後、20年後にわかることができてたらいいなくらいのノリで読んでいます。が、早くも挫折の兆しです。歩道の雪はまだまだ残ってるながらも極寒の地もようやく春めいてきました。最近やけに眠いです。春眠暁をなんとやら。プライベートな時間に勉強をするモチベーションがあまりないので、最近は映画観たりゲームばっかりやっています(下り坂)。
読む・・・!読むが・・・今回まだその時と理解度の指定まではしていない。そのことをどうか諸君らも思い出していただきたい つまり・・・我々がその気になれば理解は10年後、20年後ということも可能だろう・・・ということ・・・!
自分が理解していることについてちょっと書いてみたい
わたしの誤字ツイートがかなり恥ずかしい感じですが、こんなやり取りがありました。
@igeta Mr.えふしゃーぷサマサマですね。ところで、Stateモナ..コンピューテーション式の記事はかどってますか
2013-03-29 15:52:32 via Silver Bird Plus to @igeta
@zecl 書くかどうか、というか書けるかどうかわかりませんが、書くんだったらコンピュテーション式のモッナード側面全般の記事にしたいっすね。
@zecl DelayとRunを使ったほうが楽になる理由ってなんでしょうか?利点があればぜひ知りたいです。
2013-03-29 20:58:15 via HootSuite to @zecl
F# 専用の DBアクセスライブラリ「Tranq」を開発なさっている、ナイス F#er のなかむらさん(@nakamura_to)にリアクションを頂いたので、ある程度の前提知識がある方向け(?)にちょっと書いてみます。ガブさん(@gab_km)の「できる!コンピュテーション式」あたりを理解していることが望ましいです。モナド的なお話はあまりありません。モッナード側面のお話はいげ太さん(@igeta)がしてくれるらしい(?)
Delay と Run を使ったほうがなぜ楽になるのか。その答えは「なぜ Delayメソッドがコンピュテーション式の基本メソッドとして提供されているのか。」の理由を紐解くとおのずと見えてきます。結論から言うと、「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の部分などは、コンピュテーション式そのものが評価された後で評価されて欲しいからです。その答えはF#の言語仕様およびコンパイラのソースコードの中にあります。Delay には、他のメソッドとは少し違う特徴があります。この記事では Maybeモナドを例に、コンピュテーション式が標準でサポートする各メソッドはなんのために用意されているのか? その明確な意味について、順を追ってゆる〜く見て行きたいと思います。「ステップアップでわかるコンピュテーション式」的な何かです。
Step1 : 背景にモナドを持つ Bind と Return メソッド
Bind と Return メソッド、その背景にはモナドがあります。
ですが、"モナドとはまったく関係なく自由に利用することができるメソッドである" ということをあらかじめ申し上げておきます。
くどいと思われる方は読みとばしていただいて結構です。Delay と TryWith 関連の詳細な話題は Step5 以降で扱っています。
Step2 : 一手間を省く ReturnFromメソッド
Step3 : コンピュテーション式の流れをコントロールする Delay と Combineメソッド
Step4 : 「if式のelseを省略したい!」 Zeroメソッド
Step5 : 「try...with 式を使いたい!」 TryWithメソッド
Step6 : 「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の評価を遅延する
Step7 : 最終的なコンピュテーション式の型を決定する Runメソッド
Step8 : 「try...finaly式を使いたい!」TryFinallyメソッド
寄り道 : F#3.0のコンパイラのソースコードを読もう
Step9 : 「use 束縛を使いたい!」Usingメソッド
Step10 : 「while...do式を使いたい!」Whileメソッド
Step11 : 「for...do式を使いたい!」Forメソッド
コンピュテーション式で Maybeモナド を表現すると、以下のようになります。
type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a let maybe = new MaybeBuilder()
いわゆる「ビルダークラス」と呼ばれるクラスを定義し、そのインスタンスを使ってコンピュテーション式として使えるようにしたものです。たとえば以下のように Maybe の文脈の計算を簡単に、しかも読みやすく書くことができるようになります。
maybe { let! a = Some 15 let! b = Some 20 return a + b } |> printfn "%A" // Some 35 maybe { let! a = Some "F#!" return a + a } |> printfn "%A" // Some "F#!F#!"
Bindメソッドは、コンピュテーション式の let! および do! に対して呼び出されるものです。let! キーワードで値を束縛して次の計算に渡したり、do! キーワードで処理を行えるようになります。Returnメソッドは、コンピュテーション式の return に対して呼び出されるものです。Maybeモナド のようにコンテナ(ここではOption型)を扱うような場合、いわゆるコンテナに値を包むような操作を定義します*1。Bindメソッドは、Haskellの Monad の(>>=)演算子に。Returnメソッドは、Haskellの Monad の return に対応するように意識されて用意されたものです。
class Monad m where (>>=) :: m a -> (a -> m b) -> m b return :: a -> m a
コンピュテーション式 (F#) - MSDNライブラリのページでは、Bind と Retrun のシグネチャは以下のように示されています。
http://msdn.microsoft.com/ja-jp/library/vstudio/dd233182.aspx
引数と対応付けて書くと、こうですね。
builder.Bind(m:M<'T>, f:('T -> M<'U>)) : M<'U> builder.Return(a:'T) : M<'T>
もちろんMaybeBuilderはこのシグネチャにそっています。
type MaybeBuilder() = member b.Bind(m:option<'a>, f:('a -> option<'b>)) : option<'b> = Option.bind f m member b..Return(a:'a) : option<'a> = Some a
F# には Haskellでいう型クラスはありません。ですから、Haskell の m a と F# の M<'T> はまったく同じものを意味するわけではありませんが、
コンピュテーション式が提供された背景として、Haskell の Monad があることがわかります。
コンピュテーション式は、モナドを表現するのにとても都合がいいように設計されています。でも実際には、Bind や Return などのコンピュテーション式で利用可能な各メソッドのシグネチャにそのような制約はありません。ビルダークラスの各メソッドの定義には、引数の数のみが制限されます。なので、わりかし自由度の高い計算の構築ができるようになっています。制限がゆるいおかげでいろいろ好き勝手ができます。コンピュテーション式はモナドだけのための構文ではないのです。
type HogeBuilder() = member b.Return(a:int->int) : int -> int = a member b.Bind(m:int -> int, f: (int -> int)-> (int -> int)) = f m let hoge = new HogeBuilder() hoge { let! a = fun x -> 5 + x let! b = fun x -> 20 - x return a >> b } |> fun f-> f 0 |> printfn "%A" // 15
自由とはときとして不自由である。という話もあります。
Step2 : 一手間を省く ReturnFromメソッド
ReturnFrom は定義をしなくても別段困ることはありませんが、あると便利です。
type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a // add member b.ReturnFrom(m) = m let maybe = new MaybeBuilder()
maybe { return! None } |> printfn "%A" // <null> ※None maybe { let! a = Some "F# is fun!" return a } |> printfn "%A" // Some "F# is fun!" maybe { return! Some "F# is fun!" } |> printfn "%A" // Some "F# is fun!"
一度 Bind (!let)で受けてから、Return(return)をするという手間を省くことができるようになります。
コンピュテーション式をより書きやすくするためにあるもの。という位置づけのものと考えて差し支えないでしょう。
Step3 : コンピュテーション式の流れをコントロールする Delay と Combineメソッド
コンピュテーション式で、「if式を使いたい!」というモチベーションが発生したら、Combineメソッドを実装しましょう。
コンピュテーション式の式の流れをコントロールしたい場合は、Combineメソッドを実装しましょう。その名の意味のとおり、計算式を結合するためのものです。
※コメントでNobuhisaさん(@nobuhisa_k)からツッコミをいただきました。ありがとうございます!
修正前、Combineメソッドを実装していないと、コンピュテーション式の中で if式そのものが利用できないという誤解を与える記述がありましたが、それは誤りです。
if式をただ使うだけであれば、Combineメソッドを実装する必要はありません。以下のように利用することができます。
type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a member b.ReturnFrom(m) = m let maybe = new MaybeBuilder()
maybe { let! c = Some "C#" let! fs = Some "F#" let! vb = Some "VB" let! cpp = Some "C++" if (fs > vb) && (vb > c) then return vb elif c > vb then return c else return fs} |> printfn "%A" // Some "F#"
しかし、if式の後にも式を続けて記述したい場合はどうでしょう?
if 式だけで式の流れが完結している場合は問題ありませんが、コンピュテーション式の中で式の流れの制御が必要になった場合、Combineメソッドの実装が必要となります。
これは if式の利用に限ったことではなく、極端な話、以下のような記述をするためには Combineメソッドを実装する必要があります。
type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a member b.ReturnFrom(m) = m // add member b.Combine(x, y) = x |> ignore; y let maybe = new MaybeBuilder()
これは一体どういうことえだってばよ!?
F#3.0の言語仕様によると、Combineはコンパイラによって以下のように展開されるので、Delayが必須だということですね。
T(ce1; ce2, V, C, q) = C(b.Combine({| ce1 |}0, b.Delay(fun () -> {| ce2 |}0)))
6.3.10Computation Expressions - The F# 3.0 Language Specification
http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/spec.html#_Toc335818835
type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a member b.ReturnFrom(m) = m member b.Combine(x, y) = x |> ignore; y // add member b.Delay (f) = f() let maybe = new MaybeBuilder()
maybe { let! c = Some "C#" let! fs = Some "F#" let! vb = Some "VB" let! cpp = Some "C++" if (fs > vb) && (vb > c) then return vb elif c > vb then return c else return cpp return fs } |> printfn "%A" // Some "F#" maybe { let! c = Some "C#" let! fs = Some "F#" let! vb = Some "VB" let! cpp = Some "C++" return c return vb return cpp return fs} |> printfn "%A" // Some "F#"
コンピュテーション式の計算式の流れは、上から下へ(左から右へ)流れています、その流れを制御するのが Combineメソッドです。Combineメソッドは、Haskell の MonadPlus の mplus にあたるものとして解釈される場合もありますが、コンピュテーション式の中の式の流れを制御するためのものと理解するとよいでしょう。
Step4 : 「if式のelseを省略したい!」 Zeroメソッド
コンピュテーション式で、「if式のelse以下を省略したい!」というモチベーションが発生したら、Zeroメソッドを実装しましょう。
type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a member b.ReturnFrom(m) = m member b.Combine(x, y) = x |> ignore; y member b.Delay (f) = f() // add member b.Zero() = None let maybe = new MaybeBuilder()
maybe { if false then return "F#" } |> printfn "%A" // <null> ※None maybe { if true then return "F#" } |> printfn "%A" // Some "F#"
コンピュテーション式の計算式の流れのなかで式が省略されたとき、None が流れていることがわかります。Zeroメソッドは、Haskell の MonadPlus の mzero の意味として解釈される場合もありますが、コンピュテーション式においては、上(左)の計算式が省略された場合に利用される既定値を表すものと理解するとよいでしょう。
Step5 : 「try...with 式を使いたい!」 TryWithメソッド
コンピュテーション式で、「try...with 式を使いたい!」というモチベーションが発生したら、TryWithメソッドを実装しましょう。
type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a member b.ReturnFrom(m) = m member b.Combine(x, y) = x |> ignore; y member b.Delay(f) = f() member b.Zero() = None // add member b.TryWith (m, hander) = try m with ex -> hander ex let maybe = new MaybeBuilder()
maybe { let! x = None try return x / 0 with | e -> return 0 } |> printfn "%A" // <null> ※None maybe { let! x = Some 10 try return x / 0 with | ex -> printf "%s" ex.Message return 0 } |> printfn "%A" // 例外が発生するがキャッチできない
「0 で除算しようとしました。Some 0」 と出力されてされて欲しいところですが、コンピュテーション式そのものの評価がされる前に、0除算が先に評価されてしまっている。Delay、TryWith いずれのメソッドのシグネチャも、MSDN に書いてある通りに実装したのに、この有様です。それもそのはず。「member b.Delay(f:unit-> option<'a>) = f() 」を見れば明らか。そもそも Delay されていた処理を、即時評価してしまっているのですから、こうなります。
builder { cexpr } は、どのように展開されるか? これについては、本家の F#3.0 の言語仕様を参照しましょう。
6.3.10Computation Expressions - The F# 3.0 Language Specification
http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/spec.html#_Toc335818835
Combineメソッドのときもそうでしたが、コンピュテーション式がコンパイラによって変換されたときに、自動的に Delay メソッドが挿入されるタイミングが他にもいくつかある。それが以下のとおり、
T(e, V, C, q) where e : the computation expression being translated
V : a set of scoped variables
C : continuation (or context where “e” occurs,
up to a hole to be filled by the result of translating “e”)
q : Boolean that indicates whether a custom operator is allowed
T(while e do ce, V, C, q) = T(ce, V, lv.C(b.While(fun () -> e, b.Delay(fun () -> v))), q)
T(try ce with pi -> cei, V, C, q) =
Assert(not q); C(b.TryWith(b.Delay(fun () -> {| ce |}0), fun pi -> {| cei |}0))T(try ce finally e, V, C, q) =
Assert(not q); C(b.TryFinally(b.Delay(fun () -> {| ce |}0), fun () -> e))T(ce1; ce2, V, C, q) = C(b.Combine({| ce1 |}0, b.Delay(fun () -> {| ce2 |}0)))
これが示しているのは、while...do 式に対応する、Whileメソッド。try...with 式に対応するTryWithメソッド。try...finally式に対応するTryFinallyメソッド。そして、式の流れをコントロールする Combineメソッドの4つの式を変換するときに、暗黙的にDelayメソッドの呼び出しが挿入されることを意味している。F# は正確評価の言語なので、「member b.Delay(f:unit-> option<'a>) : unit -> option<'a> = f 」という様に、この時点では評価をせずにそのまま式を遅延した状態を維持しないと、コンピュテーション式そのものが評価される前に「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の部分が評価されてしまうので、うまくない。ということです。MSDNで示されているシグネチャは、標準的なモナドベースで書かれていて、実用的なコンピュテーション式の書き方については言及していない感があり、わかりにくいところがあります。
これを踏まえて、次のステップへ行ってみましょう。
Step6 : 「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の評価を遅延する
Step5 を踏まえて、次のように実装を変更してみましょう。
type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a member b.ReturnFrom(m) = m member b.Zero() = None // modify member b.Delay(f:unit -> option<'a>) = f member b.TryWith (c:unit -> option<'a>, hander:exn -> option<'a>) = try c() with ex -> hander ex member b.Combine(x:option<'a>, y:unit -> option<'a>) = if Option.isSome x then x else y() let maybe = new MaybeBuilder()
TryWithメソッドだけではなく、Combineメソッドにも変更が必要ということに注目です。
Delayメソッドの中で処理の遅延をそのままにするかわりに、「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の部分で、unit を適用して評価するように実装します。TryWith では、式が上から下(左から右)へ流れます。その式が Combineメソッドでも遅延されているので、このとき評価するように実装します。
let a : unit -> option<int> = maybe { let! x = None try return x / 0 with | e -> return 0 } a |> printfn "%A" // <fun:a@16> a () |> printfn "%A" // <null> ※None let b : unit -> option<int> = maybe { let! x = Some 10 try return x / 0 with | ex -> printf "%s" ex.Message return 0 } b |> printfn "%A" // <fun:b@26> b () |> printfn "%A" // 0 で除算しようとしました。Some 0
となります。try...with式が思った通りの挙動をするようになりました。
しかし、Delay によって unit を引数にとる関数になったままだと、どうも具合がよくありません。これは、Step7 で解決します。
Step7 : 最終的なコンピュテーション式の型を決定する Runメソッド
Step5 で、評価が遅延されるべき計算は、Delayメソッドによって包まれている(unitを引数にとる関数として)ことがわかりました。
しかし、遅延されたままだと具合がよくありません。そこで、Runメソッドの出番です。
6.3.10Computation Expressions - The F# 3.0 Language Specification
http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/spec.html#_Toc335818835
let b = builder-expr in b.Run (<@ {| cexpr |}C >@)
F#3.0 の言語仕様にありますように、Runメソッドは、ビルダークラスが評価されるときに呼びだされます。
つまり、「member b.Delay(f:unit-> option<'a>) = f 」に対して必要なRunメソッドの実装は「member b.Run(f:unit-> 'c) : 'c = f()」という感じになります。
type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a member b.ReturnFrom(m) = m member b.Combine(x, y) = if Option.isSome x then x else y() member b.Zero() = None member b.Delay(f) = f member b.TryWith (c, hander) = try c() with ex -> hander ex // add member b.Run(f) = f() let maybe = new MaybeBuilder()
maybe { let! x = None try return x / 0 with | e -> return 0 } |> printfn "%A" // <null> maybe { let! x = Some 10 try x / 0 |> printf "%d" with | ex -> printf "%s" ex.Message return "ここまで" } |> printfn "%A" // 0 で除算しようとしました。Some "ここまで"
ということで、このツイートで言いたかったのはこういうことでした。
大した話でもないのにダラダラと書いてしまいました。でもまだ続きもあるのでよろしければ。
Step8 : 「try...finally式を使いたい!」TryFinallyメソッド
さて、try...finally式も利用できるように実装してみましょう。Step6 と Step7を踏まえれば難しくないですね。
また、DelayメソッドとRunメソッドがどのように呼ばれているのかを確認するために、printf を入れてみました。
type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a member b.ReturnFrom(m) = m member b.Combine(x, y) = if Option.isSome x then x else y() member b.Zero() = None member b.TryWith (c, hander) = try c() with ex -> hander ex // add member b.TryFinally (c, f) = try c() finally f() // modify member b.Delay(f) = #if DEBUG printf "%s" "delay;" #endif f member b.Run(f) = f() |> fun x -> #if DEBUG printf "%s" "run;" #endif x let maybe = new MaybeBuilder()
maybe { try try printf "%s" "try" finally printf "%s" "finally1;" finally printf "%s" "finally2;" return "ここまで" } |> printfn "%A" // Debug : delay;delay;delay;tryfinally1;finally2;delay;run;Some "ここまで" // Relese: tryfinally1;finally2;Some "ここまで"
Debugモードで実行すると、「delay;delay;delay;tryfinally1;finally2;delay;run;Some "ここまで"」と出力されます。try...finally式を2回使っています。ここでDelayが2回呼ばれます。外側のtry...finally式で unit が返されますので、その後で既定値となる Zeroメソッドが呼ばれます。そのZeroメソッドで返された「None」と、 「return "ここまで"」がCombineメソッドで結合されるので、ここでも Delayが呼ばれます。Delayが呼ばれるのは、合計3回ですか? でも、実際には合計4回呼ばれています。どういうことだってばよ?
このあたりのことは、言語仕様を読めばちゃんと書いてありますね。
6.3.10Computation Expressions - The F# 3.0 Language Specification
http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/spec.html#_Toc335818835
let b = builder-expr in b.Run (<@ b.Delay(fun () -> {| cexpr |}C) >@)
ビルダークラスに Delayメソッドが実装されているときに限り、上記のように評価されるのです。いつDelayメソッドが呼ばれるかを把握しておくことは大事です。暗黙的にDelayメソッドが呼ばれるタイミングは、Whileメソッド、TryWithメソッド、TryFinallyメソッド、Combineメソッド、Runメソッド、の5つです。これ以外にDelayメソッドが呼ばれるタイミングは、自分で明示的に呼ぶように実装した場合のみになります。ここテストに出ます!ちなみに、Runメソッドについてもビルダークラスに実装されている場合に限り呼び出しが行われます。
寄り道 : F#3.0のコンパイラのソースコードを読もう
Step9へ行く、その前にちょっと寄り道。F#3.0のコンパイラのソースコードを読もうのコーナー。
F#3.0のコンパイラのコンピュテーション式の分析をしているところのソースコードについて、いくつかピックアップして見てみる。
http://fsharppowerpack.codeplex.com/SourceControl/changeset/view/71313#1230866
まずはここらへん。ふむふむなるほど。コンピュテーション式の呼び出しを生成している関数のようです。
/// Make a builder.Method(...) call let mkSynCall nm (m:range) args = let m = m.MakeSynthetic() // Mark as synthetic so the language service won't pick it up. let args = match args with | [] -> SynExpr.Const(SynConst.Unit,m) | [arg] -> SynExpr.Paren(SynExpr.Paren(arg,range0,None,m),range0,None,m) | args -> SynExpr.Paren(SynExpr.Tuple(args,[],m),range0,None,m) let builderVal = mkSynIdGet m builderValName mkSynApp1 (SynExpr.DotGet(builderVal,range0,LongIdentWithDots([mkSynId m nm],[]), m)) args m
Whileメソッドを生成するとき、mkSynCall関数の引数に"Delay"が渡されているのか確認できます。ここで自動的に Delayメソッドが呼び出される構成が作られているんですね。
| SynExpr.While (spWhile,guardExpr,innerComp,_) -> let mGuard = guardExpr.Range let mWhile = match spWhile with SequencePointAtWhileLoop(m) -> m | _ -> mGuard if isQuery then error(Error(FSComp.SR.tcNoWhileInQuery(),mWhile)) if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env mWhile ad "While" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("While"),mWhile)) if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env mWhile ad "Delay" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("Delay"),mWhile)) Some(trans true q varSpace innerComp (fun holeFill -> translatedCtxt (mkSynCall "While" mWhile [mkSynDelay2 guardExpr; mkSynCall "Delay" mWhile [mkSynDelay innerComp.Range holeFill]])) )
同様に、TryWithメソッド。
| SynExpr.TryWith (innerComp,_mTryToWith,clauses,_mWithToLast,mTryToLast,spTry,_spWith) -> let mTry = match spTry with SequencePointAtTry(m) -> m | _ -> mTryToLast if isQuery then error(Error(FSComp.SR.tcTryWithMayNotBeUsedInQueries(),mTry)) if q then error(Error(FSComp.SR.tcTryWithMayNotBeUsedWithCustomOperators(),mTry)) let clauses = clauses |> List.map (fun (Clause(pat,cond,clauseComp,patm,sp)) -> Clause(pat,cond,transNoQueryOps clauseComp,patm,sp)) let consumeExpr = SynExpr.MatchLambda(true,mTryToLast,clauses,NoSequencePointAtStickyBinding,mTryToLast) if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env mTry ad "TryWith" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("TryWith"),mTry)) if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env mTry ad "Delay" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("Delay"),mTry)) Some(translatedCtxt (mkSynCall "TryWith" mTry [mkSynCall "Delay" mTry [mkSynDelay2 (transNoQueryOps innerComp)]; consumeExpr]))
同様に、TryFinallyメソッド。
| SynExpr.TryFinally (innerComp,unwindExpr,mTryToLast,spTry,_spFinally) -> let mTry = match spTry with SequencePointAtTry(m) -> m | _ -> mTryToLast if isQuery then error(Error(FSComp.SR.tcNoTryFinallyInQuery(),mTry)) if q then error(Error(FSComp.SR.tcTryFinallyMayNotBeUsedWithCustomOperators(),mTry)) if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env mTry ad "TryFinally" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("TryFinally"),mTry)) if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env mTry ad "Delay" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("Delay"),mTry)) Some (translatedCtxt (mkSynCall "TryFinally" mTry [mkSynCall "Delay" mTry [mkSynDelay innerComp.Range (transNoQueryOps innerComp)]; mkSynDelay2 unwindExpr]))
同様に、Combineメソッド。
| SynExpr.Sequential(sp,true,innerComp1,innerComp2,m) -> // Check for 'where x > y' and other mis-applications of infix operators. If detected, give a good error message, and just ignore innerComp1 if isQuery && checkForBinaryApp innerComp1 then Some (trans true q varSpace innerComp2 translatedCtxt) else if isQuery && not(innerComp1.IsArbExprAndThusAlreadyReportedError) then match innerComp1 with | SynExpr.JoinIn _ -> () // an error will be reported later when we process innerComp1 as a sequential | _ -> errorR(Error(FSComp.SR.tcUnrecognizedQueryOperator(),innerComp1.RangeOfFirstPortion)) match tryTrans true false varSpace innerComp1 id with | Some c -> // "cexpr; cexpr" is treated as builder.Combine(cexpr1,cexpr1) // This is not pretty - we have to decide which range markers we use for the calls to Combine and Delay // NOTE: we should probably suppress these sequence points altogether let m1 = match innerComp1 with | SynExpr.IfThenElse (_,_,_,_,_,mIfToThen,_m) -> mIfToThen | SynExpr.Match (SequencePointAtBinding mMatch,_,_,_,_) -> mMatch | SynExpr.TryWith (_,_,_,_,_,SequencePointAtTry mTry,_) -> mTry | SynExpr.TryFinally (_,_,_,SequencePointAtTry mTry,_) -> mTry | SynExpr.For (SequencePointAtForLoop mBind,_,_,_,_,_,_) -> mBind | SynExpr.ForEach (SequencePointAtForLoop mBind,_,_,_,_,_,_) -> mBind | SynExpr.While (SequencePointAtWhileLoop mWhile,_,_,_) -> mWhile | _ -> innerComp1.Range if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env m ad "Combine" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("Combine"),m)) if isNil (TryFindIntrinsicOrExtensionMethInfo cenv env m ad "Delay" builderTy) then error(Error(FSComp.SR.tcRequireBuilderMethod("Delay"),m)) Some (translatedCtxt (mkSynCall "Combine" m1 [c; mkSynCall "Delay" m1 [mkSynDelay innerComp2.Range (transNoQueryOps innerComp2)]]))
Step5 にあったとおりコンピュテーション式の中で、try...with式を利用するためには、TryWithメソッドに加えて、Delayメソッドも実装しなければなりませんでした。例えば TryWithメソッドがビルダークラスに実装されている場合、TryWithメソッドの呼び出しの中で Delayメソッドの呼び出しがコンパイラによって暗黙的に挟み込まれるということがわかります。「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の部分にちょうど挿入されます。これは「このタイミングで挿入される Delayメソッドの呼び出しを利用して処理を遅延してね!」といわんばかりです。つまり、MSDNで示されている「通常のシグネチャ」はガン無視していただいて構わないということです。
ところで、ビルダークラスが評価されるタイミングについても、同じような挙動をするのでした。Runメソッドが実装されている場合のみ Runメソッドが呼び出されるようコンパイラさんがよろしくやってくれて、そのRunメソッドの中で、Delayメソッド呼ばれるかどうかについても、Delayメソッドが実装されているか否かによって判断されると、言語仕様に書いてありました。コンパイラさんのことは、コンパイラさんが一番よく知っているって、じっちゃんが言ってた。
関係ありそうなところを以下にピックアップ。
let mkSynDelay2 (e: SynExpr) = mkSynDelay (e.Range.MakeSynthetic()) e let delayedExpr = match TryFindIntrinsicOrExtensionMethInfo cenv env mBuilderVal ad "Delay" builderTy with | [] -> basicSynExpr | _ -> mkSynCall "Delay" mBuilderVal [(mkSynDelay2 basicSynExpr)] let quotedSynExpr = if isAutoQuote then SynExpr.Quote(mkSynIdGet (mBuilderVal.MakeSynthetic()) (CompileOpName "<@ @>"), (*isRaw=*)false, delayedExpr, (*isFromQueryExpression=*)true, mWhole) else delayedExpr let runExpr = match TryFindIntrinsicOrExtensionMethInfo cenv env mBuilderVal ad "Run" builderTy with | [] -> quotedSynExpr | _ -> mkSynCall "Run" mBuilderVal [quotedSynExpr]
runExprを見ると、Runメソッドがビルダークラスに実装されている場合のみ Runの呼び出しが生成され、同じく delayedExprを見ると、Delayメソッドが実装されている場合のみ 内部にDelay の呼び出しが生成される、と。
実際そのようになっているようです。コンパイラのソースを見れば...、いろいろとわかる(こともある)。オープンソースな F# いいね!
Step9 : 「use 束縛を使いたい!」Usingメソッド
少しばかり寄り道をしてしまいましたが、気を取り直して最後まで一気に駆け抜けましょう。
「use 束縛を使いたい!」そんなあなたは、Usingメソッドを実装しましょう。use 束縛は、C# や VBの using ステートメントにあたる働きをします。.NET開発者であれば、ご存知のとおり、IDisposableインターフェイスを実装しているオブジェクトについてリソースを解放をする働きがあるものです。
open System type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a member b.ReturnFrom(m) = m member b.Combine(x, y) = if Option.isSome x then x else y() member b.Zero() = None member b.Delay(f) = f member b.TryWith (c, hander) = try c() with ex -> hander ex member b.Run(f) = f() member b.TryFinally (c, f) = try c() finally f() // add member b.Using(res:#IDisposable, body:#IDisposable -> option<'a>) : option<_>= b.TryFinally((fun ()-> body res), fun () -> match res with null -> () | x -> x.Dispose()) let maybe = new MaybeBuilder()
リソースの解放は、use キーワードで束縛した値のスコープが外れた場合。つまり式が最後まで評価された、あるいは例外が発生してスコープを外れた場合が考えられます。この実装には、先ほど実装した TryFinallyメソッドをそのまま利用することができます。TryFinallyメソッド使って簡単に実装しましょう。もちろん、「{| try cexpr with | pattern_i -> expr_i |}」の cexpr の部分には、コンパイラによって Delayメソッドの呼び出しが暗黙的に挿入されることになるので処理の呼び出しが遅延されます。
let createDisposable f = { new IDisposable with member x.Dispose() = f() } maybe { use res = createDisposable (fun () -> printf "%A" "Disposeされたよ;") return "おわり" } |> printfn "%A" // "Disposeされたよ";Some "おわり" maybe { try use outp = IO.File.CreateText(@"C:\test\playlist.txt") outp.WriteLine("口がすべって") outp.WriteLine("君が好き") outp.WriteLine("言わせてみてぇもんだ") with ex -> printf "%A" ex } |> printfn "%A" // <null>
use 束縛は、let 束縛と同じ機能が提供されているだけでなく、束縛した値がスコープの外に出ると対象のオブジェクトの Dispose が呼び出されます。
また、コンパイラによって値の null チェックが挿入されますので、値が null の場合には Dispose の呼び出しは行われません。
Step10 : 「while...do式を使いたい!」Whileメソッド
コンピュテーション式で、「while...do式を使いたい!」という場合は、Whileメソッドを実装します。
open System type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a member b.ReturnFrom(m) = m member b.Combine(x, y) = if Option.isSome x then x else y() member b.Zero() = None member b.Delay(f) = f member b.TryWith (c, hander) = try c() with ex -> hander ex member b.Run(f) = f() member b.TryFinally (c, f) = try c() finally f() member b.Using(res:#IDisposable, body:#IDisposable -> option<'a>) : option<_>= b.TryFinally((fun ()-> body res), fun () -> match res with null -> () | x -> x.Dispose()) // add member b.While(guard:unit -> bool, f:unit -> option<'a>) = if not (guard()) then b.Zero() else b.Bind(f(), fun _ -> b.While(guard, f)) let maybe = new MaybeBuilder()
実装をご覧いただくとわかるように、Zeroメソッド、Bindメソッド、Whileメソッド(自身)を呼び出しています。指定した条件が falseとなったときループを抜けて、上から下(左から右)へ計算式が流れていきます。ここで Zeroメソッドを呼び出すことで既定値の計算式を下(右)へ流しています。条件が true の場合は、上(左)から流れてきた計算式の計算結果と、以後に継続されるループを表す式を 、Bindメソッドによって束縛と関数適用を繰り返すことで計算を再帰的につなぎ合わせています。ループの実装は、対象とする文脈によって特に実装内容が大きく異なってきますが、基本的にはこのような流れで実装することになるでしょう。
maybe { let i = ref 0 while !i < 10 do printf "%d;" !i incr i } |> printfn "%A" // 0;<null> maybe { let i = ref 0 while !i < 10 do printf "%d" !i incr i return! Some (printf "%s" "個;" ) return "ここまで" } |> printfn "%A" // 0個;1個;2個;3個;4個;5個;6個;7個;8個;9個;Some "ここまで" maybe { let i = ref 10 while !i >= 0 do try printf "%d;" <| 10 / !i decr i return () with | ex -> printf "%s" ex.Message return 999 } |> printfn "%A" // 1;1;1;1;1;2;2;3;5;10;0 で除算しようとしました。Some 999
Step11 : 「for...do式を使いたい!」Forメソッド
コンピュテーション式で、「for...do式を使いたい!」というニーズは結構多いのではないかと思います。
これまでのStepの中で実装してきたメソッドを活かしながら Forメソッドを実装してみましょう。
open System type MaybeBuilder() = member b.Bind(m, f) = Option.bind f m member b.Return(a) = Some a member b.ReturnFrom(m) = m member b.Combine(x, y) = if Option.isSome x then x else y() member b.Zero() = None member b.Delay(f) = f member b.TryWith (c, hander) = try c() with ex -> hander ex member b.Run(f) = f() member b.TryFinally (c, f) = try c() finally f() member b.Using(res:#IDisposable, body:#IDisposable -> option<'a>) : option<_>= b.TryFinally((fun ()-> body res), fun () -> match res with null -> () | x -> x.Dispose()) member b.While(guard:unit -> bool, f:unit -> option<'a>) = if not (guard()) then b.Zero() else b.Bind(f(), fun _ -> b.While(guard, f)) // add member this.For(sequence:#seq<_>, body) = this.Using(sequence.GetEnumerator(), fun enum -> this.While(enum.MoveNext, (fun () -> body enum.Current))) let maybe = new MaybeBuilder()
シーケンスのリソースは解放する必要があるので、実装済みのUsingメソッドを利用します。ループの表現には先程実装したWhileメソッドを利用します。Usingメソッドと、Whileメソッドを利用して、Forを実装することができました。
maybe { for i in [1..5] do printf "%d;" i return "Combineで式が結合されてコレは捨てられます" return "おしまい" } |> printfn "%A" // 1;2;3;4;5;Some "おしまい" maybe { for i in [1..5] do return printf "%d;" i return "おしまい" } |> printfn "%A" // 1;2;3;4;5;Some "おしまい"
残るは、Yieldメソッド と YieldFromメソッドの実装。と言いたいところですが、Maybeモナドの文脈では、Yieldメソッドおよび YieldFromメソッドを実装する意味はありませんので省略します*2。ということで、すべてのステップが終了しました。これでおしまいです。以上、「ステップアップでわかるコンピュテーション式」でした。何かの参考になれば幸いです。
おまけ:FsControlを拡張してお遊び
Gustavo Leon氏(@gmpl)のGJ(グッジョブ)であるところの、fsharp-typeclasses
https://code.google.com/p/fsharp-typeclasses/
を、新しく構成しなおしたプロジェクト FsControl が面白くてニヤニヤしながらたまーに見ていたりします。
https://github.com/gmpl/FsControl
そのキモとなるのが、InlinHelperモジュール。
[<AutoOpen>] module InlineHelper module Overloads = let inline instance_1 (a:^a ) = ( ^a : (static member instance: ^a -> _) (a )) let inline instance_2 (a:^a,b:^b ) = ((^a or ^b ) : (static member instance: ^a* ^b -> _) (a,b )) let inline instance_3 (a:^a,b:^b,c:^c ) = ((^a or ^b or ^c ) : (static member instance: ^a* ^b* ^c -> _) (a,b,c )) let inline instance_4 (a:^a,b:^b,c:^c,d:^d ) = ((^a or ^b or ^c or ^d ) : (static member instance: ^a* ^b* ^c* ^d -> _) (a,b,c,d )) let inline instance_5 (a:^a,b:^b,c:^c,d:^d,e:^e ) = ((^a or ^b or ^c or ^d or ^e ) : (static member instance: ^a* ^b* ^c* ^d* ^e -> _) (a,b,c,d,e )) let inline instance_6 (a:^a,b:^b,c:^c,d:^d,e:^e,f:^f) = ((^a or ^b or ^c or ^d or ^e or ^f) : (static member instance: ^a* ^b* ^c* ^d* ^e* ^f -> _) (a,b,c,d,e,f)) open Overloads type Inline = Inline with static member inline instance ( ) = fun (x:'x) -> instance_1( Unchecked.defaultof<'r>) x :'r static member inline instance (a:'a ) = fun (x:'x) -> instance_2(a ,Unchecked.defaultof<'r>) x :'r static member inline instance (a:'a, b:'b ) = fun (x:'x) -> instance_3(a,b ,Unchecked.defaultof<'r>) x :'r static member inline instance (a:'a, b:'b, c:'c ) = fun (x:'x) -> instance_4(a,b,c ,Unchecked.defaultof<'r>) x :'r static member inline instance (a:'a, b:'b, c:'c, d:'d ) = fun (x:'x) -> instance_5(a,b,c,d ,Unchecked.defaultof<'r>) x :'r static member inline instance (a:'a, b:'b, c:'c, d:'d, e:'e) = fun (x:'x) -> instance_6(a,b,c,d,e,Unchecked.defaultof<'r>) x :'r
これがなんとも黒魔術的であり、メシウマ状態であり。その一方で"今の F#"の限界を感じたり。
で、FsControl では、fsharp-typeclasses にあった、DoNotationBuilderを提供する do' が internalで宣言されていたり、DoPlusNotationBuilderとそのインスタンスを提供する doPlus がなくなっていたり、いろいろ変更が加えられている。また、FsControlというプロジェクトからは、なみなみならぬ Haskell愛 を感じるわけだけど、F# 愛の成分が不足しているように感じる。というのも、この記事で取り上げた コンピュテーション式内で利用できる for式 や try...with 式などの Haskell にはない機能についての利用は考えられていないからだ。FsControl はあくまで Haskell 的な関数型プログラミングのエミュレートを提供するという思想で作られているのかもしれない、とかなんとか。
ということで、お遊び程度でちょっとゴニョゴニョしてみました。なんか表示が崩れてしまうので埋め込みはしません。
https://gist.github.com/zecl/5280535
Whileメソッド以下については、ちょっと工夫しないと厳しそうな気がします。なので宿題とします(キリッ