yieldとreturnの話

72
の話

Upload: bleis-tift

Post on 24-May-2015

4.337 views

Category:

Technology


2 download

DESCRIPTION

クラウド温泉4.0@小樽 - The Return of F#の発表資料です。 F#のコンピュテーション式のyieldとreturnがどうあるべきかを説明しています。

TRANSCRIPT

Page 1: yieldとreturnの話

yieldと returnの話

bleis-tift

July 27, 2014

Page 2: yieldとreturnの話

自己紹介

id:bleis-tift / @bleis

なごやではたらくゆるふわ F#er

静的型付きの関数型言語が好き

Page 3: yieldとreturnの話

話すこと

第一部:コンピュテーション式

第二部:yieldと returnの違い ~考察編~

第三部:yieldと returnの違い ~実装編~

第四部:あるべき論とまとめ

コンピュテーション式を使う側の話ではなく、作る側の話をします。

Page 4: yieldとreturnの話

第一部:コンピュテーション式

Page 5: yieldとreturnの話

コンピュテーション式とは

通常のF#の文法を拡張し、カスタマイズポイントを提供した式

F#の文法に似た文法に、独自の処理を差し込める

.F#の文法..

.

let someFunc a b =

let x = f a

let y = g b

x + y

.コンピュテーション式..

.

let someFunc a b = builder {

let! x = f a

let! y = g b

return x + y

}

Page 6: yieldとreturnの話

コンピュテーション式でできることの例

optionに対するmatchのネストを取り除く

状態変数の受け渡しを隠す

非同期処理における関数のネストを取り除く

などなどただし今回はここら辺はすっ飛ばすので注意

Page 7: yieldとreturnの話

実現方法

F#では、コンピュテーション式は単なる式変形の規則群によって実現

特定の interfaceを実装するなどの必要がない式を変形した結果がコンパイル可能であるかどうかが重要

.

.コンピュテーション式の文法

変換規則通常の文法

では、変換規則を見ていきましょう!

Page 8: yieldとreturnの話

表記

ゴシック体 F#コード。例えば、fun x -> x

明朝体 F#コードの中で別の何かに置き換わる部分。例えば、cexpr

イタリック F#コードに関わらない部分(変換に関わる部分)。例えば、T (e, C)

Page 9: yieldとreturnの話

変換規則(最外部)

最初はコンピュテーション式の一番外側の部分です。.

. builder-expr { cexpr }

これは、次のように変換されます。.

. let b = builder-expr in {| cexpr |}

bはフレッシュな変数です。

Page 10: yieldとreturnの話

builder-expr

単なる式(別名「ビルダー」)

ビルダー自体は 1つのコンピュテーション式につき 1回しか評価されないビルダーの型に変換で呼び出されるメソッドを定義する

メソッドはすべてインスタンスメソッド

Page 11: yieldとreturnの話

{| ... |}

括弧の中に含まれる式をコア言語に変換する

{| cexpr |}であれば、cexprを変換する

どう変換するかは、以降の変換規則参照

Page 12: yieldとreturnの話

cexpr

変換対象となる一番外側のコンピュテーション式

これ以外のコンピュテーション式は、ceと表現

cexprは必要があればDelay変換、Quote変換、Run変換が行われる

Page 13: yieldとreturnの話

変換規則の表現

変換規則は、T 表記によって記述する.T 表記..

.T (e, C)

e:変換されるコンピュテーション式

C:変換済みのコンテキスト情報

eにマッチする変換規則を探し、変換していく

Page 14: yieldとreturnの話

{| cexpr |}のT表記による表現(T表現)

.{| cexpr |}の T 表現..

. {| cexpr |} ≡ T (cexpr, λv.v)

λv.vの部分は、無名関数ドット以前が引数ドット以降が本体

最終的に、vに変換された式がやってくるので、それをそのまま返す

関数適用は実行時ではなく、コンパイル時に行われる

Page 15: yieldとreturnの話

returnの変換規則

