0
yield とreturn の話
bleis-tift
July 27, 2014
自己紹介
id:bleis-tift / @bleis
なごやではたらくゆるふわ F#er
静的型付きの関数型言語が好き
話すこと
第一部:コンピュテーション式
第二部:yield と return の違い ∼考察編∼
第三部:yield と return の違い ∼実装編∼
第四部:あるべき論とまとめ
コンピュテーション式を使う側の話ではなく、
作る側の話をしま...
第一部:コンピュテーション式
コンピュテーション式とは
通常の F#の文法を拡張し、カスタマイズポイ
ントを提供した式
F#の文法に似た文法に、独自の処理を差し込
める
.
F#の文法
..
.
let someFunc a b =
let x = f a
let y = ...
コンピュテーション式でできることの例
option に対する match のネストを取り除く
状態変数の受け渡しを隠す
非同期処理における関数のネストを取り除く
などなど
ただし今回はここら辺はすっ飛ばすので注意
実現方法
F#では、コンピュテーション式は単なる式変
形の規則群によって実現
特定の interface を実装するなどの必要がない
式を変形した結果がコンパイル可能であるかど
うかが重要
.
.
コンピュテーション式の文法
変換規則
通常の文...
表記
ゴシック体 F#コード。例えば、fun x -> x
明朝体 F#コードの中で別の何かに置き換わ
る部分。例えば、cexpr
イタリック F#コードに関わらない部分(変換に
関わる部分)。例えば、T(e, C)
変換規則(最外部)
最初はコンピュテーション式の一番外側の部分
です。
.
. builder-expr { cexpr }
これは、次のように変換されます。
.
. let b = builder-expr in {| cexpr |}
b ...
builder-expr
単なる式(別名「ビルダー」)
ビルダー自体は 1 つのコンピュテーション式
につき 1 回しか評価されない
ビルダーの型に変換で呼び出されるメソッド
を定義する
メソッドはすべてインスタンスメソッド
{| ... |}
括弧の中に含まれる式をコア言語に変換する
{| cexpr |} であれば、cexpr を変換する
どう変換するかは、以降の変換規則参照
cexpr
変換対象となる一番外側のコンピュテーショ
ン式
これ以外のコンピュテーション式は、ce と
表現
cexpr は必要があれば Delay 変換、Quote 変
換、Run 変換が行われる
変換規則の表現
変換規則は、T 表記によって記述する
.
T 表記
..
.T(e, C)
e:変換されるコンピュテーション式
C:変換済みのコンテキスト情報
e にマッチする変換規則を探し、変換していく
{| cexpr |}のT 表記による表現(T 表現)
.
{| cexpr |} の T 表現
..
. {| cexpr |} ≡ T(cexpr, λv.v)
λv.v の部分は、無名関数
ドット以前が引数
ドット以降が本体
最終的に、v...
return の変換規則
.
return の変換規則
..
. T(return e, C) = C(b.Return(e))
cexpr が return 42 だった場合の変換は、
.
return の変換例
..
.
T(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 の変換例
..
.
...
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 th...
ce1; ce2 の変換規則
.
変換規則
..
.
{| cexpr |} ≡ T(cexpr, λv.v)
T(return e, C) = C(b.Return(e))
T(ce1; ce2, C) = C(b.Combine({| ce...
while の変換規則
.
変換規則
..
.
{| cexpr |} ≡ T(cexpr, λv.v)
T(return e, C) = C(b.Return(e))
T(if e then ce, C) = C(if e then {| c...
Fのコンピュテーション式の特徴
F#のコンピュテーション式は、
Haskell の do 式
Scala の for 式
C#のクエリ式
などと同じような仕組み
違うのは、コア言語と同等以上の表現力を持ち得
る点(ループ構文や例外処理なども使え...
第二部:yieldとreturnの違い
∼考察編∼
yield とreturn の変換規則
.
変換規則
..
.
T(yield e, C) = C(b.Yield(e))
T(return e, C) = C(b.Return(e))
メソッドが違うだけ・・・
今回の主題:
なぜコード上での...
yield とreturn を使い分ける?
yieldっぽいものには yield を使い、returnっぽ
いものには return を使う・・・?
コレクションっぽいものには yield を使い、そ
うでないっぽいものには return を使...
yield とreturn の違いを考える
辞書を引いてみる
yield 生み出す。produce/provide
return 戻す。give back
return はその後の処理を実行しないようにすべき
モナドの return?知りません...
yield とreturn の違い
.
yield
..
.
list {
yield 1
printfn done
}
.
return
..
.
list {
return 1
printfn done
}
doneが出力されるべきかどうか
Cではどうか?
return
IET
yield return
yield break
クエリ式
select
目指すのは、IETの yield return と yield break の
ようなもの
seq 式
return に非対応
C#での yield break に相当する操作が困難
よし、seq をコンピュテーション式で再実装して
みよう!
第三部:yieldとreturnの違い
∼実装編∼
実装する上での問題点
yield も return も、変換規則が同じ・・・
実装案1
return が処理を打ち切る、という点に注目し
てみる
return したらそのコンピュテーション式を抜
け、値を返す必要がある
Return メソッドで返したい値を含む例外を投
げ、Run で捕捉すればいい!
例外による実装
.
ビルダー
..
.
type ReturnExn'T(xs: 'T seq) =
inherit System.Exception()
member this.Value = xs
type SeqBuilder'T() =...
例外による実装
.
使用例
..
.
 seq2 { yield 1; yield 2 };;
val it : seqint = seq [1; 2]
 seq2 { return 1; return 2 };;
val it : seqin...
例外による実装
Scala の一部の return や、break でも例外を
使っている
分かりやすいように見える
だがしかし!
例外による実装の問題点
.
ダメな例
..
.
 seq2 { yield 1; return 2; return 3 };;
val it : seqint = seq [2]
yield が C#の yield return、return ...
改良版
.
Combine でも ReturnExn を捕捉
..
.
type SeqBuilder'T() =
member this.Yield(x: 'T) = Seq.singleton x
member this.Return(x:...
例外による実装
try-with を提供する場合は、ReturnExn を捕捉
して reraise する必要がある
結局そんなに分かりやすい実装にはならない
例外をフロー制御に使うことに対する抵抗感
やりたいことは実現できた
実装案2
その後の処理を続ける/続けないが判定でき
ればいい
「その後の処理」を関数として受け取るメ
ソッドでその関数を呼び出すか判定を入れる
状態変数による実装
.
ビルダー
..
.
type SeqBuilder() =
let mutable isExit = false
member this.Yield(x) = Seq.singleton x
member this.Re...
状態変数による実装
.
使用例
..
.
 seq2 { yield 1; yield 2 };;
val it : seqint = seq [1; 2]
 seq2 { return 1; return 2 };;
val it : seq...
状態変数による実装
単純
分かりやすいように見える
だがしかし!
状態変数による実装の問題点
ビルダーが状態を持っている
マルチスレッド等で同時に同じビルダーのイ
ンスタンス(seq2)を使うと・・・
.
.
Thread A
seq2 {
yield 1
; // Combine
yield 2 // oo...
改良版
.
ビルダー
..
.
type SeqBuilder() =
(* 省略 *)
let seq2 () = SeqBuilder()
.
使用例
..
.
 seq2 () { yield 1; yield 2 };;
val it ...
状態変数による実装
ビルダーのインスタンスを毎回作る
ユーザがインスタンスを共有することは禁止
できない
毎回関数呼び出しするのは面倒
実用には耐えない・・・
実装案3
状態変数による実装は、ビルダーのインスタ
ンスに保持しているのが問題
引数で持ちまわせばいいじゃない!
内部で状態を引数で引き回し、Run ではがす
Combine で状態が Break だったら後続処理を実行
しない
状態引数による実装
.
ビルダー
..
.
type FlowControl = Break | Continue
type SeqBuilder() =
member this.Yield(x) = Seq.singleton x, Cont...
状態引数による実装
.
使用例
..
.
 seq2 { yield 1; yield 2 };;
val it : seqint = seq [1; 2]
 seq2 { return 1; return 2 };;
val it : seq...
状態引数による実装
yield と return の対称性が明確になった
通常の実装よりもかなり複雑
いいのでは?
yield とreturn の実装比較
.
例外による実装
..
.
member this.Yield(x: 'T) = Seq.singleton x
member this.Return(x: 'T) =
raise (ReturnExn...
実装案4
例外による実装では、処理を打ち切るために
例外を使った
これは、継続を捨てることと同義
yield の場合は継続を呼び出す
return の場合は継続を捨てる
継続渡しによる実装
.
ビルダー
..
.
type SeqBuilder() =
member this.Yield(x) = fun k - k (Seq.singleton x)
member this.Return(x) = fun _...
継続渡しによる実装
yield と return の対称性が明確
コードは短いが、複雑(Bind を実装してない
詐欺)
これは状態引数版もだけど・・・
実装の速度比較
各実装で 10 万回 yield してみた
ビルダー 時間
return しない実装 20.5ms
例外による実装 20.5ms
状態変数による実装 20.7ms
状態引数による実装 21.2ms
継続による実装 22.6ms
...
第四部:あるべき論とまとめ
ここまで
コンピュテーション式は表現力が豊富
yield と return は変換規則は同じだが、意味は
違う
標準の seq 式は return に対応していない→再
実装
シーケンス用の yield と return に別の意味を持
たせる...
各ライブラリでの実装状況
seq/list/option のいずれかに対するコンピュ
テーション式が return をどう扱うか
対象ライブラリ
FSharpx
ExtCore
FSharpPlus
Basis.Core
2014 年 7 月 ...
各ライブラリでの実装状況
.
検証用コード例
..
.
let xs = [30; 10; 15; 21; -1; 50]
builder {
let i = ref 0
while !i  xs.Length do
if xs.[!i] = ...
各ライブラリでの実装状況
.
検証用コード例展開
..
.
let b = builder
b.Run(
b.Delay(fun () -
let i = ref 0
b.Combine(
b.While(
(fun () - !i  xs.L...
FSharpx
コンパイルできない
FSharpx
Combine の型が駄目
.
FSharpx の Combine のシグネチャ
..
.'a option * ('a - 'b option) - 'b option
.
エラー箇所の展開
..
.
// 'a option...
ExtCore
コンパイルできない
ExtCore
Zero の実装が駄目
.
ExtCore の Zero の実装
..
.
member inline __.Zero () : unit option =
Some () // TODO : Should this be No...
FSharpPlus
コンパイルできない
While をそもそも提供していない
それはそれでアリな選択肢。潔い
Basis.Core
コンパイルできた
falseっぽいものが返った
各ライブラリでの実装状況
比較になりませんでした!
yield とreturn の違い再考
コンピュテーション式を正しく実装できてい
るライブラリの少なさ
yield/return 以前の問題
本当に意味上の違いを与えるべき?
F#のコンピュテーション式の表現力を活かすな
ら与えるべき
Bind...
コンピュテーション式再考
Yield や Return が継続を受け取ればよかった?
効率を考えると、コンパイル時に解決したほう
がいい
現状のままでも実現可能
この柔軟性を活用しない手はないのでは?
方針の提案
ライブラリの性質によって、何を実装するかを分
けて考える
モナド/モナドプラス程度の提供でとどめる
場合
より汎用的な計算も行えるようにする場合
モナド以外にコンピュテーション式を使う
場合
モナド/モナドプラス提供程度の場合
モナド提供程度
Bind/Return は必須(定義より)
ReturnFrom もあると便利
場合によっては、Run を別で提供
モナドに包まれた値を取り出すコンピュテーショ
ン式
モナドプラス提供程度
モ...
より汎用的なライブラリの場合
機能別にモジュールを分ける
Bind/Return 程度を提供するビルダー用モ
ジュール
Combine も使える高級なビルダー用モジュール
Combine は mplus に対応するものではなく、
継続を受け取る...
モナド以外の場合
できるだけやめておいた方が・・・
Combine を提供する場合、yield と return を実
装し分ける
カスタムオペレータを使うことも考慮に入
れる
今後の課題
FSharpx/ExtCore にバグ報告
機能別にモジュールを分けたコンピュテー
ション式ライブラリの作成
実装したビルダー(特に状態引数による実装
と、継続による実装)の意味的な正しさの
検証
提案した方針の啓蒙
おわり
Upcoming SlideShare
Loading in...5
×

yieldとreturnの話

4,497

Published on

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

Published in: Technology

Transcript of "yieldとreturnの話"

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

    Clipping is a handy way to collect important slides you want to go back to later.

×