ステップアップでわかるコンピュテーション式。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メソッド以下については、ちょっと工夫しないと厳しそうな気がします。なので宿題とします(キリッ
F#3.0で加速する言語指向プログラミング(LOP)。コンピューテーション式はもはやモナドだけのための構文ではない!!!
マーチン・ファウラー先生の黒いDSL本(翻訳版)が5月2日に発売されました。遅ればせながら私も最近購入して熟読しているところです。
この本が示すDSLの種類や内容は、あくまでもオブジェクト指向というコンテキストにおいてのものであり、関数型言語によるDSL開発については一切言及はありませんが、まえがきの「本書に欠けていること」の中で「"関数型言語でのDSL"に関する言及はないので、ご了承ください。」というようなお断りがあり、好感が持てます。マーチン・ファウラー先生に限らず、オブジェクト指向の大御所たちも最近の関数型言語流行の流れにはとても敏感になっているようです。実際、ことDSLに関して言うなら、モナディックなパーサ・コンビネータの存在など、確かに関数型言語の方が有利になる点もいくつかあるし、それらについて書籍内に言及がないことを説明するのは良いことだと思う。この本で示されている考え方やパターンについて関数型言語ではどのように考え適用していけばよいのか。自分の中で消化していきたい。そんなこんなで、黒いDSL本が結構人気みたいです。日本のデベロッパーのDSLに対する関心の高まりを感じたり、ドラゴンズドグマが楽しみだったりな今日この頃ですが、いかがお過ごしでしょうか。
アレについて一言だけ言っておこうと思ったけど、黒い本読み終わった後の方がよさげかな。
まだ読み終わっていないんですが書いちゃいます。
この記事では、「F#3.0ではコンピューテーション式が拡張されたので、内部DSLが作りやすくなりましたよ。」という話題を提供します。
言語指向プログラミング(LOP)とは
言語指向プログラミング(LOP)とは、メタプログラミングと同様にひとことで言い表すことは容易ではない抽象的な概念ですが、大きな意味で「ドメイン特化言語(DSL:Domain Specific Language)を使ってソフトウェア構築を行う一般的な開発スタイル」というように具体的に捉えることもできます。言語指向プログラミングを理解するには、まずDSLとは何かと言うことを理解する必要があります。
ある特定のドメイン(目的)の問題解決のために特化させた専用のプログラミング言語のことをDSLと言います。専用言語というとなんだか難しいように聞こえるかもしれませんが、実のところそんなたいした話ではなく、多くの場合はXML等の設定ファイルやライブラリ、あるいはフレームワークの延長上に自然と現れてくるものです*1。あらかじめDSLで開発すること考えて設計をできるのが理想的ですが、少し凝った設定ファイルが拡張を繰り返すたびにいつの間にかDSLのようなものになっていたというケースは現場ではそう珍しいことではないかもしれません。
ごく身近にあるDSLの例として、Excelのセル内の値は「A2」や「D5」などのように、Excel固有の表し方でシンプルに表す機能があります。これは、汎用プログラミング言語のようにデータの型などを記述することなく、「=A2+D5」などのように単純な式において値を計算をすることができます。これは特定の問題に対する専用言語として捉えることができ、つまり一種のDSLであると言えます。この例からもわかるように、DSLの主な利点は特定の問題に対して表現がとてもシンプルになるということです。このように特定の問題に対応するためにホスト言語とは別の言語を定義して、それを用いて特定ドメインの問題を解決しようとする考え方や手法。それを言語指向プログラミングと言います。
DSLには、大きく分けて外部DSLと内部DSLの2種類があり、ホスト言語*2とは異なる言語で作成するものを外部DSL(例えばXMLファイルなどを使用する手法。弾幕記述言語BulletMLなど。)といい、ホスト言語のサブセットで書かれるタイプのものを内部DSLあるいは組み込み型DSL(.NETのLINQなど)と呼びます。言語指向プログラミングで伝統的なものとしては、Unixリトル言語、Lisp、アクティブデータモデル、XML設定ファイルなどがあり、現在も様々な場面で広く活用されています。
言語指向プログラミングおよびDSL開発についてより詳しい情報が知りたい場合は、マーチン・ファウラー先生著の黒いDSL本こと「ドメイン特化言語 パターンで学ぶDSLのベストプラクティス46項目」を読むか、あるいは「LanguageWorkbench - Martin Fowler's Bliki in Japanese」あたりを参照されたい。
F#3.0で加速する言語指向プログラミング(LOP)。コンピューテーション式はもはやモナドだけのための構文ではない!!!
F#(F#2.0)は、強い静的型付き言語としては比較的言語指向プログラミングのやりやすい言語です。パターンマッチやアクティブパターンを利用して抽象的にDSLを定義する手法、XML設定ファイルを読み込んで外部DSLを作成する古典的な手法、コンピュテーション式を用いて計算式として内部DSLを作成する手法。モナディックなパーサ・コンビネータライブラリFParsecを利用して構文解析を行う手法、あるいはfslex/fsyaccを利用したコンパイラの作成など、その方法はさまざまです。
F#3.0ってゆーとTypeProviderが注目されがちだけど、俺的にはあっちの方がよだれ生唾ごっくんものだよ
F#3.0で追加される2つの新機能によって、言語指向プログラミング(LOP)の手法の幅がさらに広がります。その1つは、ご存じTypeProvider。TypeProviderはコード生成と動的型付けの両方に替わるものとして発表当時から注目を集めています。この機能が追加された直接の目的とは異なりますが、外部DSLを作成する手法のひとつとしてTypeProviderが新たに加わりました。
もう1つは、Query expressions(クエリ式)です。クエリ式およびそのクエリメカニズムそのものがDSL作成について直接影響を与えるものではありませんが、新たにクエリ式の機能を提供するにあたって合わせて追加された仕様である「コンピューテーション式に、独自のキーワードを定義することができるカスタムクエリ拡張機能」が大きな影響を与えます。F#2.0ではコンピューテーション式において、 let!、do! 、return、return!などの特定のキーワードのみが利用可能でした。コンピューテーション式は、モナドを書くために限定された機能というわけではありませんでしたが、BindやReturnなどモナドの文脈として利用されるキーワードの色が強く、事実上モナドのための構文として利用されてきました。なぜなら、F#2.0ではコンピューテーション式で利用できるキーワードを拡張する方法が提供されていなかったからです。しかし、F#3.0のコンピューテーション式ではこれが拡張可能であり、CustomOperationAttributeクラスを用いることで独自のキーワードを定義することができ、ある程度柔軟なカスタマイズができます。これは大変エキサイティングなことです!!!
実際どういうことができるのかというと、以下のようなことが可能となります。
type SeqBuilder() = member __.For (source, body) = seq { for v in source do yield! body v } member __.Yield (item)= seq { yield item } [<CustomOperation("select")>] member __.Select (source, f)= Seq.map f source let myseq = SeqBuilder() myseq { for i in 1 .. 10 do select (fun i -> i + 100) } |> Seq.iter (printfn "%d")
この仕組みの詳細については、まだ大々的に公表されているものではありませんが、MSDN - Core.CustomOperationAttribute クラス (F#)にて、ある程度利用方法を把握することができます。F#3.0で言語指向プログラミングが加速するとはつまりこういうことです。コンピューテーション式はもはやモナドだけのための構文ではないのです!!!
ProjectionParameterAttributeの利用
カスタムオペレーションの引数をProjectionParameter属性でマークすると、自動的にパラメーター化(というかカスタムキーワードに続く式を暗黙的に関数に変換)してくれる。type SeqBuilder() = member __.For (source, body) = seq { for v in source do yield! body v } member __.Yield (item)= seq { yield item } [<CustomOperation("select")>] member __.Select (source, [<ProjectionParameter>] f) = Seq.map f source let myseq = SeqBuilder() myseq { for i in 1 .. 10 do select (i + 100) } |> Seq.iter (printfn "%d")
MaintainsVariableSpaceプロパティの利用
CustomOperation属性のMaintainsVariableSpaceプロパティをtrueに設定すると、以下のようにシーケンス内の値を変更せずに維持するカスタムキーワードに設定できる。type SeqBuilder() = member __.For (source, body) = seq { for v in source do yield! body v } member __.Yield (item)= seq { yield item } [<CustomOperation("select")>] member __.Select (source, [<ProjectionParameter>] f) = Seq.map f source [<CustomOperation("reverse", MaintainsVariableSpace = true)>] member __.Reverse (source) = List.ofSeq source |> List.rev let myseq = SeqBuilder() myseq { let x = 1 for i in 1 .. 10 do reverse select (x, i + 100) } |> Seq.iter (printfn "%A")
この他にも、 into の使用をサポートするAllowIntoPatternプロパティや、2つの入力をサポートするIsLikeZipプロパティなど、柔軟な拡張なためのオプションがいくつか用意されている。
サンプル:FizzBuzzBuilder
fizzbuzz { for i in 1..100 do execute 3 5} |> Seq.iter (printfn "%A")
上記のようにFizzBuzzを書けるようにする内部DSLを書いてみましょう。
type FizzBuzzBuilder() = member __.For (source, body) = seq { for v in source do yield! body v } member __.Yield (x) = seq { yield x } [<CustomOperation("select")>] member __.Select (source, [<ProjectionParameter>] f) = Seq.map f source [<CustomOperation("execute")>] member __.Execute (source, a, b) = if a = 0 then invalidArg "fizz" "ゼロだめ" if b = 0 then invalidArg "buzz" "ゼロだめ" let fzbz x = (x%a,x%b) |> function |0,0 -> "FizzBuzz" |0,_ -> "Fizz" |_,0 -> "Buzz" | _ -> string x source |> Seq.map fzbz let fizzbuzz = FizzBuzzBuilder()
サンプル:もっとFizzBuzzBuilder
fizzbuzz { fizz 3 buzz 5 execute [1..100]} |> Seq.iter (printfn "%A")
もうちょっとDSLっぽさを醸し出したいと思います。上記のように記述できるDSLを書いてみましょう。
type FizzBuzzBuilder() = [<CustomOperation("fizz")>] member __.Fizz (_, x) = x,0 [<CustomOperation("buzz")>] member __.Buzz ((x,_), y) = x,y [<CustomOperation("execute")>] member __.Execute ((a,b),source) = if a = 0 then invalidArg "fizz" "ゼロだめ" if b = 0 then invalidArg "buzz" "ゼロだめ" let fzbz x = (x%a,x%b) |> function |0,0 -> "FizzBuzz" |0,_ -> "Fizz" |_,0 -> "Buzz" | _ -> string x source |> Seq.map fzbz member __.Yield (x) = x let fizzbuzz = FizzBuzzBuilder()
凝ったことは何もしていませんが、これまでのF#2.0ではできない表現です。面白いですね。F#3.0のコンピューテーション式は複雑なDSLを作るには向いているとは言えませんが、あまり複雑ではないちょっとしたDSLが必要になった場合は、検討してみる価値が十分にある手法です。
ちなみに、ビルディング関数および、カスタムキーワードは以下ハードコピーのようにVisual Studio 11 Beta上でもちろんシンタックスハイライトされます。
独自に定義したキーワードもちゃんとハイライトされるなんて。すてき!!!
サンプル:ILBuilder
次はもう少し実用的なサンプル。
MSILerな人は、上記のような感じで記述できるDSLが欲しくなるかもしれません。(というか、そういう人たちはおそらくもう既にお手製のものを作っていることでしょうが。)
open System.Reflection.Emit type Stack<'a> = Stack of (ILGenerator -> unit) type Completed<'a> = Completed of (ILGenerator -> unit) type ILBuilder() = [<CustomOperation("ldc_i4_7")>] member __.ldc4_7(x) = Stack(fun ilg -> ilg.Emit(OpCodes.Ldc_I4_7)) [<CustomOperation("ldc_i4_8")>] member __.ldc4_8(Stack f : Stack<int * (int * 'r)>) = Stack(fun ilg -> f ilg; ilg.Emit(OpCodes.Ldc_I4_8)) [<CustomOperation("ldc_i4_0")>] member __.ldc4_0(Stack f : Stack<int * (int * 'r)>) = Stack(fun ilg -> f ilg; ilg.Emit(OpCodes.Ldc_I4_0)) [<CustomOperation("add")>] member __.Add(Stack f : Stack<int * (int * 'r)>) : Stack<int * 'r> = Stack(fun ilg -> f ilg; ilg.Emit(OpCodes.Add)) [<CustomOperation("mul")>] member __.Mul(Stack f : Stack<int * (int * 'r)>) : Stack<int * 'r> = Stack(fun ilg -> f ilg; ilg.Emit(OpCodes.Mul)) [<CustomOperation("ret")>] member __.Ret(Stack f : Stack<int * (int * 'r)>) = Completed(fun ilg -> f ilg; ilg.Emit(OpCodes.Ret)) member __.Yield x = x member __.Run(Completed f : Completed<'a>) : unit -> 'a = let dm = DynamicMethod("", typeof<'a>, [||]) let g = dm.GetILGenerator() g |> f (dm.CreateDelegate(typeof<System.Func<'a>>) :?> System.Func<'a>).Invoke let il = ILBuilder()
かなり適当で且つ大半を割愛しましたが、こんな感じでMSILのDSLとかも作れてしまいます。頑張って真面目に実装したら、MSIL厨歓喜間違いなしです。
RxQueryBuiler
おわりに、非常にクールな内部DSLをご紹介します。
あのReactive ExtensionsをF#でいい感じに記述することができるようになる、RxQueryBuilerです。
When the Reactive Framework meets F# 3.0 - have fun
http://mnajder.blogspot.jp/2011/09/when-reactive-framework-meets-f-30.html
上記記事に掲載されているコードは、若干バージョンが古いもの向けに書かれており、VS11 Betaおよび最新Rx_Experimental-Main(ForkJoinはExperimental版に入ってるので)に対応していないので、少しだけ修正したものを以下に掲載します。具体的な変更箇所は、IObservable<_>.Single()や、IObservable<_>.First()等が、C#およびVBの async/await サポートにより変更となり、代わりに、IObservable<_>.SingleAsync()、IObservable<_>.SingleAsync()を使用するようにしただけです。
open System open System.Reactive.Linq open System.Reactive.Concurrency type RxQueryBuiler() = member this.For (s:IObservable<_>, body : _ -> IObservable<_>) = s.SelectMany(body) [<CustomOperation("select", AllowIntoPattern=true)>] member this.Select (s:IObservable<_>, [<ProjectionParameter>] selector : _ -> _) = s.Select(selector) [<CustomOperation("where", MaintainsVariableSpace=true, AllowIntoPattern=true)>] member this.Where (s:IObservable<_>, [<ProjectionParameter>] predicate : _ -> bool ) = s.Where(predicate) [<CustomOperation("takeWhile", MaintainsVariableSpace=true, AllowIntoPattern=true)>] member this.TakeWhile (s:IObservable<_>, [<ProjectionParameter>] predicate : _ -> bool ) = s.TakeWhile(predicate) [<CustomOperation("take", MaintainsVariableSpace=true, AllowIntoPattern=true)>] member this.Take (s:IObservable<_>, count) = s.Take(count) [<CustomOperation("skipWhile", MaintainsVariableSpace=true, AllowIntoPattern=true)>] member this.SkipWhile (s:IObservable<_>, [<ProjectionParameter>] predicate : _ -> bool ) = s.SkipWhile(predicate) [<CustomOperation("skip", MaintainsVariableSpace=true, AllowIntoPattern=true)>] member this.Skip (s:IObservable<_>, count) = s.Skip(count) member this.Zero () = Observable.Empty(Scheduler.CurrentThread) member this.Yield (value) = Observable.Return(value) [<CustomOperation("count")>] member this.Count (s:IObservable<_>) = Observable.Count(s) [<CustomOperation("all")>] member this.All (s:IObservable<_>, [<ProjectionParameter>] predicate : _ -> bool ) = s.All(new Func<_,bool>(predicate)) [<CustomOperation("contains")>] member this.Contains (s:IObservable<_>, key) = s.Contains(key) [<CustomOperation("distinct", MaintainsVariableSpace=true, AllowIntoPattern=true)>] member this.Distinct (s:IObservable<_>) = s.Distinct() [<CustomOperation("exactlyOne")>] member this.ExactlyOne (s:IObservable<_>) = s.SingleAsync() [<CustomOperation("exactlyOneOrDefault")>] member this.ExactlyOneOrDefault (s:IObservable<_>) = s.SingleOrDefaultAsync() [<CustomOperation("find")>] member this.Find (s:IObservable<_>, [<ProjectionParameter>] predicate : _ -> bool) = s.FirstAsync(new Func<_,bool>(predicate)) [<CustomOperation("head")>] member this.Head (s:IObservable<_>) = s.FirstAsync() [<CustomOperation("headOrDefault")>] member this.HeadOrDefault (s:IObservable<_>) = s.FirstOrDefaultAsync() [<CustomOperation("last")>] member this.Last (s:IObservable<_>) = s.LastAsync() [<CustomOperation("lastOrDefault")>] member this.LastOrDefault (s:IObservable<_>) = s.LastOrDefaultAsync() [<CustomOperation("maxBy")>] member this.MaxBy (s:IObservable<'a>, [<ProjectionParameter>] valueSelector : 'a -> 'b) = s.MaxBy(new Func<'a,'b>(valueSelector)) [<CustomOperation("minBy")>] member this.MinBy (s:IObservable<'a>, [<ProjectionParameter>] valueSelector : 'a -> 'b) = s.MinBy(new Func<'a,'b>(valueSelector)) [<CustomOperation("nth")>] member this.Nth (s:IObservable<'a>, index ) = s.ElementAt(index) [<CustomOperation("sumBy")>] member inline this.SumBy (s:IObservable<_>,[<ProjectionParameter>] valueSelector : _ -> _) = s.Select(valueSelector).Aggregate(Unchecked.defaultof<_>, new Func<_,_,_>( fun a b -> a + b)) [<CustomOperation("groupBy", AllowIntoPattern=true)>] member this.GroupBy (s:IObservable<_>,[<ProjectionParameter>] keySelector : _ -> _) = s.GroupBy(new Func<_,_>(keySelector)) [<CustomOperation("groupValBy", AllowIntoPattern=true)>] member this.GroupValBy (s:IObservable<_>,[<ProjectionParameter>] resultSelector : _ -> _,[<ProjectionParameter>] keySelector : _ -> _) = s.GroupBy(new Func<_,_>(keySelector),new Func<_,_>(resultSelector)) [<CustomOperation("join", IsLikeJoin=true)>] member this.Join (s1:IObservable<_>,s2:IObservable<_>, [<ProjectionParameter>] s1KeySelector : _ -> _,[<ProjectionParameter>] s2KeySelector : _ -> _,[<ProjectionParameter>] resultSelector : _ -> _) = s1.Join(s2,new Func<_,_>(s1KeySelector),new Func<_,_>(s2KeySelector),new Func<_,_,_>(resultSelector)) [<CustomOperation("groupJoin", AllowIntoPattern=true)>] member this.GroupJoin (s1:IObservable<_>,s2:IObservable<_>, [<ProjectionParameter>] s1KeySelector : _ -> _,[<ProjectionParameter>] s2KeySelector : _ -> _,[<ProjectionParameter>] resultSelector : _ -> _) = s1.GroupJoin(s2,new Func<_,_>(s1KeySelector),new Func<_,_>(s2KeySelector),new Func<_,_,_>(resultSelector)) [<CustomOperation("zip", IsLikeZip=true)>] member this.Zip (s1:IObservable<_>,s2:IObservable<_>,[<ProjectionParameter>] resultSelector : _ -> _) = s1.Zip(s2,new Func<_,_,_>(resultSelector)) [<CustomOperation("forkJoin", IsLikeZip=true)>] member this.ForkJoin (s1:IObservable<_>,s2:IObservable<_>,[<ProjectionParameter>] resultSelector : _ -> _) = s1.ForkJoin(s2,new Func<_,_,_>(resultSelector)) [<CustomOperation("iter")>] member this.Iter(s:IObservable<_>, [<ProjectionParameter>] selector : _ -> _) = s.Do(selector) let rxquery = new RxQueryBuiler()
以前、「F#でRxる。よく訓練されたF#ERはコンピューテーション式をつくる。」という記事を書きましたが、F#3.0のカスタムクエリ演算子の登場によって、完全に過去のモノにしてくれました!もういろいろ自由自在ですね。F#でRxなリアクティブプログラマーもこれで勝つる!!!
最後に
大事なことなのでもう一度言っておきますが、F#3.0のコンピューテーション式はもはやモナドだけのための構文ではないのです!!!F#3.0のコンピューテーション式でイケてる内部DSLを作って、どんどん自慢しちゃいましょう。
F#で簡素なモゲマスコンプガチャシミュレータ
椎名林檎「自由へ道連れ」をヘビロテしすぎて脳内無限ループしている今日この頃ですが、皆様いかがお過ごしでしょうか。
時事ネタとしては旬を逃した感じですが、簡素なコンプガチャシミュレータをF#で書いてみました。
とは言っても、この記事で伝えたいことはコンプガチャの確率がどうのですとか、実社会におけるコンプガチャの問題点がどうのとかいうような話題を扱うものではなく、安直にモナド則を満たさないコンピューテーション式を作ってしまうよりかは、Identityモナド(恒等モナド)を使ってプログラミングをした方が、見通しが良くモジュール性の高いコードを書くことができるかもしれないよ、という話題を提供します。割とどうでもいい話題ですね。未だガラケーユーザーであり、スマホやソーシャルゲーとはほとんど縁のない私ですが(もちろんモゲマスもやったことない)、気が向いたのでちょっと書いてみました。なお、モゲマスおよびFSharpxのステマではありません。
シミュレートするコンプガチャの仕様
まずはシミュレートするコンプガチャの仕様について簡単に確認しておきましょう。今回実装してみるのは、モゲマスことモバゲー『アイドルマスター シンデレラガールズ』のコンプガチャ「パジャマパーティー」に近いコンプガチャのシミュレートを目的としてみます。
Google先生に聞いてきた モゲマス 「パジャマパーティー」コンプガチャの概要
時折行われる課金ガチャのイベント企画。1ガチャあたり300円を支払って利用するガチャです。1回に1枚のアイドルカードが得られます。実際の支払いは「モバコイン」ですが、モバコインは100G=100円で購入して利用するため、便宜上こちらではそのまま円とします。イベント期間中以下の「レア」パジャマアイドルが確率テーブルに5枚追加され、その5枚を全て集めると、特典として限定「Sレア」カード「[眠れる姫君]星井美希」を獲得できるというもの。
コンプ対象パジャマアイドル
・[パジャマパーティー]緒方智絵里 コスト12 攻2880 守1600 キュート攻中アップ
・[パジャマパーティー]間中美里 コスト08 攻1240 守1440
・[パジャマパーティー]黒川千秋 コスト10 攻1860 守1560 クール攻中アップ
・[パジャマパーティー]川島瑞樹 コスト09 攻1400 守1680
・[パジャマパーティー]若林智香 コスト12 攻1600 守2680 パッション守中アップ
なお、バンナムによる公式の発表はないが、いずれかのパジャマアイドルが出現する確率は12%程度とのこと。
※ガチャのセット販売も行われているが、ここでは1回ずつガチャを行うこととする。
※なお、ネットで適当に拾ってきた情報のため正確ではない可能性あり。
内部の実装の詳細はわかりませんが、「今すぐモゲマスPすべてにSレアを授けてみせろッ! ver. 0.141.33」というシミュレータが既にあるようです。
http://mugenmasakazu.plala.jp/cgi-bin/nijiuraIDOLMASTER/mogemaskillyourself.cgi
なお、コンプガチャがどうして危険と言われているのかの理由については、「コンプガチャの確率マジックを中学生にも分かるように説明するよ - てっく煮ブログ」の解説がわかりやすい。わたしが中学生にも分かるように説明するなら、例えばモンハンの素材であるところの「逆鱗(出現率2%)」が5種類あったとして、それらすべてを揃えないと作れない武器があったとき、それにかかる時間を想像してみると、コンプガチャへ挑む無謀さが割と想像しやすい、とか。欲しいと思う素材ほど出ないようになっているといわれる架空のシステム。いわゆる「センサー」の存在への疑い、とか。
簡素なコンプガチャシミュレータを愚直に書いてみる
まずは愚直に。細かいことは考えずにとりあえず実装してみたバージョン。
open System let tee x f = f x; x let (|>!) x f= tee x f let rand = new Random(DateTime.Now.Millisecond); type Rarity = |R of string |Other // ガチャアイテム let a,b,c,d,e,other = R("緒方智絵里"), R("間中美里"), R("黒川千秋"), R("川島瑞樹"), R("若林智香"), Other // コンプ let comp = [a;b;c;d;e] // コンプ対象が出る確率 let probability = 0.12 let shuffle source = let array = List.toArray source let rec loop i = i |> function | 1 -> () | _ -> let i = i - 1 let j = rand.Next(i) let temp = array.[i] array.[i] <- array.[j] array.[j] <- temp; loop i loop source.Length [for x in array do yield x] let completeGacha lst count total = let items = let dummy p = let e = ((float comp.Length) / p) |> int [for i in 1..(e-comp.Length) do yield Other] let target = comp@dummy probability target |> shuffle let gacha () = rand.Next(1, items.Length) |> fun i -> items.[i] let rec gacha' count total = let newitem = gacha () let current = count + 1 if List.exists (fun x -> x = newitem) comp |> not then (* でねぇ!!!*) gacha' current total elif List.forall (fun x -> x = newitem |> not) lst |> not then (* ダブりかよ...orz *) gacha' current total else (* よっしゃー!なんという引きの良さ!!! *) lst@[newitem], current, (total + current), List.length (lst@[newitem]) = comp.Length gacha' count total let printGacha x = x |>! (fun (possession, n, total, complete) -> let g = sprintf "%d回:%d円 " n (300 * n) let sum = sprintf "合計%d円" (300 * total) let result = sprintf "%s" (if complete then "コンプ" else "未コンプ") printfn "%s %s %A %s" g sum possession result) let cut (a,b,c,d) = a,b,c completeGacha [] 0 0 |> printGacha |> cut |||> completeGacha |> printGacha |> cut |||> completeGacha |> printGacha |> cut |||> completeGacha |> printGacha |> cut |||> completeGacha |> printGacha |> cut |> fun _ -> printfn "[眠れる姫君]星井美希を手に入れた!" Console.ReadLine () |> ignore
ジェネリックもへったくれもない。いくら適当とはいえ愚直すぎて泣ける。
結果は当然実行ごとに毎回変わりますが、一応実行結果の例。
1回:300円 合計300円 [R "黒川千秋"] 未コンプ 8回:2400円 合計2700円 [R "黒川千秋"; R "緒方智絵里"] 未コンプ 38回:11400円 合計14100円 [R "黒川千秋"; R "緒方智絵里"; R "川島瑞樹"] 未コンプ 78回:23400円 合計37500円 [R "黒川千秋"; R "緒方智絵里"; R "川島瑞樹"; R "間中美里"] 未コンプ 108回:32400円 合計69900円 [R "黒川千秋"; R "緒方智絵里"; R "川島瑞樹"; R "間中美里"; R "若林智香"] コンプ [眠れる姫君]星井美希を手に入れた!
1回目でレアカード"黒川千秋"を引き当てるという強運を発揮するも、2枚目のレア"緒方智絵里"を引き当てるには8回かかる。
そして、あらあらまあまあ最終的には合計69900円のぶっこみ。バンナムにかなり貢ぎましたな。
簡素なコンプガチャシミュレータをコンピューテーション式で
愚直にもほどがあるので、もうちょっとなんとかしてみましょう。
completeGacha [] 0 0 |> printGacha |> cut |||> completeGacha |> printGacha |> cut |||> completeGacha |> printGacha |> cut |||> completeGacha |> printGacha |> cut |||> completeGacha |> printGacha |> cut |> fun _ -> printfn "[眠れる姫君]星井美希を手に入れた!"
上記部分に着目すると、なんだか順々に関数を適用していく流れが見えます。なんだかコンピューテーション式にできそうです。
ということで、とりあえずコンピューテーション式にしてみる。
namespace Library1 [<AutoOpen>] module CompleteGacha = open System let tee x f = f x; x let inline (|>!) x f= tee x f let rand = new Random(DateTime.Now.Millisecond); let shuffle source = let array = List.toArray source let rec loop i = i |> function | 1 -> () | _ -> let i = i - 1 let j = rand.Next(i) let temp = array.[i] array.[i] <- array.[j] array.[j] <- temp; loop i loop (List.length source) [for x in array do yield x] let completeGacha comp d probability (lst:'a list) count total = let items = let dummy p = let e = ((float <| List.length comp) / p) |> int [for i in 1..(e - (List.length comp)) do yield d] let target = comp@(dummy probability) target |> shuffle let gacha () = let i = rand.Next(1, (List.length items)) items.[i] let rec gacha' count total = let newitem = gacha () let current = count + 1 if List.exists (fun x -> x = newitem) comp |> not then (* でねぇ!!! *) gacha' current total elif List.forall (fun x -> x = newitem |> not) lst |> not then (* ダブりかよ...orz *) gacha' current total else (* よっしゃー!なんという引きの良さ!!! *) lst@[newitem], current, (total + current), List.length (lst@[newitem]) = List.length comp gacha' count total type CompGacha<'a> = CompGacha of 'a type CompGachaBuilder () = member this.Bind(m, f) : CompGacha<_> = let (CompGacha (comp, d, p, lst,count,total,complete)) = m let lst,count,total,complete = completeGacha comp d p lst count total f (comp,d, p, lst,count,total,complete) member this.Return x = CompGacha(x) member this.ReturnFrom x = x let cg = new CompGachaBuilder() let printGacha price unit f x = x |>! (fun (comp, d, p, possession, n, total, complete) -> let g = sprintf "%d回:%d%s" n (price * n) unit let sum = sprintf "合計%d%s" (price * total) unit let result = sprintf "%s" (if complete then "コンプ" else "未コンプ") printfn "%s %s %A %s" g sum possession result if List.length comp = List.length possession then f()) open FSharpx open Operators let inline returnM x = returnM cg x let inline (>>=) m f = bindM cg m f let inline (=<<) f m = bindM cg m f let inline ap m f = f <*> m let inline map f m = liftM cg f m let inline (<!>) f m = map f m let inline lift2 f a b = returnM f <*> a <*> b let inline (>>.) m f = bindM cg m (fun _ -> f) let inline (>=>) f g = fun x -> f x >>= g let inline (<=<) x = flip (>=>) x
利用側
open System open Library1 type Rarity = |R of string |Other // ガチャアイテム let a,b,c,d,e,other = R("緒方智絵里"), R("間中美里"), R("黒川千秋"), R("川島瑞樹"), R("若林智香"), Other // コンプ let comp = [a;b;c;d;e] // コンプ対象アイテムが出る確率 let probability = 0.12 // 12% // 1ガチャあたり300円 let printg = printGacha 300 "円" (fun () -> printfn "[眠れる姫君]星井美希を手に入れた!") let mogemasu x = cg { return x } >>= fun x -> cg { return x |> printg } >>= fun x -> cg { return x |> printg } >>= fun x -> cg { return x |> printg } >>= fun x -> cg { return x |> printg } >>= fun x -> cg { return x |> printg } // 別の書き方 //let mogemasu x = // cg { let! x = cg { return x } // let! x = cg { return x |> printg } // let! x = cg { return x |> printg } // let! x = cg { return x |> printg } // let! x = cg { return x |> printg } // return x |> printg } (comp, other, probability, [], 0, 0, false) |> mogemasu |> ignore Console.ReadLine () |> ignore
とりあえずコンピューテーション式にしました以外の何物でもない。愚直版に比べるとそこそこ抽象化こそされているが、まだ不十分。コンプ対象カード中何枚揃えるまでガチャを行うかの部分がハードコーディングされている(この場合は5回のBindをすることで5枚揃えるまでガチャしている)。ちなみに、このコンピューテーション式は「Functor且つApplicative且つモナド」を満たさない。「コンピューテーション式がモナドである必要は必ずしもない」が、このような実装ではモジュール性の低下は否めない。
簡素なコンプガチャシミュレータをIdentityモナドで
HaskellでIdentityモナド(恒等モナド)と言えば、一般的にはモナド変換子からモナドを導出するために使われることで知られている。内部処理を伴わない単なる関数適用をモナドで表現する目的でIdentityモナドを使うことは、Haskellではあまりしないのかもしれない。しかし、まったく意味がないというわけではない。モナドを利用することで、モジュール性が高まりプログラムの見通しが良くなる。「Functor且つApplicative且つモナド」ではないコンピューテーション式をわざわざ作るよりかは、Identityモナドを使った実装の方が見通しの良いプログラムが書けるかもしれない。
ではやってみよう。
FSharpxには標準で実装されていないため、まずはIdentityモナドをつくる必要がある。
module Identity = type M<'T> = M of 'T let mreturn x : M<'T> = M x type IdentityBuilder () = member this.Return (x) = mreturn x member this.Bind ((M x),f) : M<'U> = f x let identity = IdentityBuilder () open FSharpx open Operators let inline returnM x = returnM identity x let inline (>>=) m f = bindM identity m f let inline (=<<) f m = bindM identity m f let inline (<*>) f m = applyM identity identity f m let inline ap m f = f <*> m let inline map f m = liftM identity f m let inline (<!>) f m = map f m let inline lift2 f a b = returnM f <*> a <*> b let inline ( *>) x y = lift2 (fun _ z -> z) x y let inline ( <*) x y = lift2 (fun z _ -> z) x y let inline (>>.) m f = bindM identity m (fun _ -> f) let inline (>=>) f g = fun x -> f x >>= g let inline (<=<) x = flip (>=>) x
利用側
open System open Library1 open Library1.Identity type Rarity = |R of string |Other // ガチャアイテム let a,b,c,d,e,other = R("緒方智絵里"), R("間中美里"), R("黒川千秋"), R("川島瑞樹"), R("若林智香"), Other // コンプ let comp = [a;b;c;d;e] // コンプ対象アイテムが出る確率 let probability = 0.12 // 12% // 1ガチャあたり300円 let printg = printGacha 300 "円" (fun () -> printfn "[眠れる姫君]星井美希を手に入れた!") let compGacha x = identity { let comp,d,probability,lst,count,total,r = x let lst,count,total,r = completeGacha comp d probability lst count total return (comp,d,probability,lst,count,total,r ) |> printg } let mogemasu () = (comp, other, probability, [], 0, 0, false) |> fun x -> compGacha x >>= compGacha >>= compGacha >>= compGacha >>= compGacha // 別の書き方 //let mogemasu () = // (comp, other, probability, [], 0, 0, false) |> fun x -> // identity { let! x = compGacha x // let! x = compGacha x // let! x = compGacha x // let! x = compGacha x // let! x = compGacha x // return x } mogemasu () |> ignore System.Console.ReadLine () |> ignore Console.ReadLine () |> ignore
Identityモナドを用いて実装することにより、冗長なコンピューテーション式をわざわざ作らなくても、見通しがよいコードを書くことができた。しかも、これはモナドであるためモジュール性が高い。その証拠にモナド則3の結合則から「何枚揃えるまでガチャを行うか」についての抽象を導き出すことができる。
例えばこうだ。
open System open Library1 open Library1.Identity type Rarity = |R of string |Other // ガチャアイテム let a,b,c,d,e,other = R("緒方智絵里"), R("間中美里"), R("黒川千秋"), R("川島瑞樹"), R("若林智香"), Other // コンプ let comp = [a;b;c;d;e] // コンプ対象アイテムが出る確率 let probability = 0.12 // 12% // 1ガチャあたり300円 let printg = printGacha 300 "円" (fun () -> printfn "[眠れる姫君]星井美希を手に入れた!") let compGacha x = identity { let comp,d,probability,lst,count,total,r = x let lst,count,total,r = completeGacha comp d probability lst count total return (comp,d,probability,lst,count,total,r ) |> printg } let mogemasu n = (comp, other, probability, [], 0, 0, false) |> fun x -> let cg = [for i in 1..n-1 do yield compGacha] List.fold (fun m f -> m >>= f) (compGacha x) cg mogemasu 5 |> ignore System.Console.ReadLine () |> ignore Console.ReadLine () |> ignore
List.foldで必要回数分のモナドを結合することで、mogemasu関数を汎化することができた。
なお、List.foldでモナドを結合している部分は、下記のようにList.foldBackに書き直しても同様に動作する。このことからもモナド則3を満たしていることが確認できる。
let mogemasu n = (comp, other, probability, [], 0, 0, false) |> fun x -> let cg = [for i in 1..n-1 do yield compGacha] List.foldBack (fun m f -> f >>= m) cg (compGacha x)
まとめ
そのまま適用しただけでは何もしてくれないので、一見使いどころがなさそうなIdentityモナド。しかし、使えないようでいて実は割と使えるかもしれない、という話題でした。
読者の中には記事の誘導によってうまいこと騙されている人もいるかもしれないけど、
いや...、つーかさ。それ再帰で書けばいんじゃね?(核心
i l l ヽ ヽ\\ ヾy ‐-~~~ ヽ、 \ ヽ ヽ ィ ヽ~\ ヽ ヽ `、 / ー-、 \ `、 / ヽヾヽ\ ヽ\ ヽ、 、 // / |\ ヽ、 ヽ ヽ | l`、 / | | l , 、\\\\ \ | l 丶 | l |. 、! \ \ ー '''' ヽ、ヽ l | | ` . |.l | r'} 、 \,,、 、__,,、-‐''`ヽ | | | | l.l | ( { `ー''丶 '''ー- '´ |/ヽ | | | ii | l | ヽ; | |' i| l | | | i ヽ .l `i. i ノ, / / /// / __________ \. l ヽ. ヽ /`" / // |~ヽ / ヽ. ヽ _,,,,,,_ /r、 / / | | <またつまらぬコードを書いてしまった。 \ /llヽ ‐-、`' /1| ヽ / /| | \__________ / ||∧. / | | \-‐' | | _ ,、 -/l ||{ ヽ,,,,,,,,,/ .| | |ヽ、、 | | _,、- ' ´ |. ||{ | | |ヽ、 ゛| |、,,_
関連記事
FizzBuzz問題から学ぶモナド
http://d.hatena.ne.jp/zecl/20110711/p1
ふと、Seq.tryFindの変な(誰得な)使い方を思いついた。F#でbreakとcontinue再び。
以前、「F#で楽々breakとcontinue。継続モナドまじパネぇっす!」を書きました。
確かに楽々ではあるんですが、継続モナドとかマジで難しいですよ。
しかも、Visual Studioでデバッグとかまともにできないですし...(´・ω・`)ショボーンな気持ちになっちゃいます。
F#でbreakとcontinue再び
ふと、Seq.tryFindの変な使い方を思いついちゃいました。
ループのbreakとcontinueっぽいものを表現するのに利用できるのではないか、と。
例えば、こんな風に書けます。「do! continue' else」のところがカッコワルイのはご愛嬌。
open System printfn "%s" "----- for" let hoge = let x = ref "/(^o^)\" loop {for i in [1..10] do if i = 5 then printfn "%s" "five" do! continue' else if i = 2 then printfn "%s" "two" do! continue' else printfn "%d" i if i = 7 then printfn "%s" "!!!" x := "\(^o^)/" return break' printfn "%d" i printfn "%s" "!" } !x hoge |> printfn "%s" printfn "%s" "----- while" let fuga = let x = ref "/(^o^)\" loop {let i = ref 0 while !i < 6 do i := !i + 1 if !i = 5 then printfn "%s" "five" do! continue' else if !i = 2 then printfn "%s" "two" do! continue' else printfn "%d" !i if !i = 7 then printfn "%s" "!!!" x := "\(^o^)/" return break' printfn "%d" !i printfn "%s" "!"} !x fuga |> printfn "%s" Console.ReadLine () |> ignore
実行結果
----- for 1 ! two 3 ! 4 ! five 6 ! 7 !!! \(^o^)/ ----- while 1 ! two 3 ! 4 ! five 6 ! /(^o^)\
LoopBuilder
Seq.tryFindの使い方が変ですw optionの使い方が変ですw
// へぼいループビルダー type LoopBuilder () = let while' gd body = (fun _ -> let b = gd() if b then if Option.isSome (body ()) then Some () else body () |> (fun _ -> None) else Some ()) |> Seq.initInfinite member this.While(gd,body) = while' gd body |> Seq.tryFind (fun x -> Option.isSome x) |> ignore member this.For (s, f) = s |> Seq.tryFind (fun x -> Option.isSome (f x)) |> ignore member this.Zero () = None member this.Combine (a,b) = a |> function |Some x -> Some x |_ -> b() member this.Return (x) = x member this.ReturnFrom (x) = Some x member this.Bind (m,f) = m |> function |Some x -> f x |> Some |_ -> None member this.Delay f = f member this.Run f = f () let break' = Some () let continue' = None let loop = LoopBuilder ()
わー!まったく難しいことをしていないシンプルな実装で、ループのbreakとcontinueな動作を表現できちゃったっぽいよ?
でも、やっぱり「do! continue' else」の部分がトテモカコワルイ。誰得かと(´・ω・`)
Imperative computation builder
ちゃんとカッコヨクやりたい人は、tomaspさんの「 Imperative computation builder 」あたりをあたった方が間違いなくよいです。
http://tomasp.net/blog/imperative-ii-break.aspx
http://fssnip.net/40
お知らせ
第61回CLR/H勉強会(TechParty2011)で、F# MVPのぶひささん( @nobuhisa_k )と、ASP.NET MVPさかもとさん( @jsakamoto )と、「F#パネルディスカッション 2011」に登壇します。F#の魅力についてお話したいと思います。
CLR/H 公式ページ
http://clr-h.jp/
TechParty2011
http://techparty2011.iinaa.net/
快刀乱麻を断つモナド - F#とIOモナドとコンピューテーション式の奥義と
,. -‐'''''""¨¨¨ヽ (.___,,,... -ァァフ| あ…ありのまま 今 起こった事を話すぜ! |i i| }! }} //| |l、{ j} /,,ィ//| 『F#でIOモナドっぽい何かを作ろうとしていたら、 i|:!ヾ、_ノ/ u {:}//ヘ いつのまにか、全然別モノのLazyモナドをつくっていた。』 |リ u' } ,ノ _,!V,ハ | /´fト、_{ル{,ィ'eラ , タ人 な… 何を言ってるのか わからねーと思うが /' ヾ|宀| {´,)⌒`/ |<ヽトiゝ おれも 何をされたのか わからなかった… ,゙ / )ヽ iLレ u' | | ヾlトハ〉 |/_/ ハ !ニ⊇ '/:} V:::::ヽ 頭がどうにかなりそうだった… // 二二二7'T'' /u' __ /:::::::/`ヽ /'´r -―一ァ‐゙T´ '"´ /::::/-‐ \ 素の「Lazy<'T>」の入れ子だとか 「unit -> 'T」の連鎖だとか / // 广¨´ /' /:::::/´ ̄`ヽ ⌒ヽ そんなチャチな遅延合成じゃあ 断じてねえ ノ ' / ノ:::::`ー-、___/:::::// ヽ } _/`丶 /:::::::::::::::::::::::::: ̄`ー-{:::... イ もっと恐ろしい言語指向プログラミングの 片鱗を味わったぜ…
に至るまでの前置き(能書き)が無駄に長いです。
F#でIOモナドの話についてのみ書くつもりが、モナド全般の話になってしまいました。
それにしてもこれは一体誰向けの記事なのか、書いた本人もよくわからないんだぜ…(ごくり)。
プログラミングの世界のモナドとは、計算を組み合わせてより複雑な計算を構築するための、戦略の抽象のことを言います。モナドと言えばHaskellが有名ですが、モナドという抽象概念は言語依存のものではありません。
「モナドは関数プログラミングにおける戦略の抽象である。」と言いました。戦略には計算に応じてさまざまな種類があり、それぞれに名前、問題、適用可能性、特徴、結果などがあります。たとえば、Maybeモナドという名前の戦略がある。これは「失敗する可能性のある計算の合成」という問題に対する解決策を持っている。複数の失敗するかもしれない計算を合成し、ひとまとめの失敗するかもしれない計算として扱いたいような場合に適用することができる。「失敗したらその後の処理はしない」という特徴を持っている。結果として、入れ子が深くなりがちな失敗可能性のある計算の連鎖について、複雑な計算をよりシンプルに表現することができる。感のよい人はもう気づいているかもしれないが、「これって、もしかしてデザインパターン?」そう、「○○モナド」をそれぞれの問題領域を解決するためのパターンとして捉えることができます。そしてそのパターン全体に係る「モナド」は、それぞれの○○モナドに対して同じ枠組みを提供している“より高い抽象”と言うことができます。モナドは同じ枠組み(同じ概念)の下で、実にさまざまな問題領域に対して解決策を提示してくれます。つまり、関数プログラミンにおける「メタデザインパターン」あるいは「メタ戦略」、「メタ理論」がモナドであると捉えることもできます。この高次な概念は、もともと数学の一分野である圏論から来ています。なぜ圏論のモナドなんていうプログラミングと何の関連もない数学的構造が、プログラミングのあらゆる問題の解決策を示していたり、多くのプログラミングパターンを説明することができるのかはわかりません。そこには深イイ理由があるのかもしれないし、ただの偶然なのかもしれない。ただこれだけは言える。「数学SUGEEEEEEEEEEE」と。
Haskellの「Monad」は、モナドの抽象概念を適用した単なる型クラスのひとつです。モナドはメタ戦略なので、他の言語でもモナドを表現することはできます。しかし、型クラスと同程度の表現力を持っていない限り、Haskellほど美しくモナドとその周辺事情を表現することはできません。HaskellのMonadは型クラスによって非常に洗練された表現をされているので、モナドを勉強する上でとても参考になります。なにかを学ぶ場合、抽象的な話だけではわかりにくいので、具体的な例で考えてみると理解の助けとなります。Haskellの知識が少し必要ですが、id:kazu-yamamotoさんの「QAで学ぶMonad - あどけない話」の説明がすばらしくわかりやすいです。
「モナドは難しい」、「モナドは象だ」、「モナドかわいいよモナド」、「このモナドの説明を書いた人はこんなモナドの説明を読んでいます」、「モナドはメタファーではない」、「モナにゃんぺろぺろ」、「圏論モナドこそ至高」など、実にさまざまな声を聞きます。Real World Haskell(オライリー)には、「モナドを使っていると苛々します」という皮肉めいた記述が現れたりもします。
憧れの(闇の)カンスウガタゲンガー達が語る「モナドの本当の意味」を理解しようとすると、やはり圏論の概念やら定理やらの正確な理解が必要なのかもしれず。でも、その域に達するには、おそらく私を含めた一般的なプログラマには険しくも困難な道。出口の見えない試練に身を投じることになります。
しかし、多くのカンスウガタゲンガーは、圏論の本質的な意味やその定理に強いわけではなく。それにもかかわらず、彼らはモナドを使ったプログラムを書きます。小難しい概念を正確に理解することはできなくても、モナドを利用してメリットを享受することはできるんです。そしてモナドはとても便利なんです。実際のところ「モナド則*1」さえわかっていればほとんどの場合は困りません。使っているうちにモナドの性質が徐々にわかってきて、いずれ立派なモナグラマ−になれるかもしれない。もしかすると、みんなに愛でられるような可愛いオレオレモナドを発明できるようになるかもしれない。
C++をモナドに置き換えて音読してみましょう。そんな感じです。
単に関数プログラミングにおけるモナドの実用性あるいは利便性という意味だけで言うと、モナド則(モナドが守るべき3つのルール)を満たす基本演算の定義をすれば、あらゆる操作を同じ枠組みのもとでジェネリックに連鎖できる仕組みを利用することができる、ということに他ならない。モナドがプログラミング上で表現している抽象は、ジェネリックな関数合成と言いかえることもできる。関数合成とモナドの合成の違いを考えると、モナドはジェネリックな合成であり、異なる型の値を持つことができる点で異なる。これはそれぞれのシグネチャを比較するとわかりやすい。
(.) :: (b -> c) -> (a -> b) -> (a -> c)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
(>>=)ではm aやm bと表されているように、合成時に異なる型の値が関わることがわかる。
Bind演算子(>>=)は、ある関数で値になんらかの処理をして、その値を次の関数に値を受け渡たしていく。ここでのある関数というのは、もちろん(a -> m b)のことであり、a は前の関数が返したモナドから値を取り出したものである。どのようにモナドから値を取り出して次の関数の引数にするのかを決定するのがBind演算子(>>=)というわけ。より関数型っぽく言いかえると、どのように(a -> m b)をm aに適用するのかを決めるための定義がBind演算子(>>=)。
では最初の、先頭にあたるモナドは一体どこからやってくるのか?ということになる。そこで、モナドが定義しなければならないもうひとつの定義、returnが導出される。returnは、なんらかの値を新たなモナド型の値にする関数で、しばしばモナドに包むなんて表現されたりもする。
return :: Monad m => a -> m a
シグネチャも御覧の通りで、何も難しいことはしていません。Haskellでは、return や Bind演算子(>>=)はただの関数ではなくて、Monadクラス(型クラス)のメソッドになっていて、これによってアドホック多相が使える。なので、モナドがどのように合成されるのかという実装の詳細を意識しなくてもよいようになっている。つまり、文法上は同じように(>>=)でモナドが合成されていたとしても、そのモナドの種類によって実際の合成の挙動は異なる。つまるところこれはオーバーロード (多重定義)のことを言っていて、F#ではコンピューテーション式を用いることで、似たようなことができる。
モナドがやっていることは、実は強力な関数合成の一種であって、実は複雑なことはなにもしていない。にもかかわらず、モナドが難しいと言われる理由は、モナドが関数合成とジェネリックとアドホック多相が絡み合う複合体として表現される抽象であるからかもしれない。加えて、Haskellにあらわれる「クラス」や「インスタンス」という用語が、一般的なオブジェクト指向のそれの意味とまるで異なることも一因かもしれない。こういった事情があるので、日頃から抽象的な設計手法やメタプログラミングにあまり馴染みのないプログラマが突然モナドに触れると火傷してしまうというのは致し方ないことだ。プログラミングの基礎的素養は必要だが、順を追って丁寧に学習していけば「モナドがなにをやってくれているのかを理解する」こと自体はそれほど難しいことではない。
憧れの(闇の)カンスウガタゲンガー達は、暇さえあればモナドの話をしています。彼らは三度の飯よりモナドを好みます。なぜ彼らはモナドに夢中になるのか。モナドはそんなにも魅力的な存在なのでしょうか。そしてモナドは一体なんの役立つのでしょう。モナドはジェネリックな関数合成だと説明しました。しかし、実際に何の役に立つかはあまりピンとこないかもしれません。
モナドはメタ戦略だといいました。プログラミングにおいて戦略は重要なファクターです。関数プログラミングを構造化するための戦略を定義するのに極めて重要なツール、それがモナドです。モナドなくして関数プログラミングは語れません。モナドには、そう言い切れるそれを特に決定付けるいくつかの特徴があります。
(1)複雑な計算の可読性向上が期待できる。モナドは美しい!
計算を繋ぐBind演算子(>>=)によって、関数プログラミングを命令型的に表現することができる。モナド合成時の実装の詳細を意識しなくてもよいようになっているので、コードを劇的に単純化することができ、可読性の向上が見込めます。HaskellのBind演算子(>>=)は、F#のコンピューテーション式(ビルダー)では
と表現されて、 シグネチャは、
です。
Bindの働きは、平たく言えばただの関数の結合ですが、通常の関数の合成を拡張した働きを持たせるのが一般的で、たとえば、Maybeモナドでは、成功と失敗を表す2種類のデータを次の関数へ伝達することができるようになっている。F#でoption型を利用することを考えた場合、Maybeモナドは欠かせないものとなります。→ F#プログラマのためのMaybeモナド入門 - みずぴー日記
ただし、モナドを利用することでコードの美しさが保てるかどうかは、実装言語によって大きく変わってくきます。たとえばHaskellには型クラスがあるのでとてもエレガントに表現されている。コードも非常に美しいです。F#ならコンピューテーション式できれいに表現できる。Scalaではfor式できれいに表現できる。他の言語だとまた状況は変わってきます。
(2)カプセル化とモジュール性を確保。頑丈なプログラミングを促進する!
モナドのBindによる合成においては、合成時の値がモナドの外に漏れ出ることがないのでモジュール性が保証されます。この特徴によって、プログラマはモナド内でプログラムしたものが他の領域に干渉することを懸念する必要がないので、とても幸せです。これはモナドプログラミングの純粋さを示すものであり、計算そのものとその合成戦略を分離していることを意味しています。つまりモナドは安全で動的に組み合わせ可能な計算として機能します。
まったくの別物なので、誤解を招いてしまいそうで例えることを躊躇しそうになるが、オブジェクト指向プログラマであれば、動的に機能拡張が可能という意味では、GoFデザインパターンにおける Decoratorパターン をイメージすると、モナドの拡張可能なモジュール性というものを想像しやすいかもしれない(あくまでイメージで)。オブジェクト指向パラダイムのDecoratorパターンでは、オブジェクトとオブジェクトを組み合わせることで、新たに複雑なオブジェクトを生成していました。 関数プログラミングのパラダイムでは、より高い抽象であるモナドでそれを高い次元で実現する。状況に応じてさまざまな用途のモナドを使い分けたり、必要に応じて複数のモナドを合成したりします。
(3)モナドによる柔軟性の高い関数型プログラミングは、アジャイル性を与える!
モナドは汎用なインターフェイスを持ち、関数の純粋さを保つ性質がある。これによりコードのリファクタリングが容易になり、また多くの場合は反復開発が容易になる(アジャイル性の向上)。モナドを用いない関数型プログラミングより、モナドを利用したプログラムはより柔軟性が高くなりやすい。これはモナドによる計算が、一連のプログラムとデータをカプセル化し、モジュール性があることからも説明できます。アジャイルというとオブジェクト指向とセットで語られることが多いが、真にアジャイル性を追及するならば、関数プログラミングを選択することがベターであることは確定的明らかである。前ふりが長くなりましたが、やっとこの記事を書くきっかけとなったIOモナドの話をします。IOモナドは副作用をモナドの中に封じ込めて、純粋関数を表現するための仕組みを提供するパターンであり、非常に巧妙な仕組みです。少し言い換えると、副作用のある部分と副作用のない部分を明確に分離するための仕組みを提供する戦略を持っています。
この夏、「ATEND - 第0回 スタートHaskell」というイベントがあるそうです。定員150人にもかかわらず、すでに定員オーバーしています(すごい)。最近モテ言語と言われているだけあって、Haskell恐ろしい人気です。そちらのキャッチフレーズ(?)より以下を引用させていただきます。
これが意味するところがそういう意味なのかはわかりませんが、IOモナドはHaskellにおいて、脱Haskell初心者の鬼門のひとつとされているモナドのひとつで、とりわけ特殊な存在として捉えられています。しかし、「IOモナドはモナドじゃないよ」「IOモナドなんて勉強しなくてもモナドは使えるよ」などと言う声もあります。それでいて魔術的な一面も持っていたり、IOモナドはモナドの中でも他とは毛色の違う不思議な存在です。これは、HaskellのIOモナドの定義がプラットフォーム依存(評価器, 実行器依存)であることに起因します。Haskellの「純粋さ(参照透明性)」に密接な関わりをもっています。さらにHaskellは遅延評価を採用している言語であることも関係してきます。このような特徴を持つIOモナドは特別な存在です。したがって、設計思想の異なる他の言語でHaskellのIOモナドを真似ることは難しいでしょう。
IOモナドについて語るには、まずは副作用とは何なのかを知る必要がありました。
副作用とは、関数やメソッドなどのある処理(計算)単位が、外部の環境になにかしら影響を及ぼすことを言います。つまり、処理の中で論理的な状態を変化させて、それ以降で得られる結果になんらかの影響を与えることです。代表的な例としては、コンソールを使った入出力や変数への値の破壊的代入、UIへの描画などがあげられます。
副作用がなく一貫性のある処理単位を純粋関数と呼ぶ。
「純粋」という修飾は、つまりそれがコンポーザブルであることを意味していて、大きく2つの特徴を持っている。まず、「自己完結」しているという特徴がある。そのため、純粋関数は他との結び付きや相互依存を一切気にすることなく、関数を自由に合成することができる。そして、純粋関数は「状態を持たない」。純粋関数の値(計算結果, 返り値)は、常にその引数の値によって決定される。同じ引数を伴う関数の計算が、いついかなる場合も同じ値となる(いわゆる参照透明性)ときそれは純粋関数だ。外部の環境に対して副作用を伴うような関数を部品として扱うと、必ず危険を伴う(バグりやすい)。環境に依存していて、事あるごとに結果が異なるような関数を使うなんて正気の沙汰じゃない!という意見はもっともです。それに対して、「自己完結」していて「状態がない」という環境に依存しない特徴を持っている純粋関数は、コンポーサブルな安全な部品として扱える。
関数型言語でこそ、より意味のある純粋関数
関数を純粋にすると、外部環境を意識せずに済むので可読性が高くなります。状態を持たないので、コードのリファクタリングが容易にもなります。また、多くの場合は設計の変更に強く、反復開発が容易になりやすいでしょう(アジャイル性の向上)。他との依存性もないので単独でのテストも容易で、テストコードは関数を呼び出すことに集中できて、デバッグも容易になるなど良いことずくめ。この純粋関数の特徴をより高く抽象的に昇華されたのがモナドで、合成可能なメタ純粋関数として捉えることもできる。純粋関数は、もちろんHaskellだけのものでもなければ、ScalaやF#などを含めた関数型言語だけのものでもない。Java, C#, VB.NETというような、一般的なオブジェクト指向言語での実装においても、副作用をともなわない純粋関数を書くことは常に意識されてきた。ただ、オブジェクト指向言語では、関数をファーストクラス(第一級の値, first-class)*2として扱えないため、関数を部品として扱えず、純粋関数が持つ特徴である「自己完結」していて「状態を持たない」ので構成可能で安全な部品として扱えるという最大のメリットを享受することができない。それに対して、関数型言語はコンポーサブルな純粋関数を自由に組み合わせることで、より大きな純粋関数を作り出して効果的に再利用することができる。モナドについても同様のことが言え、純粋関数よりもさらに強力さを持つ。これは「なぜ関数型で書くのか」の理由のひとつと言える。
初めて読んだHaskellの本「ふつうのHaskellプログラミング」にはこんなことが書いてありました。
IOモナドにはふたつの目的があって、まずひとつは「副作用を伴う処理(アクション)の評価順序を確実に指定する」こと。もつひとつは、「参照透明性を保ったまま副作用を実現する」ということでした。
あるいは、「IOモナドは、まだ入出力を実行していない世界をアクションの中に包み込んでいて、アクションが評価されると、入出力の結果と、入出力を実行した後の世界が生成されます。」というようなことも書いてありました。3年くらい前に読んで、なんとなくわかったような気になっていましたが実は何もわかっていませんでした。そのときはまだ他の関数型言語も学んだことがなく、Haskellにも触れたばかりだったので。とここに言い訳をしておく。今思えば、IOモナドからモナドを入門するのは、ちょっと難しいのではないかと思います。
IOモナドの目的ですが、まず、ひとつめの目的は、Haskellが遅延評価*3を特徴としている言語であるということが深く関係しています。なので、主にHaskellというプログラミング言語におけるIOモナドの立ち位置としての目的と言えるかもしれません。ふたつめの目的は、副作用のある計算を純粋関数で表現すること。ひいては、副作用のカプセル化とモジュール性の確保が目的であると言えます。Haskellでは、副作用を持ち込みながら参照透明性を保つ手段として、モナドの持つ特徴をうまく利用していて、IOモナドを一方向モナドとして扱い、実行器まで巻き込むことで副作用をうまく封じ込めることに成功している。実行器まで巻き込むということがあまり説明されていないことが多いので、初心者は路頭に迷ってしまうのだと思う。
一方向モナドの素晴しい機能は、プログラムのモナドではない部分の関数型の性質を破壊することなく、そのモナド演算子中で副作用をサポートすることができるというものです。
Haskellでは、すべての計算はトップレベルのIOモナドの中で起こります。実行器まで巻き込むというのはつまりそういう意味です。副作用は全てIOモナドにリフトされます。開発者が関数の外部環境に勝手にアクセスすることはできないようになっています。なので純粋です。「副作用のある計算を純粋関数で表現する」という、副作用と純粋関数の意味を考えると一見矛盾した要求に対して、モナドはみごとな解決策を与えているというわけです。IOモナドが一方向モナドであるため、純粋関数の役割を果たしていて、副作用を内包しながらも参照透明性が確保されている。これは実に巧妙というかなんというか、面白い。
F#やScalaなど副作用を許容する非純粋関数型言語においても、出来る限り参照透明性を保つようにプログラムを組むことが推奨される。非純粋関数型言語の「純粋さ」は開発者にゆだねられています。
IOモナドが副作用を構成可能な純粋関数としてくれるのならば、それは非純粋関数型言語屋にとっても朗報です。副作用をカプセル化してコンポーサブル化するということも、IOモナドの目的のひとつと言うことができるでしょう。
さて、無駄に長い前置きは以上です。
ここからが本題。
Haskellは純粋関数型言語です。副作用に対して厳格です。そして遅延評価を採用しています。よく訓練されたカンスウタガゲンガーは、副作用のある関数とない関数を分けて実装することを考えるそうです。しかし、それに依存したのでは、プログラムの純粋さを死守することはできません。そこで、HaskellではIOモナドというプラットフォーム依存のモナドを導入することでこの問題をエレガントに解決しました。言い方を変えると、Haskellでは、IOモナドによって副作用のある部分とない部分を完全に分けることをプログラマに強制させる仕組みを設けました。ゆえにHaskellは純粋でいられるのです。
先に「設計思想の異なる他の言語でHaskellのIOモナドを真似ることは難しいでしょう。」と述べました。F#でHaskellのIOモナドを完全模倣することはできません。が、「F#の立ち位置としてのIOモナド」ならば表現することはできなくはありません。
F#は非純粋関数型言語です。seq, asyncなどの組み込み済みのコンピューテーション式が提供されてはいますが、コンピューテーション式(によるモナドの実装)は基本的に自作してくださいというスタンスです。当然、IOモナドをはじめとしたHaskellで標準的に備わっているモナドはありません。
また、副作用に対しては比較的寛容で、プログラマのさじ加減ひとつでプログラムは純粋にも破壊的にもなります。
もちろん我々カンスウガタゲンガーは出来る限り純粋さを保つことを望んでいます。F#で開発する場合でも、副作用を適切に分けてプログラミングしたいという要求は当然あります。すべての副作用をIOモナドの中に封じ込めるいうのは、F#の場合あらゆる面で現実的ではありませんが、局所的に副作用をIOモナドによって管理することは、それなりに意味のあることです。なぜなら、よく訓練されたカンスウタガゲンガーは、副作用のある関数とない関数を分けて実装することを考えるから*4。
以上のことを踏まえて、F#でIOモナドを模倣することを考えてみます。しかし、これには大きな問題がいくつかあります。ひとつは、F#がオープンソースであるとは言え、Haskellのように評価器、実行器まで巻き込むことはできないこと。この点に関しては即詰みです。なので評価器と実行器のことは忘れて、能動的に着火する必要がある関数としてIOモナドの表現を考えます。また、副作用を含む合成可能な純粋関数としてのIOモナド(副作用のコンポーサブル化)を実現することを考えます。
では、さっそくF#でIOモナドを書いてみよう!となるわけですが、その前に、F#のもっとも優れた機能のひとつであり、F#でモナドを書く時に必要不可欠であるコンピューテーション式について簡単に触れておきたい。
コンピューテーション式は、あるF#コードを独自の解釈でユーザー定義することができる機能です。
つまり、F#言語構造の意味解析をプログラマが上書きしちゃえるんです。いわゆる言語指向プログラミング(LOP)を可能としています。非常に強力で面白い機能です。コンピューテーション式は、F#でモナドを実装するのにとても便利です。ただ、モナドを書くためだけに存在するものではなく、ドメイン固有言語(DSEL)としての利用も一般的です*5。for, if, yield!, use!, try withなどの補助的なキーワードの再定義も充実していて、モナドをより扱いやすいように式をカスタマイズすることもできます。目的によっては、コンピューテーション式(ビルダー)がモナドである(モナド則を満たす)必要はありません。
namespace CompExpr.Monad open System open Microsoft.FSharp.Core.Operators.Unchecked [<AutoOpen>] module Base = /// ComExprBase type ComExprBase () = member this.Using (x : #IDisposable, f) = use x = x f x member this.TryWith (f, handler) = try f () with e -> handler e member this.TryFinally (f, final) = try f () finally final () /// IOモナドモジュール module IO = /// IO判別共用体 type IO<'T> = internal IO of 'T let (>>) (_ : IO<_>) f = f () /// bind let (>>=) (IO(a)) k : IO<_> = k a let ioBind m k = m >>= k /// return let ioreturn a = IO a /// zero let iozero = IO () /// IOビルダー type IOBuilder() = inherit ComExprBase () member this.Bind (m, k) = m >>= k member this.Return x = ioreturn x member this.ReturnFrom m = m member this.Zero() = iozero member this.Delay (f : unit -> IO<_>) = f member this.Combine (g, f) = g >> f member this.For (xs : #seq<_>, f : (_ -> IO<unit>)) = xs |> Seq.fold (fun _ x -> f x) iozero member this.Yield (x) = ioreturn x member this.YieldFrom (x) = x member this.While (condition, (f : (_ -> IO<unit>))) = while condition() do f () |> ignore iozero /// IOビルダーを生成します。 let io = IOBuilder () let iowrap f a = IO (f a) let iowrap2 f a b = IO (f a b) let iowrap3 f a b c = IO (f a b c) let putChar = printf "%c" |> iowrap let putStr = printf "%s" |> iowrap let putStrLn = printfn "%s" |> iowrap let ioignore = fun _ -> iozero let getLine = System.Console.ReadLine |> iowrap let getKey = System.Console.ReadKey |> iowrap let getCurrentTime = io { return DateTime.Now } <| () let liftM f m = io { let! x = m <| () return f x} let join (IO(IO(a))) = io{ return a } type Either<'T, 'U> = |Left of 'T |Right of 'U //try :: Exception e => IO a -> IO (Either e a) let tryIO io = try Right ((io:unit -> IO<_>) <| ()) with e -> Left (id e) //catch :: Exception e => IO a -> (e -> IO a) -> IO a let catchIO io f = try (io: unit->IO<_>) <| () with e -> f e //finally :: IO a -> IO b -> IO a let finallyIO ioa iob = try (ioa:unit -> IO<_>) <| () finally (iob:unit ->IO<_>) <| () |> ignore let tryCatchFinallyIO io fio = try try Right ((io:unit->IO<_>) <| ()) with e -> Left e finally (fio:unit -> IO<_>) <| () |> ignore let getContents = seq { let condition = ref true while !condition do let line = System.Console.ReadLine() if line = null then condition := false yield fun () -> ioreturn line} let interact f = io { for x in getContents do let! x = x <| () putStr (f x) |> ignore }
とりあずではありますが、IOモナドっぽい何かと、その周辺機能の一部を実装しました。モナド則も満たしています。着火タイミングについては、IOモナドの合成結果をunit -> IO<'T> にすることでプログラマが制御できるようにしています。また、コンピューテーション式の特徴を活かしてビルダー内で、if, for, use!, try with, try finally などが利用できるようにしました。
let hello = io { return! putStrLn "Hello,World!" } let inout = io { let! s = getLine () return! putStrLn s } let h2 = io { do! hello () do! hello () return "こんにちは、世界!"} let h3 = io { let! s = h2 () do! hello () putStrLn s |> ignore do! inout () do! hello () return "さようなら" } let fire = io { let! s = h3 () do! hello () return s } fire () |> ignore ()
実行結果
Hello,World! Hello,World! Hello,World! こんにちは、世界! やっほー やっほー Hello,World! Hello,World!
IOモナドであるinout内のgetLineで入力を求められるので、「やっほー」と入力します。すると、実行結果は上記のようになります。とりあえずOKですね。「さようなら」はignoreで捨てられました。
IOモナドの処理順序
モナドは評価順序を決定するという特徴をもっていました。それは遅延評価の場合のみ関係してくるわけではありません。それを確認するために、forld(foldl)とfoldBack(foldr)で、IOモナドの合成順序を逆にしても評価順序に変化がないことを確認してみましょう。let aiueo = [io { return! putStrLn "あ"}; io { return! putStrLn "い"}; io { return! putStrLn "う"}; io { return! putStrLn "え"}; io { return! putStrLn "お"}] aiueo |> Seq.iter (fun x -> x () |> ignore) let compose a b = fun () -> ioBind (a()) b Console.WriteLine ("-----foldしても") |> ignore let aiueo'' = List.fold (fun (a:unit->IO<_>) b -> compose a b) (fun ()->iozero) aiueo aiueo'' () |> ignore Console.WriteLine ("-----foldBackしても") |> ignore let aiueo' = List.foldBack (fun (a:unit->IO<_>) b -> compose a b) aiueo (fun ()->iozero) aiueo' () |> ignore Console.WriteLine ("同じ結果になるよね?") |> ignore
実行結果
あ い う え お -----foldしても あ い う え お -----foldBackしても あ い う え お 同じ結果になるよね?
はい。いずれも同じ結果になりますね。Haskellの場合はさらに遅延評価も絡んでくるのでまた事情がちょっと変わってきますが、正確評価のF#において、IOモナドの処理順序が確定的に明らかになっていることが確認できました。
IOモナドの無限リスト
IOモナドによって副作用を包み込むことで、副作用をあたかもファーストクラスとして扱えるようになりました。つまり、副作用を含む関数をリストに格納できることを意味します。IOモナドで、副作用の無限リストを表現してみましょう。let hello = io { return! putStrLn "Hello,World!" } let infiniteHello = Seq.initInfinite (fun x -> hello) infiniteHello |> Seq.iter (fun x -> x () |> ignore)
Hello,World!フォーエバー…!!
5件だけ処理してみます。
let hello = io { return! putStrLn "Hello,World!" } let infiniteHello = Seq.initInfinite (fun x -> hello) infiniteHello |> Seq.take (5) |> Seq.iter (fun x -> x () |> ignore)
実行結果
Hello,World! Hello,World! Hello,World! Hello,World! Hello,World!
たいへんよくできました。
IOモナドと他のモナドを合成したい。
モナドを利用してプログラムを組んでいると、複数のモナドを組み合わせて扱いたくなるケースが多々でてきます。さまざまな領域の問題に対処するために必要なことです。例えば、IOモナドの中で「失敗するかもしれない計算」を扱いたいなんてことは、比較的多い要求のように思います。試しにIOモナドとMaybeモナドを組み合わせて利用してみましょう。let fire = io { let! iom = io {let! gl = getLine () return maybe { return gl } } <| () match iom with |Some x -> if x <> "" then putStrLn (x + x) |> ignore else putStrLn "入力してよねッ!ぷんぷくり〜ん(怒" |> ignore |None -> ()} fire () |> ignore
F#の立ち位置としてのIOモナドでは、上記のようにコンピューテーション式をネストして記述するかたちで、比較的簡単にモナドを合成することができます。ただ、ちょっと不吉なにおいがします。この程度のネストであれば許容範囲内かもしれないが、式が複雑化してくると可読性を犠牲にしてしまいそう。地獄のモナド「F#でモナドないわー、コンピューテーション式の多重入れ子ないわー、マジでないわー」と苛々しちゃうかもしれません。
そういえば、Haskellにはモナドを合成するための仕組みを持つ、モナド変換子というトランスレータがありました。IOモナドとMaybeモナドを合成するために、モナド変換子 MaybeTをちょっと真似てみましょう。
まず、合成対象のMaybeモナド
module Maybe = open IO let bind x k = match x with | Some value -> k value | None -> None let mreturn x = Some x type MaybeBuilder() = inherit ComExprBase () member this.Bind(x, k) = bind x k member this.Return(x) = mreturn x member this.ReturnFrom(x) = x member this.Delay(f) = f() member this.Combine(a, b) = if Option.isSome a then a else b () member this.Zero() = None member this.For(inp,f) = seq {for a in inp -> f a} member this.Yield(x) = mreturn x member this.YieldFrom(x) = x let maybe = new MaybeBuilder() let liftM f m = maybe { let! x = m return f x}
そしてモナド変換子 MaybeT
module Maybe = ...... module Trans = /// Maybeモナド変換子判別共用体 type MaybeT<'T> = internal MaybeT of 'T /// bind let (>>=) (MaybeT(a)) k : MaybeT<_> = k a let bindMaybeT m k = m >>= k /// return let maybeTreturn a = MaybeT a /// zero let maybeTzero = MaybeT () /// Maybeモナド変換子ビルダー type MaybeTBuilder () = inherit ComExprBase () member this.Bind(m, k) = m >>= k member this.Return(a) = maybeTreturn a member this.ReturnFrom(m) = m member this.Delay(f) = f() member this.Combine(a, b) = if Option.isSome a then a else b () member this.Zero() = maybeTzero member this.For(inp,f) = seq {for a in inp -> f a} member this.Yield(x) = maybeTreturn x member this.YieldFrom(x) = x // Maybeモナド変換子ビルダー let maybeT = new MaybeTBuilder () /// MaybeTはMonadIOのインスタンスだとかなんとか /// IO a をMaybeT IO aに持ち上げます let liftIO (m:IO<_>) = maybeT {return m} /// MaybeTはMonadのインスタンスだとかなんとか let liftM f m = maybeT { return f m } /// コンストラクター /// MaybeT IO a -> IO (Maybe a) let runMaybeT (MaybeT(IO(a))) = io { let! r = io { return maybe { return a } } <| () return r} module Unsafe = module IO = open IO /// 危険! (>_<) let unsafePerformIO (IO(a)) = a
MaybeTを使ってみます。
let fire = io { let iomaybe = Maybe.Trans.liftIO (getLine ()) |> runMaybeT <| () let! x = iomaybe match x with |Some x -> if x <> "" then putStrLn (x + x) |> ignore else putStrLn "入力してよねッ!ぷんぷくり〜ん(怒" |> ignore |None -> ()} fire () |> ignore
MaybeTを利用することで、IOモナドとMaybeモナドの合成をとてもシンプルに記述できました。それに、「ここでモナドを合成しているんだな!」ということが一目瞭然でわかりやすくなります。
F#でlazyれても、結局自分でファイヤーしなきゃスルーされてしまうわけで。あんまりありがたみないなあみたいな場面には割りと遭遇する。合成するような場合とか。
2011-06-20 16:31:57 via web
F#で遅延評価をするには、unit -> 'T で処理の実行を遅らせる。あるいは System.Lazy<'T>によって、遅延評価をカプセル化します。つまり、遅延評価の処理を合成したい場合は、lazy関数の中で合成したいすべてのSystem.Lazy<'T>に対してForce()メソッドを呼び出せばよいということになります。
let result = let a = lazy ("Hello,") let b = lazy ("World!") let h = lazy (a.Force() + b.Force()) lazy ([for i in 0..4 -> h.Force()]) Seq.iter (printfn "%s") <| result.Force()
たとえば、上記のような感じでlazyな関数は簡単に合成することができます。しかし、合成する際に必ず Force() で着火して評価しなればならず、着火漏れはバグの温床になりますし、見た目的にもどこか不恰好です。また、List.foldなどで、lazyな関数を合成したいような場合にも不便さを感じます。
F#のlazyは、System.Lazy<'T>型であらわされる遅延計算される値をカプセル化したものです。Lazy<'T>は遅延する値の入れ物とみなすことができます。ここで思い出されるのがモナド、あるいはコンピューテーション式です。モナドはジェネリックな関数合成を表現できて、どのように合成されるのかという実装の詳細を意識しなくてもよいという特徴がありました。ある入れ物に対して、モナド則を満たすようなBindとreturnを定義すれば、それはもう立派なモナドです。遅延評価な値を合成するための戦略を自作するとよさそうです。つまりオレオレモナドを作ることでこの不便さを解消できそうです。
とりあえず、基本的な実装をしてみます。以下のようになります。
namespace CompExpr.Monad module Lazy = type LazyBuilder () = let force (x : Lazy<'T>) = x |> function | Lazy x -> x /// bind let (>>=) (x:Lazy<'a>) (f:('a -> Lazy<'a>)) : Lazy<'a> = lazy (x |> force |> f |> force) /// zero let lazyzero = Seq.empty member this.Bind(x, f) = x >>= f member this.Return (x) = lazy x member this.ReturnFrom (x) = x member this.Zero() = lazyzero member this.Delay f = f() let lazyz = LazyBuilder () let lifM f (Lazy(a)) = lazy f a
利用側の実装サンプル
open System open CompExpr.Monad.Lazy let result = lazyz { let! a = lazy ("Hello,") let! b = lazy ("World!") return a + b} result.Force() |> (printfn "%s") |> ignore Console.ReadLine () |> ignore
実行結果
Hello,World!
で、とりあえずF#でコンピューテーション式として利用できるLazyモナドをつくることができたのですが、これだけだと利用する側は実はあまりうれしくありません。確かに、lazyな関数を合成する際に、Force ()メソッドを明示的に呼び出す部分を隠ぺいできましたが、この実装では単純な式を構築する場合にしか利用できません。lazyな関数をもっと直観的でかつ自由に組み立てることができる仕組みが欲しくなってきます。コンピューテーション式はそれができます。たとえば、コンピューテーション式内で、if式, for式, yield などを用いて遅延評価を合成しながら組み立てられるようにすると便利そうです。つまり、ドメイン固有言語(DSL)としての側面も持たせることで、さらに便利さが増します。
奥義とか言っちゃうと胡散臭さを禁じえませんが、あえてそう言い切っちゃいました。お察しください。
コンピューテーション式でドメイン固有言語(DSL)を作った経験をお持ちの方はおわかりの通り、
重要となってくるのは、Combine と Run をいかに使いこなすかということです。もちろん、コンピューテーション式で再定義可能なキーワードおよび、それに対応するすべてのメソッドが重要ですが、
コンピューテーション式を上手に使いこなす上で特に重要となってくるのが以下の2つメソッドです。
Combineメソッド
その名の通り式の「結合」を定義するためのものです。コンピューテーション式内の式の流れを統制するような役割を持っています。左から流れてくる式(式の境界線の上)を、右の式へ(式の境界線の下)受け流すというように、ムーディー勝山(消)をイメージすると理解しやすいです。さまざまな異なる式の変換をオーバーロード(多重定義)することで、柔軟な式を表現することができるようになります。ムーディー勝山は冗談なので忘れてください。
Runメソッド
これはコンピューテーション式の変換規則に明示されてはいませんが、ビルダークラス内で定義すると特殊な解釈がされます。Runメソッドを定義すると、builder {...}というコンピューテーション式を宣言すると同時にDelayメソッドが呼び出されます。つまり、最終的なビルダーの結果を決定づける定義を持たせることができ、最終的なビルダーの値を適切な型に変換する仕組みを提供していると解釈できます。Combineメソッドで扱っている式の流れにあわせて、最終的な式の結果を統一するのに役立ちます。もちろんオーバーロード(多重定義)可能です。Runメソッドの詳細については、The F# Draft Language Specification(http://goo.gl/7a1QH)を参照されたい。
F# Snippets(http://fssnip.net/)に投稿したものとして、冒頭でも紹介しましたが、LazyBuilderを作りました。
これは、ConbineとRunを効果的に利用した具体的な実装サンプルです。CombineおよびRunを多重定義することで、lazyな関数の合成を自然にコンピューテーション式内で表現可能にしています。また、IOモナドも扱えるように、F# Snippetsへ投稿したものに一部機能を追加しています。
namespace CompExpr.Monad open CompExpr.Monad.IO module Lazy = type LazyBuilder () = let force (x : Lazy<'T>) = x |> function | Lazy x -> x /// bind let (>>=) (x:Lazy<'a>) (f:('a -> Lazy<'a>)) : Lazy<'a> = lazy (x |> force |> f |> force) /// zero let lazyzero = Seq.empty let ioToLazyIgnore (xs:seq<Lazy<(unit -> IO<'T>)>>) = xs |> Seq.reduce (fun (Lazy(a)) (Lazy(b)) -> lazy (fun () -> (ioBind (a()) (fun x -> b()))) ) |> fun (Lazy(x)) -> lazy (x() |> ignore) let ioToLazy (xs:seq<Lazy<(unit -> IO<'T>)>>) = xs |> Seq.map (fun (Lazy(a)) -> a) |> fun x -> lazy (x) member this.Bind(x, f) = x >>= f member this.Return (x) = lazy x member this.ReturnFrom (x) = x member this.Zero() = lazyzero member this.Delay f = f() member this.Combine (a,b) = Seq.append a b member this.Combine (a,b) = Seq.append (Seq.singleton a) b member this.Combine (a,b) = Seq.append a (Seq.singleton b) member this.Combine (Lazy(a), Lazy(b)) = lazy Seq.append a b member this.Combine (a:Lazy<'T>, b:Lazy<seq<'T>>) = (a,b) |> function | (Lazy x, Lazy y) -> lazy Seq.append (Seq.singleton x) y member this.Combine (a:Lazy<seq<'T>>, Lazy(b)) = a |> function | Lazy x -> lazy Seq.append x (Seq.singleton b) member this.Combine (a:Lazy<seq<'T>>, b:seq<Lazy<'T>>) = let notlazy (xs:seq<Lazy<'T>>) = xs |> Seq.map (fun (Lazy(a)) -> a) a |> function | Lazy x -> lazy Seq.append x (notlazy b) member this.Combine (a:seq<Lazy<'T>>, b:Lazy<seq<'T>>) = let notlazy (xs:seq<Lazy<'T>>) = xs |> Seq.map (fun (Lazy(a)) -> a) b |> function | Lazy x -> lazy Seq.append (notlazy a) x member this.For (s,f) = s |> Seq.map f member this.Yield x = lazy x member this.YieldFrom x = x member this.Run (x:Lazy<'T>) = x member this.Run (xs:seq<Lazy<unit>>) = xs |> Seq.reduce (fun a b -> this.Bind(a, fun _ -> b)) member this.Run (xs:seq<Lazy<'T>>) = xs |> Seq.map (fun (Lazy(a)) -> a) |> fun x -> lazy x member this.Run (xs:seq<Lazy<(unit -> IO<unit>)>>) = xs |> ioToLazyIgnore member this.Run (xs:seq<Lazy<(unit -> IO<string>)>>) = xs |> ioToLazy member this.Run (xs:seq<Lazy<(unit -> IO<int>)>>) = xs |> ioToLazy let lazyz = LazyBuilder () let liftM f (Lazy(a)) = lazy f a let liftIO (Lazy(a)) = IO a
例1
let result = let h = lazyz { let! a = lazy ("Hello,") let! b = lazy ("World!") return a + b} lazyz { for i in 0..4 do yield! h } Seq.iter (printfn "%s") <| result.Force()
実行結果
Hello,World! Hello,World! Hello,World! Hello,World! Hello,World!
例2
CombineとRunを実装しているので、以下のように書くことができる。
let lazyp n = lazy (printfn "%d" n) let result2 = let a = lazyz { yield! lazyp 2 ()} lazyz { yield! lazyp 0 yield! lazyp 1 yield! a for i in 3..5 do yield! lazyp i yield! lazyp 6 yield! lazyz { yield! lazyp 7 yield! lazyp 8 ()} for i in 9..10 do yield! lazyp i yield! seq {for n in [11..15] -> lazyp n} yield! lazyz { yield! lazyp 16 yield! lazyp 17 ()} yield! lazyz { yield! lazyp 18 yield! lazyp 19 ()} yield! lazyp 20 () } result2.Force()
実行結果
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
例3
せっかくなのでIOモナドを利用したい。
let iom1 = io { return! getLine () } let iom2 = io { let! s = iom1 () return s} let iom3 = io { let! s = iom2 () return! putStrLn s} let result1 = lazyz { yield iom3 yield iom3 yield iom3 () } let ioResult1 = io { return result1.Force()} ioResult1 () |> ignore let iom5 = io { let! s = iom1 () putStrLn s |> ignore return s} let result2 = lazyz { yield iom5 yield iom5 yield iom5 () } let ioResult2 = result2.Force() let io6 = io{ let xs = Seq.map (fun x -> x) ioResult2 for x in xs do let! s = x() putStrLn s |> ignore return () } io6 () |> ignore Console.ReadLine () |> ignore
まとめ
・モナドはさまざまなプログラミングの問題に適用できるメタ戦略である。圏論からきてるらしいけど、ただ使うぶんには小難しい理論は割とどうでもいい。
・モナドを理解するには、ある程度プログラミングの素養が必要だが、とんでもなく難しいというわけではない。無意識に使っていることさえある。
・ある問題に対するモナドの適用可能性については、モナドをジェネリックな関数合成として捉えるとイメージしやすい。
・F#の立ち位置としてのIOモナドは、「副作用のある関数とそうでない関数を切り分けて管理したい」という要求に対してはある程度意味のあることだ。
・コンピューテーション式でモナドを表現することができる。さらにそれを柔軟なものとする仕組みを持っている。
・コンピューテーション式のCombineとRunを効果的に使えるようになると、利便性の高いモナドあるいはDSELを作れるようになる。要チェックや。
お疲れさまでした。
簡単そうに見えてややこしく 困難そうに思えてたやすい そんなモナド そんなモナド探してる 探してる
2011-06-19 16:00:34 via web
太陽系より果てしなく コンビニより身近な そんなモナド そんなモナド 探してる 探している
2011-06-19 16:06:45 via web
追記:やばい。モナドって実は空気だった。
.@igetaさんにモナドの(>>=)はF#の(>>)というより(|>)に似てんじゃね?と言われてアハ体験を頂いた。つまりモナドは空気だったんだよ!(えっまじで?)F#の(>>)は(>>=)の方じゃなくてKleisli composition演算子(>=>)というやつに似てたんだ
2011-07-05 09:00:50 via web
わたしはF#において、前方パイプライン演算子 (|>)はいい意味で空気のような存在だと思ってます*6。
(* F# *) (>>) : ('a -> 'b) -> ('b -> 'c) -> ('a -> 'c) (|>) : 'a -> ('a -> 'b) -> 'b
-- Haskell (.) :: (b -> c) -> (a -> b) -> (a -> c) ($) :: (a -> b) -> a -> b (=<<) :: Monad m => (a -> m b) -> m a -> m b (>>=) :: Monad m => m a -> (a -> m b) -> m b (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c) (<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
なるほど。($)演算子に似ているのは(=<<)の方だったかー。そして、(>>=)はF#の(|>)に似ているという事実。これつまり、モナドって空気だったんだ!(えっまじで?)みたいなアハ体験。そもそも、MonadはApplicative(適用可能)だったやん!ということに気づかされたのでありました。「ジェネリックな関数合成」というよりも「ジェネリックな関数適用」の方がより適切な表現でしたね。まぁイメージとしては大きくははずしていないと思うし、説明するための用語的には「関数合成」でもそんなにわるくはないとは思います。でも、これからは「関数適用」に改めたいと思います。本文中はそんな感じで脳内補完しつつ読みかえていただけると良いかなと思います。
いげ太先生ありがとう!
F#で楽々breakとcontinue。継続モナドまじパネぇっす!
id:einblickerさんが、「F#で継続モナド - einblickerの日記」というステキな記事を書いてくださいました。グッジョブすぎる!
以前、F#で継続渡し形式(CPS)変換を抽象的に考えてみたら、それってつまりHaskellの継続モナドみたいなものでした。ということで継続ワークフロー(簡易版)作った。という記事を書いたのですが、
当時の私には継続モナドはとても難しく、モナドで包む部分とcallCCについて華麗にスルーしていました。
今回、einblickerさんのコードを読んで、継続モナドについて少し理解が深まりました。相変わらずとても難しいんですけど。
で、コードを読んでいて少し気づいたことがあったので、
einblickerさんのコードを踏まえつつ、自分ならこんな風に書くかなーというのを書いてみました。が、間違えていました。
コメントにてeinblickerさんにご指摘いただいたとおりに修正しました。また、YieldとYieldFromの実装を追加しました。
どうもありがとうございます。もう一度見直してみます。
ContMonad.fs
namespace Monad.ContMonad [<AutoOpen>] module ContMonad = type Cont<'r, 'a> = Cont of (('a -> 'r) -> 'r) let runCont (Cont c) = c let callCC f = Cont <| fun k -> runCont (f (fun a -> Cont <| fun _ -> k a)) k let creturn a = Cont <| fun k -> k a type ContBuilder () = member this.Return(a) = creturn a member this.ReturnFrom(a) = a member this.Bind(Cont c, f) = Cont <| fun k -> c (fun a -> runCont (f a) k) member this.Zero() = this.Return() member this.Combine(c1, c2) = this.Bind(c1, fun _ -> c2) member this.For(seq, f) = Seq.fold (fun cc elem -> this.Combine(cc, f elem)) (f <| Seq.head seq) <| Seq.skip 1 seq member this.Delay (f) = f () member this.Yield (a) = creturn a member this.YieldFrom (a) = a let cont = new ContBuilder () type ContBuilder with member this.foreach seq f = cont { do! callCC <| fun break' -> cont { for i in seq do do! callCC <| fun continue' -> cont { do! f i (break'()) (continue'()) } } } |> runCont <| ignore
Sample.fs
namespace ConsoleApplication1 module Sample = open Monad.ContMonad cont.foreach [1..20] (fun i break' continue' -> cont { if i = 18 then do! break' printfn "foo" else if i % 2 = 0 then do! continue' printfn "bar" else printfn "%d" i }) System.Console.WriteLine () |> ignore cont.foreach [1..20] (fun i break' continue' -> cont { if i = 18 then do! break' printfn "foo" else for x in 1..i do printf "%d" x printfn "" }) System.Console.ReadLine () |> ignore
実行結果
1 3 5 7 9 11 13 15 17 1 12 123 1234 12345 123456 1234567 12345678 123456789 12345678910 1234567891011 123456789101112 12345678910111213 1234567891011121314 123456789101112131415 12345678910111213141516 1234567891011121314151617
F#で楽々breakとcontinueできちゃってるよ。継続モナドまじパネぇっす!
(よいこは、「C#ならふつうにbreakとcontinueできるじゃん」とかなんとか言わない。)
ちなみに、 break と continue の2つのキーワードは将来利用するために予約されているので、
F#の今後のバージョンでサポートされるかもしれません。
おまけ:モナド則の確認
namespace ContMonad.UT open System module Tests = open NUnit.Framework open FsUnit open Monad.ContMonad [<TestFixture>] type ``ContMonad モナド則`` () = let (>>=) m f = cont {let! x = m return! f x} let return' x = cont { return x } let x = 1 let m = cont { return 3 } let f x = cont { return 4 + x } let g x = cont { return 2 * x } let assertEqual (left, right) = let result = cont {let! a1 = left let! a2 = right return a1 |> should equal a2} |> runCont <| ignore () let (==) left right = assertEqual (left, right) [<Test>] // モナド則1: return x >>= f == f x member test.``モナド則1`` () = return' x >>= f == f x [<Test>] // モナド則2: m >>= return == m member test.``モナド則2`` () = m >>= return' == m [<Test>] // モナド則3: (m >>= f) >>= g == m >>= (\x -> f x >>= g) member test.``モナド則3`` () = (m >>= f) >>= g == (m >>= (fun x -> f x >>= g)) // nunit-gui-runner let main () = NUnit.Gui.AppEntry.Main([|System.Windows.Forms.Application.ExecutablePath|]) |> ignore main ()
Observableコンピューテーション式はモナド則を満たしているか否か。
前回のエントリ「F#でRxる。よく訓練されたF#erはコンピューテーション式をつくる。」で紹介いたしました、
Observableコンピューテーション式について補足します。モナド則を満たしているか否かについてです。
Haskellにおいて、モナドがモナド則を満たしているとき、
1. (return x) >>= f == f x
2. m >>= return == m
3. (m >>= f) >>= g == m >>= (\x -> f x >>= g)
以上の三つの条件に満足しています。
モナドのすべてによると、最初の規則は return が >>=*1 に関して左単位元に なっていることを要請していて、
二番目の規則は return が >>= に関して右単位元になっていることを要請してる。 最後の規則は >>= に関する一種の結合法則とのことです。
これを初めて読んだときはさっぱりわかりませんでした。今も確信を持ってわかったとは言いきれませんが、まぁ、なんとなく雰囲気はつかんでいるつもりです。
Observableのコンピューテーション式では、どうでしょうか。
namespace FSharp.Rx.UT open System module Tests = open System.Reactive open NUnit.Framework open FsUnit open FSharp open FSharp.Rx [<TestFixture>] type ``monad soku `` () = [<Test>] // モナド則1: return x >>= f == f x // return された値を保持するための変数(というか継続)は、なくしてしまうことができますよの規則 // モナドで計算を繋ぐにしろ、もともと繋がっている計算をモナドでくるんでも一緒だね member test.``monad soku1`` () = observable { let! a1 = observable{ let! a = observable {return fun x -> x * 2} let! b = observable {return 3} return a b} let! a2 = observable{ return 3 |> fun x -> x * 2 } return a1 |> should equal a2 } |> Rx.subscribe(fun _ -> ()) |> ignore [<Test>] // モナド則2: m >>= return == m // bindしてreturnしても結局同じことだよねの規則 // あるいは、結局最後の式が return されることになるので、明示的な return は意味ナッシンというお話 member test.``monad soku2`` () = let m1 = observable{ let! a = observable {return "F#"} return a} let m2 = observable{ return "F#" } Reactive.Testing.Extensions.AssertEqual(m1, m2) // モナド則3: m >>= (\x -> f x >>= g) == (m >>= f) >>= g // 多段ネストしたモナドでも(どんなに深くても)、結局のところ一段のモナドと等価ですよの規則 [<Test>] member test.``monad soku3`` () = let m1 = observable{ let! a = observable {return 3} let! b = observable {return 4} let! x = observable {let! d = observable{return a} let! e = observable{return b} return d + e} let! c = observable {return fun x -> x * 2} return c x} let m2 = observable{ return 3 + 4 |> fun x -> x * 2 } Reactive.Testing.Extensions.AssertEqual(m1, m2) // nunit-gui-runner let main () = NUnit.Gui.AppEntry.Main([|System.Windows.Forms.Application.ExecutablePath|]) |> ignore main ()
ごくごく単純な確認ですので、正確な証明とまではいきませんが、
Observableコンピューテーション式が、モナド則を満たしているであろうことをゆるく確認することができます。
てゆうか、Observableコンピューテーション式は、単純にRxのObservableをはめ込んで適用しただけの代物なので、
Rxがそもそもモナド則を満たしているというのが本当のところです。RxのObservableはまさにモナドなのです。
[追記]
@nobuhisa_kさんに、コメントにてナイスつっこみを頂きました!
おっしゃるとおりで、上記のモナド則3の確認は間違えていますね。
モナド則3は、誤解を恐れずイメージしやすいように大雑把に言うと、「(2 * 3) * 4 * 5 == 2 * (3 * 4) * 5」だよねみたいな。
モナドにおいてもこれと同じことが成立していて欲しいと。していなきゃ困ると。
ということで、丸パクリで書き直してみました。
お手軽にお試しできる版
let x = 1 let m = observable { return 3 } let f x = observable { return 4 + x } let g x = observable { return 2 * x } let (>>=) m f = observable {let! x = m return! f x} let return' x = observable { return x } let prove (left,right) = observable {let! x = (left:IObservable<'a>) let! y = (right:IObservable<'a>) return x = y} prove (return' x >>= f, f x) |> Rx.subscribe(fun b -> printfn "モナド則1 : %b" b) |> ignore // true prove (m >>= return', m) |> Rx.subscribe (fun b -> printfn "モナド則2 : %b" b) |> ignore // true prove ((m >>= f) >>= g, m >>= (fun x -> f x >>= g)) |> Rx.subscribe(fun b -> printfn "モナド則3 : %b" b) |> ignore // true
よりテストっぽく
namespace FSharp.Rx.UT open System module Tests = open System.Reactive open NUnit.Framework open FsUnit open FSharp open FSharp.Rx [<TestFixture>] type ``Observable モナド則`` () = let (>>=) m f = observable {let! x = m return! f x} let return' x = observable { return x } let x = 1 let m = observable { return 3 } let f x = observable { return 4 + x } let g x = observable { return 2 * x } let assertEqual (left, right) = Reactive.Testing.Extensions.AssertEqual((left:IObservable<'a>), right) let (==) left right = assertEqual (left, right) [<Test>] // モナド則1: return x >>= f == f x member test.``モナド則1`` () = return' x >>= f == f x [<Test>] // モナド則2: m >>= return == m member test.``モナド則2`` () = m >>= return' == m [<Test>] // モナド則3: (m >>= f) >>= g == m >>= (\x -> f x >>= g) member test.``モナド則3`` () = (m >>= f) >>= g == (m >>= (fun x -> f x >>= g)) // nunit-gui-runner let main () = NUnit.Gui.AppEntry.Main([|System.Windows.Forms.Application.ExecutablePath|]) |> ignore main ()
わかりやすい。これならHaskellな人にも怒られなさそうですw
コンピューテーション式(Computation Expressions)をつくったら、
こんな風にモナド則を満たしているかどうか確認するとよいですね(実際はもっと抽象化した方がいい)。
これでモナド則ももうこわくないでござる。
あわせて読みたい
モナド則を満たすべき理由 - HaHaHa!(old)
http://haskell.g.hatena.ne.jp/nobsun/20080928/p1
これは参考になる
*1:bind