.returnの変換規則..

. T (return e, C) = C(b.Return(e))

cexprが return 42だった場合の変換は、.returnの変換例..

.

T (return 42, λv.v)

−→(λv.v)(b.Return(42))

−→b.Return(42)

できた!

Page 16: yieldとreturnの話

letの変換規則

.変換規則..

.

T (return e, C) = C(b.Return(e))

T (let p = e in ce, C) = T (ce, λv.C(let p = e in v))

.letの変換例..

.

T (let x = 42 in return x, λv1.v1)

−→T (return x, λv2.(λv1.v1)(let x = 42 in v2))

−→(λv2.(λv1.v1)(let x = 42 in v2))(b.Return(x))

−→(λv1.v1)(let x = 42 in b.Return(x))

−→let x = 42 in b.Return(x)

Page 17: yieldとreturnの話

ifの変換規則.変換規則..

.

{| cexpr |} ≡ T (cexpr, λv.v)

T (return e, C) = C(b.Return(e))

T (if e then ce1 else ce2, C) = C(if e then {| ce1 |} else {| ce2 |})T (if e then ce, C) = C(if e then {| ce |} else b.Zero())

.ifの変換例..

.

T (if c then return 42, λv1.v1)

−→(λv1.v1)(if c then {| return 42 |} else b.Zero())

−→(λv1.v1)(if c then T (return 42, λv2.v2) else b.Zero())

−→(λv1.v1)(if c then (λv2.v2)(b.Return(42)) else b.Zero())

−→(λv1.v1)(if c then b.Return(42) else b.Zero())

−→if c then b.Return(42) else b.Zero()

Page 18: yieldとreturnの話

ce1; ce2の変換規則

.変換規則..

.

{| cexpr |} ≡ T (cexpr, λv.v)

T (return e, C) = C(b.Return(e))

T (ce1; ce2, C) = C(b.Combine({| ce1 |},b.Delay(fun () -> {| ce2 |})))

.ce1; ce2の変換例..

.

T (return 10; return 20, λv1.v1)

−→(λv1.v1)(b.Combine({| return 10 |},b.Delay(fun () -> {| return 20 |})))−→(λv1.v1)

(b.Combine(T (return 10, λv2.v2),b.Delay(fun () -> T (return 20, λv3.v3))))

−→(λv1.v1)

(b.Combine((λv2.v2)(b.Return(10)),b.Delay(fun () -> (λv3.v3)(b.Return(20)))))

−→(λv1.v1)(b.Combine(b.Return(10),b.Delay(fun () -> b.Return(20))))

−→b.Combine(b.Return(10),b.Delay(fun () -> b.Return(20)))

Page 19: yieldとreturnの話

whileの変換規則.変換規則..

.

{| cexpr |} ≡ T (cexpr, λv.v)

T (return e, C) = C(b.Return(e))

T (if e then ce, C) = C(if e then {| ce |} else b.Zero())

T (ce1; ce2, C) = C(b.Combine({| ce1 |},b.Delay(fun () -> {| ce2 |})))T (while e do ce, C) = T (ce, λv.C(b.While(fun () -> e,b.Delay(fun () -> v))))

.whileの変換例..

.

T (while f() do if g() then return 42 done; return 0, λv1.v1)

−→(λv1.v1)(b.Combine({| while f() do if g() then return 42 |},b.Delay(fun () -> {| return 0 |})))−→(λv1.v1)(b.Combine(

T (if g() then return 42, λv2.b.While(fun () -> f(),b.Delay(fun () -> v2)))

,b.Delay(fun () -> b.Return(0))))

−→(λv1.v1)(b.Combine(

(λv2.b.While(fun () -> f(),b.Delay(fun () -> v2)))(if g() then b.Return(42) else b.Zero())

,b.Delay(fun () -> b.Return(0))))

−→(λv1.v1)(b.Combine(

b.While(fun () -> f(),b.Delay(fun () -> if g() then b.Return(42) else b.Zero()))

,b.Delay(fun () -> b.Return(0))))

