matarillo.com

The best days are ahead of us.

FAIC モナド、パート8

2018-01-19 17:00:18

原文

前回のFAICでは、やっとのことでモナドパターンのルールについて言及を終えた; もちろんその前から知っていたことはあって、それはパターンの主要な部分は、CreateSimpleM<T> という名前の生成ヘルパーと、ApplySpecialFunction<A, R> という名前の関数適用ヘルパーだということだ。うすうす分かっていると思うが、これらの関数の名前は通例的なものではない。1

生成関数の伝統的な名前は「unit」だ。これはなんとなく意味が分かるだろう。Haskell(モナドを多く利用する純粋関数型プログラミング言語)では unit 関数は return という名前になっている。この理由を説明するにはHaskellにおける背景から話さないといけなくて、少々厄介だ。2 C#では特に慣例的な名前はない。ここまで例としてみてきた5つのモナド型では、1つの単純な値からモナドのインスタンスを生成するためにそれぞれ別の5通りの方法がある:

Nullable<int> nullable = new Nullable<int>(123);
Task<int> task = Task.FromResult<int>(123);
Lazy<int> lazy = new Lazy<int>(() => 123);
OnDemand<int> onDemand = () => 123;
IEnumerable<int> sequence = Enumerable.Repeat<int>(123, 1);

ぶっちゃけ、最後のはちょっとアレだ。1要素のシーケンスを作成する専用のスタティックメソッドが Enumerable にあればよかったのに。

「関数適用」ヘルパーの伝統的な名前は「bind」だ。実はHaskellでは bind 関数は中置3演算子だ; Haskellで、関数 f をモナドのインスタンス m に適用させるときは、m >>= f と書く。C#では bind 関数は明示的に提供されていないことが多く、たいてい名前も持っていない。4

「unit」はなんとなく意味が分かるけど「bind」にはどんな意味があるっていうんだ?それにあの意味不明な Haskell の文法は何だ?

すでに気づいているかもしれないが、非同期モナド、遅延初期化モナド、シーケンスモナドには共通するおもしろい性質がある: これらのモナドに関数を適用すると、結果として得られるものは その関数を後から実行するオブジェクト なのだ。本質的には、 bind 関数は変更不可のワークフローとその次のステップを受け取って、その結果として新しいワークフローを返す のだ。したがって m >>= f は「ワークフロー m の末尾に処理 f を結びつけて(バインドして)新しいワークフローを返せ」という意味だ。Haskellのこの文法は実はとても適切なのだ; ワークフローが自身の結果を次の関数に送っているような感じがする。

このことを掘り下げてみよう: bind 処理は1つの ワークフロー と1つの 関数 を受け取って新しいワークフローを返すが、そのワークフローは実行されたときに 元のワークフローの結果を新しい関数に送る。 bind 演算子自体はワークフローを 実行 していない; 元のワークフローを新しいワークフローに変えているだけだ5

これらのワークフローの実際の動作は個々のモナドの意味論によって変わることは言うまでもない。シーケンスモナドではワークフローの部分は MoveNext が呼ばれた時に起動されて、シーケンスの次の値が計算される前に実行される。遅延初期化モナドのワークフローは Value プロパティが最初にアクセスされた時に起動される; その後はキャッシュされた値が使われる。オンデマンドモナドのワークフローはデリゲートが呼び出されるたびに起動される。そして非同期モナドのワークフローは……そう、タスクの実行が予定された時に起動する!

これこそが、これらのモナドの重要なポイントなんだ: 4つはどれも、なされるべき作業の組とその順番を表している。一方、ずっと単純なヌル許容モナド6は後で実行するべきワークフローを表しているわけではなく、元の値と関連する特別な状態 – 1つの Boolean 値– を表している。ヌル許容モナドに関する計算は未来に延期されたりせずに「先行して」行われる。

次回のFAIC : 私はStackExchangeのポッドキャストの特別ゲストになっているので一旦横道にそれる。連載記事を再開するときには「状態」モナドのアドホックな例をいくつか考え出して、この概念についての考察を進める予定だ。


インデックスへ戻る


  1. もしこっちの名前が標準になってたんだとしたら、モナドについてもっと速く理解できてたと思うんだけどな。こっちの名前がみなさんの手助けになってるといいんだけど。 ↩︎

  2. Haskellの初学者は return が言語キーワードじゃなくて関数だと知って混乱することもある。 ↩︎

  3. 中置演算子とはオペランド(演算対象)の間に置かれる演算子のこと。 ↩︎

  4. この法則の最も重要な例外は、シーケンスモナドのbind処理は Enumerable.SelectMany メソッドだということだ。これについては後のエピソードで補足する予定。 ↩︎

  5. C#でも同じことで、前のクエリーから新しいクエリーを作ったからといって、どちらのクエリーも実行されない。これは偶然の一致なんかではない!C# 3でこの機能を設計した人の一人は、Haskellの設計者でもあった。その人はErik Meijerだ。 LINQのクエリーとは、実はモナドのバインドの構文糖だ。 さっきも書いたが、後のエピソードで補足する予定だ。 ↩︎

  6. ちなみに、これはほかの言語では伝統的に「Maybeモナド」と呼ばれている。それは、値がある「かもしれない(maybe)」し、ないかもしれないからだ。 ↩︎