~スマートフォン用の表示で見るスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。読者です読者をやめる読者になる読者になるぐるぐる~2014-01-27詳説コンピュテーション式F#コンピュテーション式とはコンピュテーション式とは、機能を制限したマクロです。「式変形によって言語の用意する構文の意味をカスタマイズできるようにする仕組み」です。モナド用の構文として紹介されることもありますが、それはコンピュテーション式という仕組みの上でモナドを扱っているだけに過ぎません。コンピュテーション式はモナド用の構文として使うことが一番多いでしょうから、モナド用の構文と理解しても問題はないでしょう。現状では「これはモナドではありません」という表明をドキュメントなりなんなりでしておくのが無難でしょう。let!とreturnを提供する場合でコンピュテーション式をモナドではない構文とする場合は、うるさいくらいその旨を伝えるようにしましょう。作ったものがモナドっぽいけど確信が持てない・・・という場合にも、「モナドっぽいけど本当にそうかどうかはちゃんと確認してないよ」とか書いておくといいでしょう。これは、見ず知らずの誰かのためですし、自衛のためでもあります。日本語で出来る限り分かりやすく解説することを試みたナニカです。コンピュテーション式自体を使ったことがある人を対象に、その変換の仕組みを説明します。カスタムオペレータがかかわる部分を省いてあるため、仕様書よりも単純化されており、より理解しやすくなっていると思います。これを読んでから仕様書を読めば、カスタムオペレータ部分を理解するのもそう難しくはないでしょう。詳説コンピュテーション式コンピュテーション式は、以下の構文で表されます。}builder-exprは、後述するReturnやBindなどのメソッドを持つ型(以降、ビルダー)をもった式です。cexprは、通常のF#の文法に加えて、この中でのみ使える記法(let!など)を加えた文法によって書かれた式です。cexprの中で使える記法は、ビルダーがどんなメソッドを提供しているかによります。提供されていない場合はlet!は使えません。関数呼び出しの結果やメソッド呼出しの結果としてビルダーが戻ってくるようなコンピュテーション式も当然考えられます。}これは、コンピュテーション式の動作を使う側で変更したい場合や、ビルダーが内部に状態を持つなどして使いまわせない場合に有効です。これを使っている例としては、Basis.CoreのResultWithZeroBuilderがあります。通常のResultBuilderでは提供不可能な構文をサポートしています。*)コンピュテーション式の変換コンピュテーション式はまず、以下のように変換されます((bは「フレッシュな変数(名前が他とかぶらない変数)」です))。|}cという表記が出てきますが、これは「その中の式をコア言語の式(つまり通常のF#の式)に変換する」ことを意味します。|}0もありますが、これは「カスタムオペレータが許されるかどうか」が違います。|}0の方はカスタムオペレータが許されません。以降では、話を単純化するためにカスタムオペレータについては省略します。|}という表記を使いますが、言語仕様を読む際は注意してください。builder-exprは1回しか実行されない点に注意してください。cexprの変換中にビルダーにアクセスする際は、bを通してアクセスされます。毎回ビルダーを作るようにすれば、ビルダーの中に状態を持っても大丈夫なのです。Quoteの各メソッドが実装されている場合、コンピュテーション式は先ほどの変換の際に、これらのメソッドの呼び出しを差し込みます。3つとも実装されている場合、以下のように変換されます。Runの順で変換されます。対象のメソッドがなかった場合は、対応する変換は行われません。Delay変換ビルダーにDelayメソッドが存在する場合、cexprの変換した式をラムダ式で囲い、Delayに渡します。}これが、以下のように変換されます。|}に対する変換の結果の式を、delayed-exprとします。|}がdelayed-exprです。Delayは、ここでの変換よりも、他の変換の中で現れる際に重要になってきます。whileの変換までDelayは出てきませんが、Delayの存在はその時まで頭の片隅にでも置いておいてください。builder-exprdelayed-exprこれが、以下のように変換されます。@&gt;ビルダーにQuoteメソッドが存在しない場合、コード引用符で囲わずに、特に変換は行いません。delayed-exprに対する変換の結果の式を、quoted-exprとします。@>、存在しない場合はdelayed-exprがquoted-exprです。このように、Quoteメソッドは呼び出されることはありません。存在すればいいだけのメソッドです。ビルダーに対する属性にしなかったのは、拡張メソッドでQuoteの機能を後付けできるようにするためでしょうか?Run変換ビルダーにRunメソッドが存在する場合、quoted-exprをRunに渡します。builder-exprquoted-exprこれが、以下のように変換されます。builder-exprb.Run(quoted-expr)ビルダーにRunメソッドが存在しない場合、特に変換は行いません。Runは最後に一回だけ呼び出されるメソッドになるので、このメソッドの結果がコンピュテーション式の結果になります。活用方法としては、Readerモナド用のビルダーに対して、最終的に値を取り出すビルダーを作るようなものが考えられます。false)が変換のルールとして記載されています((このTは、translateもしくはtranslationの略でしょう))。q)とあった時、eは変換されるコンピュテーション式Vはこれまでにバインドされた変数群Cは変換済みのコンテキスト情報qはカスタムオペレータを許すかどうかの真偽値を表します。Cの部分に変換されたものが入ると考えましょう。vと、id関数になっています。この中で、Vはカスタムオペレータに関係する変換中で使われるもののため、またqもカスタムオペレータが絡む変換の際に必要になってくるもののため、今回は無視します。v)という単純化したルールを使います。returnの変換とyieldの変換returnの変換は、以下のルールで表されます。eがb.Return(e)に変換されることを意味します。簡単な例を考えてみましょう。}この最後の行を変換してみます*1。b.Return(10)変換できました。C(b.Yield(e))と、returnがyieldに、ReturnがYieldになっただけなので、同じステップで変換できます。return!の変換とyield!の変換return!の変換は、以下のルールで表されます。eがb.ReturnFrom(src(e))に変換されることを意味します。src(e)は、ビルダーがSourceメソッドを持っており、かつ最も内側のForEachがユーザによるもの(変換により生成されたコードではなく、ユーザが書いたコードであるということ)である場合のみ、b.Source(e)に変換されます。2番目の条件は仕様書からのものですが、どうも実際の実装はちょっと違っているようです。コードを軽く眺めただけですが、ForEachとForEachThenJoinOrGroupJoinOrZipClauseのForEach部分とLetOrUseBangの場合に、ユーザコードかどうかの判定をしているようで、そのほかの部分ではビルダーにSourceメソッドがあればそれを呼び出しているようです。eはビルダーにSourceメソッドが存在すればb.ReturnFrom(b.Source(e))に、存在しなければb.ReturnFrom(e)に変換されます。yield!は、ReturnFromではなくYieldFromが使われる以外は同じです。letの変換letの変換は以下のルールで表されます。C)形式が出てくるとおり、変換後もさらに変換が行われます。実際にどう変換されるかを一歩一歩確認していきましょう。letの変換を追うreturnの時に定義したSimpleBuilderを使い、以下のコンピュテーション式を変換してみます。2}letの変換規則はこうでした。v))これを使って変換します。returnの時も言いましたが、変換の途中は有効なF#コードではないことに注意してください。2)これで変換できました。最終段階まで変換するまでは有効なF#のコードではないことに注意してください。ceの変換は、ceの部分のみ変換することになります。何も定義しなくても、letはコンピュテーション式の中でも使えるということですね。let!の変換let!の変換は以下のルールで表されます。v)))letの変換を見たので、この変換は分かりやすいですね。このように、let!を使うためにはビルダーにBindメソッドが定義されている必要があります。Bindメソッドは引数を2つ取り、2番目の引数は関数である必要があることが分かります。eの型とpの型は一致していなくてもいいことも分かります。第二引数の関数に渡すようにBindメソッドを実装することがほとんどでしょう。通常はBindメソッドはモナドにおける>>=の定義と同じようなものになるでしょう。useの変換とuse!の変換useの変換は以下のルールで表されます。|}))ここまで理解できた人にとっては簡単ですね。use!も見てみましょう。|})))ちょっと複雑ですね。順番に見て行けば難しくはありません。SimpleBuilderにUsing(とBind)を追加してみましょう。&quot;end&quot;これを変換します。2}順番に変換していきます。何も定義せずともuseが使え、Usingによってuse!が使えると思ってしまうかもしれません。変換規則を見たとおり、useであってもUsingがビルダーに定義されていなければ使えません。eのeがそのまま、use!の場合はBindの第二引数で渡された関数の引数が入ってくることから、UsingはそのままBindに相当するような機能*2は持たせられず、単にletやlet!をラップする存在でしかないことが分かります。useはletに対してUsing機能を付けたもの、use!はlet!に対してUsing機能*3を付けたもの、と考えてください。通常、Usingの実装は以下のようになるでしょう。x.Dispose()doの変換doの変換は以下のルールで表されます。v))特に問題はないでしょう。eが実行された後、変換されたceが実行されるだけです。do!の変換do!の変換はルールが2つあり、後ろに式が続く場合と、do!より後ろにこれ以上変換する式がなかった場合で変換のされ方が若干異なります。()が書かれているような動作になります。do!の変換規則は、let!に変換してからlet!の変換規則を適用するような形になっています。それ以外は、特に難しいところはないでしょう。do!の変換にはlet!が必要となるため、do!を使うためにはBindメソッドがビルダーに必要になります。場合によってはBindメソッドのほかに、Returnメソッドも必要となります。if-then-elseの変換2分岐の条件分岐の変換は以下のルールで表されます。|})特に難しくありませんね。ビルダーに何も定義しなくても、elseを伴うifを使えることが分かります。if-thenの変換elseを省略したifの変換は以下のルールで表されます。b.Zero())elseを持つifとは違い、ビルダーにZeroメソッドが必要なことが分かります。elseを省略しないifと表記を合わせると、変換規則は以下のようになります。b.Zero())matchの変換matchの変換は以下のルールで表されます。|})piはi番目のパターンを表し、ceiはi番目の式を表します。パターンが複数あった場合は、それぞれが変換されます。何も定義しなくてもmatchは使えることが分かります。whileの変換whileの変換は以下のルールで表されます*4。v))))Whileのほかに、Delayメソッドも必要であることが分かります。手で変換してみましょう。ビルダーには、SimpleBuilderの拡張を使います。()変換する対象はこれです。()}変換してみましょう。b.Return())))変換できました。コンパイル時に行われる変換はここまでですが、これだと結局どうなるのか分かりにくいので、Delayメソッドを展開してみましょう。expr)をexprにするだけですね。b.Return())whileを使うためには、このようにWhileメソッドとDelayメソッドが必要です。これで本当に大丈夫でしょうか・・・?実際に、fを呼び出したときにどう動くのか見てみます。falseを渡してみましょう。arg2なにかおかしいのが分かりますか?変換する前のコードを再掲します。()}変換する前のコードは、printfnによる出力はwhileの中にありました。そのため、直感的にはxがfalseであれば、何も出力されることなくループが終了すると考えてしまいます。変換後のコードではwhileの外側に出力が移動してしまいました。これでは、xにtrueを渡そうがfalseを渡そうが元のwhileの処理が呼び出されてしまいます((さらには、trueを渡したにもかかわらず、whileの中の処理は一回しか実行されません))。これは使い物になりませんね。これではまずい場合は、WhileメソッドとDelayメソッドの定義を変更し、Runメソッドを実装することで解決します。SimpleBuilderを再実装SimpleBuilderを拡張してきましたが、もう最初らへんのメソッド忘れ気味ですよね。ここで今までのSimpleBuilderを捨て、新たなSimpleBuilderを定義します。()}fの本体を変換した結果がこちらになります(実際の変換は各自の課題とします)。...)に展開できます。b.Return()))展開できました。Runが必要なのは、Delayを実装すると一番外側も関数にくるまれてしまうため、それを実行しないと全体として関数が戻ってしまうからですね。元の展開結果と比べてみます。b.Return()))第二引数部分が関数で包まれたままになりました。この関数fにfalseを渡すと、最終的には以下のようなコードが実行されることになります。()これで、思い通りの結果になりますね。このように、whileの展開にはWhileメソッドとDelayメソッドのみが必要ですが、現実的にはRunメソッドも実装する必要がある場合が多いです。ce2の変換は以下のルールで表されます。|})))CombineメソッドとDelayメソッドが必要ですね。ce2の変換は、whileなどを実用的なものにするために必要になるため、非常に重要です。実はこれだけではまだ足りません。今のWhileの実装では、while以降に別のceを置けず、かつwhileの本体はunitを返す必要があります。*)どうすればいいか。()isExitとCombineの追加、ReturnとWhileの変更を行いました。simpleを関数にしています。詳しい説明は省きますが、これによって以下のようなコードが書けるようになりました。0}これで、trueを渡すと10が返り、falseを渡すと0が返ります。展開や、なぜsimpleを関数にしたのかを考えるのは読者の課題とします。ちなみに、simpleを関数にしなくても済む方法があります。それを実装した例がBasis.CoreのOptionBuilderなどで見れます。*5このエントリは、その時の苦労の反動が書くきっかけとなっています。あるコンピュテーション式の実装では、上のようなwhileの中でreturnするコードを上手く扱えないと思います。「このコードを扱えるコンピュテーション式を実装しているコードが他にもあるよ!」というものがあったらコメント欄で教えてくれると嬉しいです。tryの変換tryの変換は以下のルールで表されます。e)))ここまで来たら、もはや何も難しいことはありませんね。withの変換はちょっと怪しいです。正しくは多分、こうですね。|}))forの変換forの変換は以下のルールで表されます。C)forも難しいところはありません。下の形式は、上の形式のforに書き換えたうえで変換するようになっていますね。eの変換eの変換は以下のルールで表されます。b.Zero())簡単ですね。}などは、Zeroの呼び出しを付けて変換されます。コンピュテーション式の実装でよく使うであろう変換規則の説明は以上です。ここまで理解できれば、カスタムオペレータが絡む変換規則も理解できるでしょう。変換規則一覧このエントリで紹介した変換規則の一覧を載せておきます。b.Zero())Delay変換、Quote変換、Run変換は上記規則で表現できないので省略します。Blog|ブログを報告する。