この投稿は、シリーズの2番目です。 前回の投稿では、通常の世界から高次の世界へ値を持ち上げるためのコア関数について説明しました。
今回は、「世界をまたぐ」関数と、bind
関数を使ってそれらを制御する方法を見ていきます。
シリーズの内容
このシリーズで言及する様々な関数へのショートカットリストです。
- パート1:高次の世界への持ち上げ
- パート2:世界をまたぐ関数の合成方法
- パート3:コア関数の実際的な使い方
- パート4:リストと高次の値の混合
- パート5:すべてのテクニックを使用する実世界の例
- パート6:独自の高次の世界を設計する
- パート7:まとめ
パート2:世界をまたぐ関数の合成方法
bind
関数
一般的な名前:bind
、flatMap
、andThen
、collect
、SelectMany
一般的な演算子:>>=
(左から右)、=<<
(右から左)
機能:世界をまたぐ(「モナディックな」)関数の合成
シグネチャ:(a->E<b>) -> E<a> -> E<b>
、または引数を逆にして:E<a> -> (a->E<b>) -> E<b>
説明
通常の世界と高次の世界を行き来する関数をよく扱います。
たとえば、string
をint
に解析する関数が通常のint
ではなくOption<int>
を返したり、
ファイルから行を読み込む関数がIEnumerable<string>
を返したり、
Webページを取得する関数がAsync<string>
を返したりします。
このような「世界をまたぐ」関数は、a -> E<b>
というシグネチャで認識できます。入力は通常の世界にありますが、出力は高次の世界にあります。
残念ながら、このタイプの関数は標準的な合成では連結できません。
「bind」の機能は、世界をまたぐ関数(一般に「モナディック関数」と呼ばれる)を持ち上げられた関数E<a> -> E<b>
に変換することです。
これを行う利点は、結果として得られる持ち上げられた関数が純粋に高次の世界に存在し、簡単に合成できることです。
たとえば、a -> E<b>
型の関数はb -> E<c>
型の関数と直接合成できませんが、
bind
を使用すると、2番目の関数はE<b> -> E<c>
型になり、合成可能になります。
このように、bind
を使えば任意の数のモナディック関数を連鎖できます。
別の解釈
bind
の別の捉え方として、高次の値(E<a>
)とモナディック関数(a -> E<b>
)という2つの引数を取り、
入力の中身を「アンラップ」してa -> E<b>
関数を実行することで新しい高次の値(E<b>
)を生成する関数と考えられます。
この「アンラップ」という比喩はすべての高次の世界に当てはまるわけではありませんが、こう考えると役立つことが多いです。
実装例
F#で2つの型に対してbind
を定義した例を見てみましょう。
module Option =
// オプション用のbind関数
let bind f xOpt =
match xOpt with
| Some x -> f x
| _ -> None
// 型:('a -> 'b option) -> 'a option -> 'b option
module List =
// リスト用のbind関数
let bindList (f: 'a->'b list) (xList: 'a list) =
[ for x in xList do
for y in f x do
yield y ]
// 型:('a -> 'b list) -> 'a list -> 'b list
注意:
- この2つの場合、F#ではすでに
Option.bind
とList.collect
として関数が存在します。 List.bind
では再びfor..in..do
を使っていますが、この実装がリストでのbindの動きを明確に示しています。 純粋な再帰的実装もありますが、ここでは省略します。
使用例
冒頭で述べたように、bind
は世界をまたぐ関数を合成するのに使えます。
シンプルな例でその動きを見てみましょう。
まず、特定のstring
をint
に解析する関数があるとします。とてもシンプルな実装です。
let parseInt str =
match str with
| "-1" -> Some -1
| "0" -> Some 0
| "1" -> Some 1
| "2" -> Some 2
// 以下同様
| _ -> None
// シグネチャはstring -> int option
時には整数を返し、時には返しません。そのため、シグネチャはstring -> int option
となり、世界をまたぐ関数です。
そして、int
を入力として受け取り、OrderQty
型を返す別の関数があるとします。
type OrderQty = OrderQty of int
let toOrderQty qty =
if qty >= 1 then
Some (OrderQty qty)
else
// 正の数のみ許可
None
// シグネチャはint -> OrderQty option
これも、入力が正でない場合はOrderQty
を返さないかもしれません。したがって、シグネチャはint -> OrderQty option
となり、これも世界をまたぐ関数です。
では、文字列を直接OrderQty
に変換する関数をどのように作れば良いでしょうか。
parseInt
の出力を直接toOrderQty
に渡せないので、ここでbind
が役立ちます。
Option.bind toOrderQty
を実行すると、int option -> OrderQty option
関数に持ち上げられ、parseInt
の出力を入力として使えます。
let parseOrderQty str =
parseInt str
|> Option.bind toOrderQty
// シグネチャはstring -> OrderQty option
新しいparseOrderQty
関数のシグネチャはstring -> OrderQty option
となり、これもまた世界をまたぐ関数です。
そのため、出力のOrderQty
を使って何かをする場合は、チェーンの次の関数でもbind
を使う必要があるかもしれません。
中置演算子版のbind
apply
と同様に、名前付きのbind
関数は扱いづらいことがあります。そのため、中置演算子版を作るのが一般的です。
通常、左から右へのデータの流れには>>=
、右から左への流れには=<<
を使います。
これを使えば、parseOrderQty
の別バージョンを次のように書けます。
let parseOrderQty_alt str =
str |> parseInt >>= toOrderQty
ご覧の通り、>>=
はパイプ演算子(|>
)と同じような役割を果たしますが、「高次の」値を世界をまたぐ関数にパイプするのに使います。
「プログラム可能なセミコロン」としてのbind
bindは任意の数の関数や式を連鎖させるのに使えるので、次のようなコードがよく見られます。
expression1 >>=
expression2 >>=
expression3 >>=
expression4
これは、命令型プログラムで>>=
をセミコロン(;
)に置き換えたものとさほど変わりません。
statement1;
statement2;
statement3;
statement4;
このため、bind
は「プログラム可能なセミコロン」と呼ばれることがあります。
bindとreturnの言語サポート
ほとんどの関数型プログラミング言語には、bind
のための何らかの構文サポートがあり、一連の継続を書いたり、明示的にbindを使ったりしなくて済むようになっています。
F#では、これはコンピュテーション式の(一つの)要素です。次のような明示的なbindの連鎖は次のように書けます。
initialExpression >>= (fun x ->
expressionUsingX >>= (fun y ->
expressionUsingY >>= (fun z ->
x+y+z ))) // return
これをlet!
構文を使って暗黙的に表現できます。
elevated {
let! x = initialExpression
let! y = expressionUsingX x
let! z = expressionUsingY y
return x+y+z }
Haskellでは、同等のものは「do記法」と呼ばれます。
do
x <- initialExpression
y <- expressionUsingX x
z <- expressionUsingY y
return x+y+z
Scalaでは、同等のものは「for内包表記」と呼ばれます。
for {
x <- initialExpression
y <- expressionUsingX(x)
z <- expressionUsingY(y)
} yield {
x+y+z
}
bind/returnを使う際に特別な構文を使う必要はないことを強調しておくのは重要です。他の関数と同じように、bind
や>>=
を常に使えます。
Bind vs. Apply vs. Map
bind
とreturn
の組み合わせは、apply
とreturn
よりもさらに強力だと考えられています。
なぜなら、bind
とreturn
があればmap
とapply
を構築できますが、その逆はできないからです。
たとえば、bindを使ってmapをエミュレートする方法を見てみましょう。
- まず、通常の関数から世界をまたぐ関数を構築します。出力に
return
を適用することでこれを行います。 - 次に、この世界をまたぐ関数を
bind
を使って持ち上げられた関数に変換します。これにより、単にmap
を行った場合と同じ結果が得られます。
同様に、bind
はapply
をエミュレートできます。以下は、F#でOptionに対するmap
とapply
をbind
とreturn
(Some)を使って定義する方法です。
// bindとreturn (Some)を使ってmapを定義
let map f =
Option.bind (f >> Some)
// bindとreturn (Some)を使ってapplyを定義
let apply fOpt xOpt =
fOpt |> Option.bind (fun f ->
let map = Option.bind (f >> Some)
map xOpt)
この時点で、人々はしばしば「bind
がより強力なのに、なぜapply
を使うべきなのか」と疑問に思います。
答えは、apply
がbind
でエミュレートできるからといって、そうすべきだというわけではないということです。
たとえば、bind
の実装ではエミュレートできない方法でapply
を実装することも可能です。
実際、apply
(「アプリカティブスタイル」)やbind
(「モナディックスタイル」)を使うことで、プログラムの動作に大きな影響を与える可能性があります。
これら2つのアプローチの詳細については、このポストのパート3で説明します。
正しいbind/return実装の特性
map
の場合と同様に、またapply
/return
の場合と同様に、
正しいbind
/return
の実装には、どの高次の世界で作業していても真となるべき特性がいくつかあります。
いわゆる3つの「モナド則」があります。
(プログラミングの観点での)モナドを定義する一つの方法は、ジェネリック型コンストラクタE<T>
とモナド則に従う関数のペア(bind
とreturn
)から成るものと言うことです。
モナドを定義する方法はこれだけではありません。数学者は通常、少し異なる定義を使います。
しかし、ジェネリック型コンストラクタと2つの関数によるこの定義が、プログラマにとって最も役立ちます。
これまでに見たファンクターとアプリカティブの法則と同様に、これらの法則はかなり理にかなっています。
まず、return
関数自体が世界をまたぐ関数であることに注目してください。
これは、bind
を使ってそれを高次の世界の関数に持ち上げられることを意味します。そして、この持ち上げられた関数は何をするのでしょうか。うまくいけば、何もしません!
単に入力を返すだけです。
そして、これがまさに最初のモナド則です。この持ち上げられた関数は、高次の世界でのid
関数と同じでなければならないと言っています。
2番目の法則は似ていますが、bind
とreturn
が逆になっています。通常の値a
と、a
をE<b>
に変換する世界をまたぐ関数f
があるとします。
f
にはbind
を、a
にはreturn
を使って、両方を高次の世界に持ち上げましょう。
ここで、f
の高次バージョンをa
の高次バージョンに適用すると、ある値E<b>
が得られます。
一方、f
の通常バージョンをa
の通常バージョンに適用しても、ある値E<b>
が得られます。
2番目のモナド則は、これら2つの高次の値(E<b>
)が同じであるべきだと言っています。言い換えれば、これらの bind
と return
の適用はデータを歪めるべきではありません。
3番目のモナド則は結合法則に関するものです。
通常の世界では、関数合成は結合法則を満たします。
たとえば、値を関数f
にパイプし、その結果を関数g
にパイプすることができます。
あるいは、最初にf
とg
を合成して単一の関数にしてから、a
をそれにパイプすることもできます。
let groupFromTheLeft = (a |> f) |> g
let groupFromTheRight = a |> (f >> g)
通常の世界では、これらの代替案が同じ答えを与えることを期待します。
3番目のモナド則は、bind
とreturn
を使用した後でも、グループ化は問題にならないと言っています。以下の2つの例は、上記の例に対応します。
let groupFromTheLeft = (a >>= f) >>= g
let groupFromTheRight = a >>= (fun x -> f x >>= g)
そして再び、これらの両方が同じ答えを与えることを期待します。
リストはモナドではない。オプションもモナドではない。
モナドは3つの要素から成ります。ジェネリック型(別名「型コンストラクタ」)、2つの関数、そして満たすべき一連の特性です。
したがって、List
データ型はモナドの1つの構成要素にすぎず、Option
データ型も同様です。List
とOption
は、それ自体ではモナドではありません。
モナドを変換として考えるのがより適切かもしれません。 「リストモナド」は通常の世界を高次の「リスト世界」に変換するものであり、「オプションモナド」は通常の世界を高次の「オプション世界」に変換するものです。
ここに多くの混乱の源があると思います。「リスト」という言葉には多くの異なる意味があります。
List<int>
のような具体的な型またはデータ構造。- 型コンストラクタ(ジェネリック型):
List<T>
。 List
クラスやモジュールのような型コンストラクタと何らかの操作。- 型コンストラクタと何らかの操作、そしてそれらの操作がモナド則を満たすもの。
モナドなのは最後のものだけです!他の意味も有効ですが、混乱の原因となります。
また、最後の2つのケースはコードを見ただけでは区別がつきにくいです。残念ながら、モナド則を満たしていない実装も存在します。 「モナド」と呼ばれていても、必ずしも真のモナドではない場合があるのです。
個人的に、このサイトでは「モナド」という言葉の使用を避けています。代わりに、bind
関数に焦点を当てています。
抽象的な概念ではなく、問題解決のためのツールキットの一部として扱うためです。
そのため、「これはモナドですか?」とは尋ねないでください。
代わりに、次のように尋ねるべきです。使えるbind
とreturn
関数がありますか?そして、それらは正しく実装されていますか?
まとめ
これでコア関数が4つ揃いました。map
、return
、apply
、そしてbind
です。これらの機能がそれぞれ明確になったことを願っています。
しかし、まだ答えていない質問もあります。たとえば次のような疑問です。
「なぜapply
の代わりにbind
を選ぶべきなのか?」「複数の高次の世界を同時に扱うにはどうすればよいのか?」
次の投稿では、これらの疑問に答え、一連の実践的な例を通じてこのツールセットの使い方を示します。
更新:@joseanpgに指摘されたモナド則の誤りを修正しました。ありがとうございます!