Optionに見るコンピュテーション式のつくり方
またの名を入門コンピュテーション式(嘘
この記事は、「コンピュテーション式ってどうやって作ればいいの?」に対する自分なりの回答です。
matchのネスト
option
を返す3つの関数f
, g
, h
があったとします。
で、このような処理がしたいとしましょう。
let niceFunction (arg1, arg2, arg3) = match f arg1 with | Some x -> match g arg2 with | Some y -> match h arg3 with | Some z -> Some (x, y, z) | None -> None | None -> None | None -> None
この関数は、3つの関数すべてが成功したときだけ、その結果をまとめて成功として返しています。 それ以外は何もせずに失敗として返しています。
このように、「すべて成功したときだけ計算したい」という状況はよく起こります。
例えば、f
がDBから何か取得する関数、g
がファイルシステムからファイルを取得する関数、h
がネットワークから何か取得する関数だとして、
これらすべてが成功したらそれらの情報を使って何か処理がしたい、というケースが考えられます。
これを毎回書くのはだるいですし、計算のもとになるソースが増えれば増えるほど、match
がネストしていきます。
どうにかできないでしょうか?
コードの「形」の共通部分
ここで注目してほしいのは、このコードの構造が再帰構造になっている点です。
match <expr> with | Some <v> -> +----------------+ | 全体と似た構造 | +----------------+ | None -> None
このように、Some
の場合の処理に、全体の構造に似た形が再び現れることが分かります。
まずは、この部分をカスタマイズできるように関数の引数として渡せるようにしてみましょう。
コードの再帰構造の関数化
<expr>
の部分と、Some
の場合に行う処理を引数に取ればよさそうです。
また、Some
の場合に行う処理では、Some
が持っている値も必要になるため、関数の引数として渡すことにします。
let matchSome target procForSome = match target with | Some v -> procForSome v | None -> None
この関数は、procForSome
に渡す関数の中で再びmatchSome
関数が呼び出されることを想定しています。
これを使うと、最初のコードはこう書けます。
let niceFunction (arg1, arg2, arg3) = matchSome (f arg1) (fun x -> matchSome (g arg2) (fun y -> matchSome (h arg3) (fun z -> Some (x, y, z) )))
無名関数のネストはありますが、疑似的にフラットに出来ました。
g
とh
の間に何か挟まってきても、
let niceFunction (arg1, arg2, arg3) = matchSome (f arg1) (fun x -> matchSome (g arg2) (fun y -> matchSome (hoge (arg1, arg2, arg3)) (fun w -> matchSome (h arg3) (fun z -> Some (x, y, w, z) ))))
なんとか対処できます。 ただ、末尾の閉じカッコはどんどん増えていきます。 どうにかならないでしょうか・・・
無名関数によるletの除去
さて、ちょっと話題を変えて、let
を除去する方法を考えてみましょう。
let x = 2 let y = "aaa" let z = [0..x] printfn "%A" (x, y, z)
F#で変数を導入したい場合に真っ先に思い浮かぶのがlet
です。
しかし、他にも変数が導入できるものがあります。
関数の引数です*1。
let x = 42 printfn "%A" x
このコードを無名関数を使って書き直すと、
(fun x -> printfn "%A" x) 42
となります。 これを参考に、最初のコードを書き替えてみます。
(fun x -> (fun y -> (fun z -> printfn "%A" (x, y, z)) [0..x]) "aaa") 2
これでは読めないので、|>
を使ってさらに変形します。
2 |> (fun x -> "aaa" |> (fun y -> [0..x] |> (fun z -> printfn "%A" (x, y, z))))
2
をx
に入れ、"aaa"
をy
に入れ、[0..x]
をz
に入れ、本体を実行しているように見えませんか?
コンピュテーション式の導入
さて、話を元に戻しましょう。
let niceFunction (arg1, arg2, arg3) = matchSome (f arg1) (fun x -> matchSome (g arg2) (fun y -> matchSome (hoge (arg1, arg2, arg3)) (fun w -> matchSome (h arg3) (fun z -> Some (x, y, w, z) ))))
このコードの末尾部分のカッコをどうにかしたいのでした。
そして、let
は無名関数で除去できる、ということを見ました。
では、無名関数をlet
で除去できないでしょうか・・・?
これは残念ながらできません。
ですが、コンピュテーション式を使えばlet!
という構文を使うことで可能になります。
let!
は、簡単にはコンピュテーションビルダーのBind
メソッド呼び出しに変形されます。
builder { let! v = expr ... }
このコードは、
builder.Bind(expr, (fun v -> ...))
このように変形されます。
ここで、matchSome
関数を思い出してください。
let matchSome target procForSome = match target with | Some v -> procForSome v | None -> None
この関数、Bind
が要求する形に似ていますね。
実は、matchSome
関数は、ほとんどそのままBind
として使えます。
では、コンピュテーションビルダーを定義してみましょう。
type OptionBuilder() = member __.Bind(x, f) = matchSome x f member __.Return(x) = Some x let option = OptionBuilder()
これを使えば、元のコード
let niceFunction (arg1, arg2, arg3) = matchSome (f arg1) (fun x -> matchSome (g arg2) (fun y -> matchSome (h arg3) (fun z -> Some (x, y, z) )))
は、こう書き直せます。
let niceFunction (arg1, arg2, arg3) = option { let! x = f arg1 let! y = g arg2 let! z = h arg3 return (x, y, z) }
フラットになりました!
このように、コンピュテーション式はある種のネスト構造をフラットに(読みやすく、かつ編集しやすく)書けるようにする機能を持ちます*2。
コンピュテーション式を作るには
自分でコンピュテーション式を作ろうとする場合、その「同じような構造がネストしている」ことを発見できねばなりません。 というか順番が逆で、「同じような構造がネストしている」のがだるいからコンピュテーション式でフラットにするのであって、コンピュテーション式が作りたいからそのような構造を見つけるのではないです。
で、先人はいくつも「同じような構造がネストしている」パターンを見つけてくれており、それぞれに名前まで付けてくれています。
例えば、上で例にした'a option
を対象にしたものは、MaybeモナドやOptionモナドとして広く知られています。
コンピュテーション式を作れるようになるための近道、それは色々なモナドを理解し、そのコンピュテーション式を実際に作ってみることです。
余談
モナドさえ理解できればコンピュテーション式は作れるようになるか、というとそういうわけではありません。 コンピュテーション式はモナド以上のことができてしまうため、モナドだけわかってもすべての機能の実装はできません(し、間違った実装を提供してしまいます)。
また、全然モナドじゃないコンピュテーション式も作れます。 作れますが、それが実用的になることはそうそうないでしょう。 コンピュテーション式の基本、それはモナドです。