yieldとreturnの話
Upcoming SlideShare
Loading in...5
×

Like this? Share it with your network

Share

yieldとreturnの話

  • 4,416 views
Uploaded on

クラウド温泉4.0@小樽 - The Return of F#の発表資料です。 ...

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

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
4,416
On Slideshare
3,705
From Embeds
711
Number of Embeds
8

Actions

Shares
Downloads
13
Comments
0
Likes
17

Embeds 711

http://bleis-tift.hatenablog.com 439
https://twitter.com 218
http://feedly.com 27
http://connpass.com 16
http://www.feedspot.com 6
http://www.slideee.com 2
https://www.inoreader.com 2
https://tweetdeck.twitter.com 1

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. yield とreturn の話 bleis-tift July 27, 2014
  • 2. 自己紹介 id:bleis-tift / @bleis なごやではたらくゆるふわ F#er 静的型付きの関数型言語が好き
  • 3. 話すこと 第一部:コンピュテーション式 第二部:yield と return の違い ∼考察編∼ 第三部:yield と return の違い ∼実装編∼ 第四部:あるべき論とまとめ コンピュテーション式を使う側の話ではなく、 作る側の話をします。
  • 4. 第一部:コンピュテーション式
  • 5. コンピュテーション式とは 通常の 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 }
  • 6. コンピュテーション式でできることの例 option に対する match のネストを取り除く 状態変数の受け渡しを隠す 非同期処理における関数のネストを取り除く などなど ただし今回はここら辺はすっ飛ばすので注意
  • 7. 実現方法 F#では、コンピュテーション式は単なる式変 形の規則群によって実現 特定の interface を実装するなどの必要がない 式を変形した結果がコンパイル可能であるかど うかが重要 . . コンピュテーション式の文法 変換規則 通常の文法 では、変換規則を見ていきましょう!
  • 8. 表記 ゴシック体 F#コード。例えば、fun x -> x 明朝体 F#コードの中で別の何かに置き換わ る部分。例えば、cexpr イタリック F#コードに関わらない部分(変換に 関わる部分)。例えば、T(e, C)
  • 9. 変換規則(最外部) 最初はコンピュテーション式の一番外側の部分 です。 . . builder-expr { cexpr } これは、次のように変換されます。 . . let b = builder-expr in {| cexpr |} b はフレッシュな変数です。
  • 10. builder-expr 単なる式(別名「ビルダー」) ビルダー自体は 1 つのコンピュテーション式 につき 1 回しか評価されない ビルダーの型に変換で呼び出されるメソッド を定義する メソッドはすべてインスタンスメソッド
  • 11. {| ... |} 括弧の中に含まれる式をコア言語に変換する {| cexpr |} であれば、cexpr を変換する どう変換するかは、以降の変換規則参照
  • 12. cexpr 変換対象となる一番外側のコンピュテーショ ン式 これ以外のコンピュテーション式は、ce と 表現 cexpr は必要があれば Delay 変換、Quote 変 換、Run 変換が行われる
  • 13. 変換規則の表現 変換規則は、T 表記によって記述する . T 表記 .. .T(e, C) e:変換されるコンピュテーション式 C:変換済みのコンテキスト情報 e にマッチする変換規則を探し、変換していく
  • 14. {| cexpr |}のT 表記による表現(T 表現) . {| cexpr |} の T 表現 .. . {| cexpr |} ≡ T(cexpr, λv.v) λv.v の部分は、無名関数 ドット以前が引数 ドット以降が本体 最終的に、v に変換された式がやってくるの で、それをそのまま返す 関数適用は実行時ではなく、 コンパイル時に行われる
  • 15. 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) できた!
  • 16. 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)
  • 17. 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()
  • 18. 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)))
  • 19. 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)))
  • 20. Fのコンピュテーション式の特徴 F#のコンピュテーション式は、 Haskell の do 式 Scala の for 式 C#のクエリ式 などと同じような仕組み 違うのは、コア言語と同等以上の表現力を持ち得 る点(ループ構文や例外処理なども使える) → F#のコンピュテーション式は 表現力が豊富!
  • 21. 第二部:yieldとreturnの違い ∼考察編∼
  • 22. yield とreturn の変換規則 . 変換規則 .. . T(yield e, C) = C(b.Yield(e)) T(return e, C) = C(b.Return(e)) メソッドが違うだけ・・・ 今回の主題: なぜコード上での意味が同じ 変換規則を持つものがあるのか?
  • 23. yield とreturn を使い分ける? yieldっぽいものには yield を使い、returnっぽ いものには return を使う・・・? コレクションっぽいものには yield を使い、そ うでないっぽいものには return を使う・・・? っぽいって何! 曖昧な判断基準は避けたい
  • 24. yield とreturn の違いを考える 辞書を引いてみる yield 生み出す。produce/provide return 戻す。give back return はその後の処理を実行しないようにすべき モナドの return?知りませんなぁ
  • 25. yield とreturn の違い . yield .. . list { yield 1 printfn "done" } . return .. . list { return 1 printfn "done" } "done"が出力されるべきかどうか
  • 26. Cではどうか? return IE<T> yield return yield break クエリ式 select 目指すのは、IE<T>の yield return と yield break の ようなもの
  • 27. seq 式 return に非対応 C#での yield break に相当する操作が困難 よし、seq をコンピュテーション式で再実装して みよう!
  • 28. 第三部:yieldとreturnの違い ∼実装編∼
  • 29. 実装する上での問題点 yield も return も、変換規則が同じ・・・
  • 30. 実装案1 return が処理を打ち切る、という点に注目し てみる return したらそのコンピュテーション式を抜 け、値を返す必要がある Return メソッドで返したい値を含む例外を投 げ、Run で捕捉すればいい!
  • 31. 例外による実装 . ビルダー .. . 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>() // 型関数
  • 32. 例外による実装 . 使用例 .. . > seq2 { yield 1; yield 2 };; val it : seq<int> = seq [1; 2] > seq2 { return 1; return 2 };; val it : seq<int> = seq [1] おぉ!
  • 33. 例外による実装 Scala の一部の return や、break でも例外を 使っている 分かりやすいように見える だがしかし!
  • 34. 例外による実装の問題点 . ダメな例 .. . > 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 を含むシーケンスを返す
  • 35. 改良版 . 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>()
  • 36. 例外による実装 try-with を提供する場合は、ReturnExn を捕捉 して reraise する必要がある 結局そんなに分かりやすい実装にはならない 例外をフロー制御に使うことに対する抵抗感 やりたいことは実現できた
  • 37. 実装案2 その後の処理を続ける/続けないが判定でき ればいい 「その後の処理」を関数として受け取るメ ソッドでその関数を呼び出すか判定を入れる
  • 38. 状態変数による実装 . ビルダー .. . 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()
  • 39. 状態変数による実装 . 使用例 .. . > 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] おぉ!
  • 40. 状態変数による実装 単純 分かりやすいように見える だがしかし!
  • 41. 状態変数による実装の問題点 ビルダーが状態を持っている マルチスレッド等で同時に同じビルダーのイ ンスタンス(seq2)を使うと・・・ . . Thread A seq2 { yield 1 ; // Combine yield 2 // oops! } // Run val it : seq<int> = seq [1] seq2.isExit false true false Thread B seq2 { return 10 } // Run
  • 42. 改良版 . ビルダー .. . 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]
  • 43. 状態変数による実装 ビルダーのインスタンスを毎回作る ユーザがインスタンスを共有することは禁止 できない 毎回関数呼び出しするのは面倒 実用には耐えない・・・
  • 44. 実装案3 状態変数による実装は、ビルダーのインスタ ンスに保持しているのが問題 引数で持ちまわせばいいじゃない! 内部で状態を引数で引き回し、Run ではがす Combine で状態が Break だったら後続処理を実行 しない
  • 45. 状態引数による実装 . ビルダー .. . 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()
  • 46. 状態引数による実装 . 使用例 .. . > 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] おぉ!
  • 47. 状態引数による実装 yield と return の対称性が明確になった 通常の実装よりもかなり複雑 いいのでは?
  • 48. 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 x member this.Return(x) = isExit <- true Seq.singleton x . 状態引数による実装 .. . member this.Yield(x) = Seq.singleton x, Continue member this.Return(x) = Seq.singleton x, Break
  • 49. 実装案4 例外による実装では、処理を打ち切るために 例外を使った これは、継続を捨てることと同義 yield の場合は継続を呼び出す return の場合は継続を捨てる
  • 50. 継続渡しによる実装 . ビルダー .. . 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]
  • 51. 継続渡しによる実装 yield と return の対称性が明確 コードは短いが、複雑(Bind を実装してない 詐欺) これは状態引数版もだけど・・・
  • 52. 実装の速度比較 各実装で 10 万回 yield してみた ビルダー 時間 return しない実装 20.5ms 例外による実装 20.5ms 状態変数による実装 20.7ms 状態引数による実装 21.2ms 継続による実装 22.6ms seq 式 1.18ms 実装法による差は小さいが、 そもそも独自のビルダーは遅い
  • 53. 第四部:あるべき論とまとめ
  • 54. ここまで コンピュテーション式は表現力が豊富 yield と return は変換規則は同じだが、意味は 違う 標準の seq 式は return に対応していない→再 実装 シーケンス用の yield と return に別の意味を持 たせる複数の実装 例外による実装 状態変数による実装(実用には耐えない) 状態引数による実装 継続による実装
  • 55. 各ライブラリでの実装状況 seq/list/option のいずれかに対するコンピュ テーション式が return をどう扱うか 対象ライブラリ FSharpx ExtCore FSharpPlus Basis.Core 2014 年 7 月 21 日時点
  • 56. 各ライブラリでの実装状況 . 検証用コード例 .. . 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 的なものが返るか
  • 57. 各ライブラリでの実装状況 . 検証用コード例展開 .. . 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)))))
  • 58. FSharpx コンパイルできない
  • 59. 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
  • 60. ExtCore コンパイルできない
  • 61. ExtCore Zero の実装が駄目 . ExtCore の Zero の実装 .. . member inline __.Zero () : unit option = Some () // TODO : Should this be None? コメント・・・
  • 62. FSharpPlus コンパイルできない While をそもそも提供していない それはそれでアリな選択肢。潔い
  • 63. Basis.Core コンパイルできた falseっぽいものが返った
  • 64. 各ライブラリでの実装状況 比較になりませんでした!
  • 65. yield とreturn の違い再考 コンピュテーション式を正しく実装できてい るライブラリの少なさ yield/return 以前の問題 本当に意味上の違いを与えるべき? F#のコンピュテーション式の表現力を活かすな ら与えるべき Bind と Return くらいしか提供しないなら不要 (FSharpPlus はこちら)
  • 66. コンピュテーション式再考 Yield や Return が継続を受け取ればよかった? 効率を考えると、コンパイル時に解決したほう がいい 現状のままでも実現可能 この柔軟性を活用しない手はないのでは?
  • 67. 方針の提案 ライブラリの性質によって、何を実装するかを分 けて考える モナド/モナドプラス程度の提供でとどめる 場合 より汎用的な計算も行えるようにする場合 モナド以外にコンピュテーション式を使う 場合
  • 68. モナド/モナドプラス提供程度の場合 モナド提供程度 Bind/Return は必須(定義より) ReturnFrom もあると便利 場合によっては、Run を別で提供 モナドに包まれた値を取り出すコンピュテーショ ン式 モナドプラス提供程度 モナド用のメソッドに加え、Zero と Combine Zero は mzero、Combine は mplus に対応 Combine の変換規則により、Delay も必要 member this.Delay(f) = f ()
  • 69. より汎用的なライブラリの場合 機能別にモジュールを分ける Bind/Return 程度を提供するビルダー用モ ジュール Combine も使える高級なビルダー用モジュール Combine は mplus に対応するものではなく、 継続を受け取る版を採用する 必然的に、Delay/Run の実装も必要 member this.Delay(f) = f member this.Run(f) = f () Zero もあった方がいい else なしの if が使えるようになる
  • 70. モナド以外の場合 できるだけやめておいた方が・・・ Combine を提供する場合、yield と return を実 装し分ける カスタムオペレータを使うことも考慮に入 れる
  • 71. 今後の課題 FSharpx/ExtCore にバグ報告 機能別にモジュールを分けたコンピュテー ション式ライブラリの作成 実装したビルダー(特に状態引数による実装 と、継続による実装)の意味的な正しさの 検証 提案した方針の啓蒙
  • 72. おわり