−→b.Combine(b.While(fun () -> f(),b.Delay(fun () -> if g() then b.Return(42) else b.Zero()))

,b.Delay(fun () -> b.Return(0)))

Page 20: yieldとreturnの話

F#のコンピュテーション式の特徴

F#のコンピュテーション式は、

Haskellの do式

Scalaの for式

C#のクエリ式

などと同じような仕組み違うのは、コア言語と同等以上の表現力を持ち得る点(ループ構文や例外処理なども使える)

→ F#のコンピュテーション式は表現力が豊富!

Page 21: yieldとreturnの話

第二部:yieldと returnの違い~考察編~

Page 22: yieldとreturnの話

yieldと returnの変換規則

.変換規則..

.

T (yield e, C) = C(b.Yield(e))

T (return e, C) = C(b.Return(e))

メソッドが違うだけ・・・今回の主題:

なぜコード上での意味が同じ変換規則を持つものがあるのか?

Page 23: yieldとreturnの話

yieldと returnを使い分ける?

yieldっぽいものには yieldを使い、returnっぽいものには returnを使う・・・?

コレクションっぽいものには yieldを使い、そうでないっぽいものには returnを使う・・・?

っぽいって何!曖昧な判断基準は避けたい

Page 24: yieldとreturnの話

yieldと returnの違いを考える

辞書を引いてみる

yield 生み出す。produce/provide

return 戻す。give back

returnはその後の処理を実行しないようにすべきモナドの return?知りませんなぁ

Page 25: yieldとreturnの話

yieldと returnの違い

.yield..

.

list {

yield 1

printfn "done"

}

.return..

.

list {

return 1

printfn "done"

}

"done"が出力されるべきかどうか

Page 26: yieldとreturnの話

C#ではどうか?

returnIE<T>

yield returnyield break

クエリ式select

目指すのは、IE<T>の yield returnと yield breakのようなもの

Page 27: yieldとreturnの話

seq式

returnに非対応

C#での yield breakに相当する操作が困難

よし、seqをコンピュテーション式で再実装してみよう!

Page 28: yieldとreturnの話

第三部:yieldと returnの違い~実装編~

Page 29: yieldとreturnの話

実装する上での問題点

yieldも returnも、変換規則が同じ・・・

Page 30: yieldとreturnの話

実装案1

returnが処理を打ち切る、という点に注目してみる

returnしたらそのコンピュテーション式を抜け、値を返す必要がある

Returnメソッドで返したい値を含む例外を投げ、Runで捕捉すればいい!

Page 31: yieldとreturnの話

例外による実装

.ビルダー..

.

type ReturnExn<'T>(xs: 'T seq) =

inherit System.Exception()

member this.Value = xs

type SeqBuilder<'T>() =

