購読中です 読者をやめる 読者になる 読者になる
ようこそ。睡眠不足なプログラマのチラ裏です。

快刀乱麻を断つモナド - 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ほど美しくモナドとその周辺事情を表現することはできません。HaskellMonadは型クラスによって非常に洗練された表現をされているので、モナドを勉強する上でとても参考になります。なにかを学ぶ場合、抽象的な話だけではわかりにくいので、具体的な例で考えてみると理解の助けとなります。Haskellの知識が少し必要ですが、id:kazu-yamamotoさんの「QAで学ぶMonad - あどけない話」の説明がすばらしくわかりやすいです。




モナドは難しい」、「モナドは象だ」、「モナドかわいいよモナド」、「このモナドの説明を書いた人はこんなモナドの説明を読んでいます」、「モナドはメタファーではない」、「モナにゃんぺろぺろ」、「圏論モナドこそ至高」など、実にさまざまな声を聞きます。Real World Haskellオライリー)には、「モナドを使っていると苛々します」という皮肉めいた記述が現れたりもします。




憧れの(闇の)カンスウガタゲンガー達が語る「モナドの本当の意味」を理解しようとすると、やはり圏論の概念やら定理やらの正確な理解が必要なのかもしれず。でも、その域に達するには、おそらく私を含めた一般的なプログラマには険しくも困難な道。出口の見えない試練に身を投じることになります。
しかし、多くのカンスウガタゲンガーは、圏論の本質的な意味やその定理に強いわけではなく。それにもかかわらず、彼らはモナドを使ったプログラムを書きます。小難しい概念を正確に理解することはできなくても、モナドを利用してメリットを享受することはできるんです。そしてモナドはとても便利なんです。実際のところ「モナド*1」さえわかっていればほとんどの場合は困りません。使っているうちにモナドの性質が徐々にわかってきて、いずれ立派なモナグラマ−になれるかもしれない。もしかすると、みんなに愛でられるような可愛いオレオレモナドを発明できるようになるかもしれない。

C++モナドに置き換えて音読してみましょう。そんな感じです。






単に関数プログラミングにおけるモナドの実用性あるいは利便性という意味だけで言うと、モナド則(モナドが守るべき3つのルール)を満たす基本演算の定義をすれば、あらゆる操作を同じ枠組みのもとでジェネリックに連鎖できる仕組みを利用することができる、ということに他ならない。モナドがプログラミング上で表現している抽象は、ジェネリックな関数合成と言いかえることもできる。関数合成とモナドの合成の違いを考えると、モナドジェネリックな合成であり、異なる型の値を持つことができる点で異なる。これはそれぞれのシグネチャを比較するとわかりやすい。


Haskell:関数合成の演算子

(.) :: (b -> c) -> (a -> b) -> (a -> c)


Haskell:Bind演算子

(>>=) :: 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は、なんらかの値を新たなモナド型の値にする関数で、しばしばモナドに包むなんて表現されたりもする。


Haskell

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恐ろしい人気です。そちらのキャッチフレーズ(?)より以下を引用させていただきます。

スタートHaskell :: [初心者] → IO [経験者] -- この夏、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 ()

    /// MaybeTMonadIOのインスタンスだとかなんとか
    /// IO a をMaybeT IO aに持ち上げます
    let liftIO (m:IO<_>) = maybeT {return m}

    /// MaybeTMonadのインスタンスだとかなんとか
    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#で遅延評価をするには、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を作れるようになる。要チェックや。



お疲れさまでした。


追記:やばい。モナドって実は空気だった。

わたしは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(適用可能)だったやん!ということに気づかされたのでありました。「ジェネリックな関数合成」というよりも「ジェネリックな関数適用」の方がより適切な表現でしたね。まぁイメージとしては大きくははずしていないと思うし、説明するための用語的には「関数合成」でもそんなにわるくはないとは思います。でも、これからは「関数適用」に改めたいと思います。本文中はそんな感じで脳内補完しつつ読みかえていただけると良いかなと思います。


いげ太先生ありがとう!

*1:モナドが満たすべき3つの法則

*2:数値や文字列のような基本的な値と同じように扱うことができる値

*3:値が実際に必要となるまで式の評価を遅らせる

*4:どうしてもすべての副作用をIOモナドの中に封じ込めたい!と言うのであれば、Haskellのような純粋関数型言語の利用をご検討ください。

*5:ただし、ほとんどの場合はモナドにしておいた方が便利で、きっと幸せになります。

*6:パイプライン演算子が好きすぎてC#で生きるのがつらい