コンピュテーション式におけるreturnとyield
今日、id:htid46 とF#の話をしつつ帰った時のまとめです。
前提条件
次の2つのエントリを読んでいることが前提です。
returnとyieldの変換規則
先日のエントリでも書いたように、ReturnとYield、ReturnFromとYieldFromは全く同じ変換のされ方をします。
T(return e, C) = C(b.Return(e)) T(yield e, C) = C(b.Yield(e)) T(return! e, C) = C(b.ReturnFrom(src(e))) T(yield! e, C) = C(b.YieldFrom(src(e)))
つまり、ReturnもYieldも同じ実装にしたとしても、コンパイルは通ります。 ということは、そのコンピュテーション式により合うと思う方を実装すればいい・・・?
returnの意味とyieldの意味の違い
ところで、return
とyield
ではそれを使ったコードの持つ意味が違うように思えます。
例として、listコンピュテーション式を作ったとしましょう。
let f x = list { if x = 0 then return -1 return 1 } let g x = list { if x = 0 then yield -1 yield 1 } let f0, f1 = (f 0, f 1) let g0, g1 = (g 0, g 1)
さて、f0
, f1
, g0
, g1
のそれぞれの値は、どうなっていてほしいでしょうか?
こうなっていることを期待しませんか?
val f0 : int list = [-1] val f1 : int list = [1] val g0 : int list = [-1; 1] val g1 : int list = [1]
つまり、return
はそこで処理を打ち切るけど、yield
は打ち切らない、という違いがあるように思うのです。
これ、C#で考えてみた場合、yield break
とyield return
の関係に似ていませんか?
F#にはyield breakがない
そういえばありませんでした。
が、OptionBuilderでの実装を使うと、return
がyield break
の代わりになるのでは!?
ということで、こういう実装を考えてみました。
open System open Basis.Core.ComputationExpr (* 最初にpredを満たさなかった要素は結果に含めるtakeWhileの別バージョン *) module Seq = let takeWhileButFirst pred xs = seq { let cont = ref true use itor = (xs :> _ seq).GetEnumerator() while itor.MoveNext() && !cont do let x = itor.Current if not (pred x) then cont := false yield x } type ListBuilder internal () = member this.Zero() = [], Continue (* returnはBreak、yieldはContinue *) member this.Return(x) = [x], Break member this.ReturnFrom(xs: _ list) = xs, Break member this.Yield(x) = [x], Continue member this.YieldFrom(xs: _ list) = xs, Continue (* fしていって、Breakが出たら後ろは捨てる(isBreakをtrueにしたうえでfalseを返す) *) member this.Bind(xs, f: _ -> _ list * FlowControl) = let isBreak = ref false let res = xs |> Seq.map f |> Seq.takeWhileButFirst (function _, Continue -> true | _ -> isBreak := true; false) |> Seq.collect fst |> Seq.toList (res, if !isBreak then Break else Continue) (* Continueだったらrestを実行してappend、Breakだったらrestは捨てる *) member this.Combine((x: _ list, cont), rest: unit -> _ list * FlowControl) = match cont with | Break -> x, Break | Continue -> let rest, cont = rest () in List.append x rest, cont (* 以降、OptionBuilderと型以外は同じ定義 *) member this.While(guard, f) = if not (guard ()) then this.Zero() else let x = f () in this.Combine(x, fun () -> this.While(guard, f)) member this.For(xs: #seq<_>, f) = this.Using( xs.GetEnumerator(), fun itor -> this.While(itor.MoveNext, fun () -> f itor.Current)) member this.Delay(f: unit -> _ list * FlowControl) = f member this.Run(f) = f () |> fst let list = List.ListBuilder()
Bindは複雑ですが、まぁ、こんなもんでしょう。 最初に出てきたBreakは結果に含めたいので、takeWhileではなく別バージョンを定義して呼び出しています。
Combineも難しくはないでしょう。
ミソは、Return系とYield系で、タプルの第二要素が違う点です。 こうしてみると、シグネチャや変換規則が同じなだけで、ReturnとYieldは別のものだという感じがしませんか?
C#のyield break
は式をとれないため、yield return
してからyield break
する必要がありました。
ですが、今回実装したlistコンピュテーション式は、return expr
とすることで値を返しつつ処理を抜けることができます。
ちなみに、C#でのyield break
がしたい場合は、return! []
とでもするといいでしょう。
カスタムオペレータが使えたら素敵!
queryコンピュテーション式のhead
などのように、yieldBreak
のようなカスタムオペレータが作れれば、よりそれっぽいと思いませんか?
・・・が、これは出来ません。
先日は簡単のために省略したq
というパラメータを覚えているでしょうか?
これは、「その中でカスタムオペレータが使えるかどうか」を示すフラグです。
通常、yieldBreak
したいのはif
の中ですが・・・お気づきですね。
最後のパラメータがq
です。
T(if e then ce1 else ce2, V, C, q) = Assert(not q); C(if e then {| ce1 |}0 else {| ce2 |}0)
if
を使うためには、q
がfalse
である必要があります。
つまり、カスタムオペレータが使えない場所にしかif
は書けないんですね。残念・・・
ちなみに、else
を伴わないif
は、else
を伴うif
に変換されたうえでさらに変換が走るので、結局上のAssert(not q)
から逃れることは出来ません。
無念・・・
最後に
return
の挙動をどうするのがいいのか、実は2通り考えたんですが、どういう方針で行けばいいのか指針がないのでつらかったです。
yield break
をまねる、という方針があったのでよかったのですが、それがなかった場合はもう一方の(実装が楽な方)を実装してしまっていたでしょうね。
たぶん、それがなかったら今回のエントリの挙動ではなく、簡単な方を実装して済ませていたと思われます。
さて、もう一つの挙動はどんなものでしょうか。 これは読者への課題にしますね!