member this.Yield(x: 'T) = Seq.singleton x

member this.Return(x: 'T) =

raise (ReturnExn(Seq.singleton x))

member this.Combine(xs: 'T seq, cont: unit -> 'T seq) =

Seq.append xs (cont ())

member this.Delay(f: unit -> 'T seq) = f

member this.Run(f: unit -> 'T seq) =

try f () with

| :? ReturnExn<'T> as e -> e.Value

let seq2<'T> = SeqBuilder<'T>() // 型関数

Page 32: yieldとreturnの話

例外による実装

.使用例..

.

> seq2 { yield 1; yield 2 };;

val it : seq<int> = seq [1; 2]

> seq2 { return 1; return 2 };;

val it : seq<int> = seq [1]

おぉ!

Page 33: yieldとreturnの話

例外による実装

Scalaの一部の returnや、breakでも例外を使っている

分かりやすいように見える

だがしかし!

Page 34: yieldとreturnの話

例外による実装の問題点

.ダメな例..

.

> seq2 { yield 1; return 2; return 3 };;

val it : seq<int> = seq [2]

yieldがC#の yield return、returnがC#の yield breakだとすると.C#でやる..

.

IEnumerable<int> F() {

yield return 1;

yield break 2;

yield break 3; }

これは、1と 2を含むシーケンスを返す

Page 35: yieldとreturnの話

改良版

.Combineでも ReturnExnを捕捉..

.

type SeqBuilder<'T>() =

member this.Yield(x: 'T) = Seq.singleton x

member this.Return(x: 'T) =

raise (ReturnExn(Seq.singleton x))

member this.Combine(xs: 'T seq, cont: unit -> 'T seq) =

try

Seq.append xs (cont ())

with

| :? ReturnExn<'T> as e ->

raise (ReturnExn(Seq.append xs e.Value))

member this.Delay(f: unit -> 'T seq) = f

member this.Run(f: unit -> 'T seq) =

try f () with

| :? ReturnExn<'T> as e -> e.Value

let seq2<'T> = SeqBuilder<'T>()

Page 36: yieldとreturnの話

例外による実装

try-withを提供する場合は、ReturnExnを捕捉して reraiseする必要がある

結局そんなに分かりやすい実装にはならない

例外をフロー制御に使うことに対する抵抗感

やりたいことは実現できた

Page 37: yieldとreturnの話

実装案2

その後の処理を続ける/続けないが判定できればいい

「その後の処理」を関数として受け取るメソッドでその関数を呼び出すか判定を入れる

Page 38: yieldとreturnの話

状態変数による実装

.ビルダー..

.

type SeqBuilder() =

let mutable isExit = false

member this.Yield(x) = Seq.singleton x

member this.Return(x) =

isExit <- true

Seq.singleton x

member this.Combine(xs, cont) =

if isExit then xs else Seq.append xs (cont ())

member this.Delay(f) = f

member this.Run(f) =

let res = f ()

isExit <- false

res

let seq2 = SeqBuilder()

Page 39: yieldとreturnの話

状態変数による実装

.使用例..

.

> seq2 { yield 1; yield 2 };;

val it : seq<int> = seq [1; 2]

> seq2 { return 1; return 2 };;

val it : seq<int> = seq [1]

> seq2 { yield 1; return 2; return 3 };;

val it : seq<int> = seq [1; 2]

おぉ!

Page 40: yieldとreturnの話

状態変数による実装

単純

分かりやすいように見える

だがしかし!

Page 41: yieldとreturnの話

状態変数による実装の問題点

ビルダーが状態を持っている

マルチスレッド等で同時に同じビルダーのインスタンス(seq2)を使うと・・・

.

.

Thread A

seq2 {yield 1

; // Combineyield 2 // oops!

} // Run

val it : seq<int> = seq [1]

seq2.isExit

false

true

false

Thread B

seq2 {return 10

} // Run

Page 42: yieldとreturnの話

改良版

.ビルダー..

.

type SeqBuilder() =

(* 省略 *)

let seq2 () = SeqBuilder()

.使用例..

.

> seq2 () { yield 1; yield 2 };;val it : seq<int> = seq [1; 2]> seq2 () { return 1; return 2 };;val it : seq<int> = seq [1]> seq2 () { yield 1; return 2; return 3 };;val it : seq<int> = seq [1; 2]

Page 43: yieldとreturnの話

状態変数による実装

ビルダーのインスタンスを毎回作る

ユーザがインスタンスを共有することは禁止できない

毎回関数呼び出しするのは面倒

実用には耐えない・・・

Page 44: yieldとreturnの話

実装案3

状態変数による実装は、ビルダーのインスタンスに保持しているのが問題引数で持ちまわせばいいじゃない!

内部で状態を引数で引き回し、RunではがすCombineで状態が Breakだったら後続処理を実行しない

Page 45: yieldとreturnの話

状態引数による実装

.ビルダー..

.

type FlowControl = Break | Continue

type SeqBuilder() =

member this.Yield(x) = Seq.singleton x, Continue

member this.Return(x) = Seq.singleton x, Break

member this.Combine((xs, st), cont) =

match st with

| Break -> xs, Break

| Continue ->

let ys, st = cont ()

Seq.append xs ys, st

member this.Delay(f) = f

member this.Run(f) = f () |> fst

let seq2 = SeqBuilder()

Page 46: yieldとreturnの話

状態引数による実装

.使用例..

.

> seq2 { yield 1; yield 2 };;

val it : seq<int> = seq [1; 2]

> seq2 { return 1; return 2 };;

val it : seq<int> = seq [1]

> seq2 { yield 1; return 2; return 3 };;

val it : seq<int> = seq [1; 2]

おぉ!

Page 47: yieldとreturnの話

状態引数による実装

yieldと returnの対称性が明確になった

通常の実装よりもかなり複雑

いいのでは?

Page 48: yieldとreturnの話

yieldと returnの実装比較

.例外による実装..

.

member this.Yield(x: 'T) = Seq.singleton x

member this.Return(x: 'T) =raise (ReturnExn(Seq.singleton x))

.状態変数による実装..

.

member this.Yield(x) = Seq.singleton xmember this.Return(x) =isExit <- trueSeq.singleton x

.状態引数による実装..

.member this.Yield(x) = Seq.singleton x, Continuemember this.Return(x) = Seq.singleton x, Break

Page 49: yieldとreturnの話

実装案4

例外による実装では、処理を打ち切るために例外を使ったこれは、継続を捨てることと同義

yieldの場合は継続を呼び出すreturnの場合は継続を捨てる

Page 50: yieldとreturnの話

継続渡しによる実装.ビルダー..

.

type SeqBuilder() =

member this.Yield(x) = fun k -> k (Seq.singleton x)

member this.Return(x) = fun _ -> Seq.singleton x

member this.Combine(f, cont) =

fun k -> f (fun xs -> cont () k |> Seq.append xs)

member this.Delay(f) = f

member this.Run(f) = f () id

let seq2 = SeqBuilder()

.使用例..

.

> seq2 { yield 1; yield 2 };;

val it : seq<int> = seq [1; 2]

> seq2 { return 1; return 2 };;

val it : seq<int> = seq [1]

> seq2 { yield 1; return 2; return 3 };;

val it : seq<int> = seq [1; 2]

Page 51: yieldとreturnの話

継続渡しによる実装

yieldと returnの対称性が明確コードは短いが、複雑(Bindを実装してない詐欺)

これは状態引数版もだけど・・・

Page 52: yieldとreturnの話

実装の速度比較

各実装で 10万回 yieldしてみた

ビルダー 時間

returnしない実装 20.5ms例外による実装 20.5ms状態変数による実装 20.7ms状態引数による実装 21.2ms継続による実装 22.6msseq式 1.18ms

実装法による差は小さいが、そもそも独自のビルダーは遅い

Page 53: yieldとreturnの話

第四部:あるべき論とまとめ

Page 54: yieldとreturnの話

ここまで

コンピュテーション式は表現力が豊富

yieldと returnは変換規則は同じだが、意味は違う

標準の seq式は returnに対応していない→再実装シーケンス用の yieldと returnに別の意味を持たせる複数の実装

例外による実装状態変数による実装(実用には耐えない)状態引数による実装継続による実装

Page 55: yieldとreturnの話

各ライブラリでの実装状況

seq/list/optionのいずれかに対するコンピュテーション式が returnをどう扱うか対象ライブラリ

FSharpxExtCoreFSharpPlusBasis.Core

2014年 7月 21日時点

Page 56: yieldとreturnの話

各ライブラリでの実装状況.検証用コード例..

.

let xs = [30; 10; 15; 21; -1; 50]

builder {

let i = ref 0

while !i < xs.Length do

if xs.[!i] = -1 then

return false

incr i

return true

}

コンパイルできるか

false的なものが返るか

Page 57: yieldとreturnの話

各ライブラリでの実装状況

.検証用コード例展開..

.

let b = builder

b.Run(

b.Delay(fun () ->

let i = ref 0

b.Combine(

b.While(

(fun () -> !i < xs.Length),

b.Delay(fun () ->

b.Combine(

(if xs.[!i] = -1 then b.Return(false)

else b.Zero()),

b.Delay(fun () -> incr i; b.Zero())))),

b.Delay(fun () -> b.Return(true)))))

Page 58: yieldとreturnの話

FSharpx

コンパイルできない

Page 59: yieldとreturnの話

FSharpx

Combineの型が駄目.FSharpxの Combineのシグネチャ...'a option * ('a -> 'b option) -> 'b option

.エラー箇所の展開..

.

// 'a option * ('a -> 'b option) -> 'b option

b.Combine(

// bool option

(if xs.[!i] = -1 then b.Return(false) else b.Zero()),

// unit -> 'a option

b.Delay(fun () -> incr i; b.Zero()))

.正しいシグネチャ...'a option * (unit -> 'a option) -> 'a option

Page 60: yieldとreturnの話

ExtCore

コンパイルできない

Page 61: yieldとreturnの話

ExtCore

Zeroの実装が駄目.ExtCoreの Zeroの実装..

.member inline __.Zero () : unit option =

Some () // TODO : Should this be None?

コメント・・・

Page 62: yieldとreturnの話

FSharpPlus

コンパイルできない

Whileをそもそも提供していない

それはそれでアリな選択肢。潔い

Page 63: yieldとreturnの話

Basis.Core

コンパイルできた

falseっぽいものが返った

Page 64: yieldとreturnの話

各ライブラリでの実装状況

比較になりませんでした!

Page 65: yieldとreturnの話

yieldと returnの違い再考

コンピュテーション式を正しく実装できているライブラリの少なさ

yield/return以前の問題

本当に意味上の違いを与えるべき?F#のコンピュテーション式の表現力を活かすなら与えるべきBindと Returnくらいしか提供しないなら不要(FSharpPlusはこちら)

Page 66: yieldとreturnの話

コンピュテーション式再考

YieldやReturnが継続を受け取ればよかった?効率を考えると、コンパイル時に解決したほうがいい

現状のままでも実現可能この柔軟性を活用しない手はないのでは?

Page 67: yieldとreturnの話

方針の提案

ライブラリの性質によって、何を実装するかを分けて考える

モナド/モナドプラス程度の提供でとどめる場合

より汎用的な計算も行えるようにする場合

モナド以外にコンピュテーション式を使う場合

Page 68: yieldとreturnの話

モナド/モナドプラス提供程度の場合

モナド提供程度Bind/Returnは必須(定義より)ReturnFromもあると便利場合によっては、Runを別で提供

モナドに包まれた値を取り出すコンピュテーション式

モナドプラス提供程度モナド用のメソッドに加え、Zeroと CombineZeroはmzero、Combineはmplusに対応Combineの変換規則により、Delayも必要

member this.Delay(f) = f ()

Page 69: yieldとreturnの話

より汎用的なライブラリの場合

機能別にモジュールを分けるBind/Return程度を提供するビルダー用モジュールCombineも使える高級なビルダー用モジュール

Combineはmplusに対応するものではなく、継続を受け取る版を採用する

必然的に、Delay/Runの実装も必要member this.Delay(f) = f

member this.Run(f) = f ()

Zeroもあった方がいいelseなしの ifが使えるようになる

Page 70: yieldとreturnの話

モナド以外の場合

できるだけやめておいた方が・・・

Combineを提供する場合、yieldと returnを実装し分ける

カスタムオペレータを使うことも考慮に入れる

Page 71: yieldとreturnの話

今後の課題

FSharpx/ExtCoreにバグ報告

機能別にモジュールを分けたコンピュテーション式ライブラリの作成

実装したビルダー(特に状態引数による実装と、継続による実装)の意味的な正しさの検証

提案した方針の啓蒙

Page 72: yieldとreturnの話

おわり