この広告は、90日以上更新していないブログに表示しています。
またの名を入門コンピュテーション式(嘘
この記事は、「コンピュテーション式ってどうやって作ればいいの?」に対する自分なりの回答です。
optionを返す3つの関数f,g,hがあったとします。で、このような処理がしたいとしましょう。
let niceFunction(arg1, arg2, arg3)=match f arg1with|Some x->match g arg2with|Some y->match h arg3with|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 targetwith|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 x=2let y="aaa"let z=[0..x]printfn"%A"(x, y, z)
F#で変数を導入したい場合に真っ先に思い浮かぶのがletです。しかし、他にも変数が導入できるものがあります。関数の引数です*1。
let x=42printfn"%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 targetwith|Some v-> procForSome v|None->None
この関数、Bindが要求する形に似ていますね。実は、matchSome関数は、ほとんどそのままBindとして使えます。
では、コンピュテーションビルダーを定義してみましょう。
typeOptionBuilder()= member __.Bind(x, f)= matchSome x f member __.Return(x)=Some xletoption=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 arg1let! y= g arg2let! z= h arg3 return(x, y, z)}
フラットになりました!
このように、コンピュテーション式はある種のネスト構造をフラットに(読みやすく、かつ編集しやすく)書けるようにする機能を持ちます*2。
自分でコンピュテーション式を作ろうとする場合、その「同じような構造がネストしている」ことを発見できねばなりません。というか順番が逆で、「同じような構造がネストしている」のがだるいからコンピュテーション式でフラットにするのであって、コンピュテーション式が作りたいからそのような構造を見つけるのではないです。
で、先人はいくつも「同じような構造がネストしている」パターンを見つけてくれており、それぞれに名前まで付けてくれています。
例えば、上で例にした'a optionを対象にしたものは、MaybeモナドやOptionモナドとして広く知られています。コンピュテーション式を作れるようになるための近道、それは色々なモナドを理解し、そのコンピュテーション式を実際に作ってみることです。
モナドさえ理解できればコンピュテーション式は作れるようになるか、というとそういうわけではありません。コンピュテーション式はモナド以上のことができてしまうため、モナドだけわかってもすべての機能の実装はできません(し、間違った実装を提供してしまいます)。
また、全然モナドじゃないコンピュテーション式も作れます。作れますが、それが実用的になることはそうそうないでしょう。コンピュテーション式の基本、それはモナドです。
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。