結論、これだけ
F#言語のコンピューテーション式とは何か?答えは簡単です。例えば以下のコンピューテーション式
builder {
let! x = expression
return some_expression_using_x
}
これは以下の糖衣構文です。
builder.Bind(expression, fun x -> builder.Return some_expression_using_x)
ただこれだけです。しかしこれだけでは意義が分からないと思うので、この糖衣構文がどう役立つのかを説明します。
ロガーの例
例えば以下のように変数の内容をログに書き出したいとします。
let log p = printfn "expression is %A" p
let loggedWorkflow =
let x = 42
log x
let y = x + 1
log y
//return
y * 2
これを実行すると当然以下のような出力がされます。
expression is 42
expression is 43
これをもっとスマートに書きたいです。コンピューテーション式を活用すれば以下のように書くことが可能です。
let loggedWorkflow = logger {
let! x = 42
let! y = x + 1
return y * 2
}
ログ出力したい部分をlogger {}
でくくり、その中でlet!
で束縛した内容がログに出力されるという仕組みです。とても読みやすいですね。こんな書き方ができるためには以下のようにlogger
を定義すればよいです。
type LoggingBuilder() =
let log p = printfn "expression is %A" p
member this.Bind(x, f) =
log x
f x
member this.Return(x) =
x
let logger = new LoggingBuilder()
Bind
の中でログ出力しています。このlogger
を定義したうえで前述のコンピューテーション式を実行すると最初のコードと同じようにログ出力がされます。
expression is 42
expression is 43
処理の流れを追う
なぜこれで所望の動作になるのか考えます。まずは簡単な場合としてログ出力が1か所のみの以下のコードを考えます。
let loggedWorkflow = logger {
let! x = 42
return x + 1
}
これを実行すると以下の出力になります。
expression is 42
コンピューテーション式はただの糖衣構文なので上記は以下のように展開されます。
let loggedWorkflow = logger.Bind(
42, fun x -> logger.Return x + 1
)
上記展開されたコードとlogger
のコードから処理の流れを追ってみてください。logger.Bind
で42
のログが出力された後 x + 1
の部分の計算が行われ、確かに所望の動作になることが分かります。また当然ですが、展開後のコードと展開前のコンピューテーション式どちらを実行しても同じ動作、出力になります。
もともとの2つのlet!
がある場合も全く同じように考えられます。糖衣構文を展開すると以下のようになります。
let loggedWorkflow = logger.Bind(
42, fun x -> logger.Bind(
x + 1, fun y -> logger.Return (y * 2)
)
)
複数のlet!
があるのでBind
の中でBind
が使われることになります。ただ糖衣構文の変換を2度適用しただけです。ちょっと複雑ですが処理の流れを追ってみると、確かにlet!
で束縛した値がログに出力されるという挙動になります。
Optionの例
以下のようにOption
型を伴う式でどこかでNone
になったらそこで処理を止めてNone
を返すような処理を考えます。
let map = Map [("a", Map [("z", 1)])]
let optionWorkflow =
let x = map.TryFind "a"
match x with
| None -> None
| Some xx ->
let y = xx.TryFind "b"
match y with
| None -> None
| Some yy -> yy + 1
入れ子のMapから順にキー"a"
、"b"
とたどってその値に1を足したものを返す、ただしキーが見つからなければNone
を返す、ということをしています。
これをコンピューテーション式を活用することで以下のように書けます。
let optionWorkflow = option {
let! xx = map.TryFind "a"
let! yy = xx.TryFind "b"
return yy + 1
}
とても見やすくなりました。option {}
でくくった中のlet!
で束縛した値がNone
なら後続の処理を実行せず即座にNone
を返すという仕組みです。こんな書き方ができるためには以下のようにoption
を定義すればよいです。
type OptionBuilder() =
member this.Bind(x: 'a option, f: 'a -> 'b option) =
match x with
| Some value -> f value
| None -> None
member this.Return(x) =
Some x
let option = new OptionBuilder()
Bindの中で場合分けをして、None
ならNone
を返し、そうでなければその値を使ってf
を実行します。このoption
を定義したうえで前述のコンピューテーション式を実行すると最初のコードと同じ効果が得られます。
処理の流れを追う
これもロガーの場合と全く同じです。この場合のコンピューテーション式は以下のように展開されます。
let optionWorkflow = option.Bind(
map.TryFind "a", fun xx -> option.Bind(
xx.TryFind "b", fun yy -> option.Return (yy + 1)
)
)
処理の流れをひとつずつ追って、所望の動作になることを確かめてください。
そもそもこの糖衣構文はなにを意味しているのか?
let!
が糖衣構文であり、それを活用することでいろいろ便利なことができることはわかりましたが、そもそもこの糖衣構文はどこから生まれて何を意味しているのでしょう?
builder.Bind(expression, fun x -> builder.Return some_expression_using_x)
結論を言うと、これは「手続き型言語における変数(定数)定義を関数型的に表現したもの」です。変数定義とはF#でいうlet a = 1
の様な構文ですね。実はこのlet
のような変数定義は厳格な関数型の思想では許されないです。じゃあこの変数定義と同じことを関数型でどう行うのかという問いに対する答えが上の書き方になります。
と言っても意味不明だと思うので説明します。まず関数型言語ではすべてを入力と出力がある関数で考えます。手続き型言語における変数定義というものは、まず変数を定義し、その後その変数を定義以降のどこかで使います。この「定義以降のどこか」で変数を使っている部分は変数の値に依存する処理であり、言い換えれば変数の値を入力とする関数ととらえられます。
具体例を示すとlet
を使って変数q
を作っている以下のコード
let divmod (x, y) =
let q = x / y
(q, x - q * y)
これは厳格な関数指向に沿った、変数を使わない以下のコードに変換できます。
let divmod (x, y) =
(fun q -> (q, x - q * y)) (x / y)
// または以下のように書いた方が分かりやすいかも
// (x / y) |> (fun q -> (q, x - q * y))
変数を使っている部分はfun q -> (q, x - q * y)
のように定義した変数を入力とする関数になっていることが分かります。
つまりこれはlet
という変数定義構文を、関数指向の考え方で以下の同値のコードに解釈しなおすことができるという事です。
let Bind (v, f) = f v
let divmod (x, y) =
Bind (x / y, (fun q -> (q, x - q * y)))
これはlet!
の糖衣構文がやっているのと同じ変換です。
参考文献
以下の記事が無茶苦茶分かりやすかった