yield and return
~ Poor English ver. ~
bleis-tift
July 27, 2014
Self introduction
https://twitter.com/bleishttps://github.com/bleis-tift
Yuru-Fuwa F#er in Nagoya
I like static typed functional languages
Agenda
Part1: Computation Expression
Part2:Di�erence of yield and return~considerations~
Part3:Di�erence of yield and return~implementations~
Part4:Conclusion
I will talk about how to implement computationexpression.
Part1: Computation Expression
Computation expression is...
expression that extends normal F# grammer andprovides some customize points
able to de�ne an user de�ne process
.the grammer of F#..
.
let someFunc a b =
let x = f a
let y = g b
x + y
.computation expression..
.
let someFunc a b = builder {
let! x = f a
let! y = g b
return x + y
}
Usages
Remove nests of match expressions for option
Hide parameters for state
Remove nests of function call for async proc
and so on...But I will skip these topics today.
The way of implementation
The computation exprs are implemented bysome translation rules in F#
Needless any interfacesMost important thing is which translated exprs arecompilable
.
.grammer of computation expr
translatenormal grammer
Let's look at some translation rules together!
Notation
Sans-Serif Code of F#. ex) fun x -> x
Serif meta Variables. ex) cexpr
Itaric The part related to the translation. ex)T (e, C)
Translation rule (most outside)
.
. builder-expr { cexpr }
F#compiler translates following:.
. let b = builder-expr in {| cexpr |}
b is a fresh variable.
builder-expr
Just a normal expression
Builder is evaluated only onceDe�ne methods called at runtime into buildertype
All methods are de�ned as instance method
{| ... |}
Translate expr to core language grammer
ex) {| cexpr |} ... translate cexpr
See below for further detais
cexpr
The most outer target of translation
Other computation expr is represented by ce
cexpr is translated by Delay-trans, Quote-trans,Run-trans if necessary
Representation of the translation rules
The translation rules are described by T -notation.T -notation..
.T (e, C)
e:The computation expr that will be translated
C:The context that was translated
Find the translation rule that match e, and translateit
T -notation of {| cexpr |}
.T -notation..
. {| cexpr |} ≡ T (cexpr, λv.v)
λv.v is anonymous functionbefore dot: the parameterafter dot: the function body
v is the translated expression
Function application is done at compile-time(not run-time)
Trans rule for return
.Trans rule..
. T (return e, C) = C(b.Return(e))
if cexpr is "return 42" then:.Example..
.
T (return 42, λv.v)
−→(λv.v)(b.Return(42))
−→b.Return(42)
Complete!
Trans rule for let
.Trans rules..
.
T (return e, C) = C(b.Return(e))
T (let p = e in ce, C) = T (ce, λv.C(let p = e in v))
.Example..
.
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)
Trans rule of if.Trans rules..
.
{| 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())
.Example..
.
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()
Trans rule of ce1; ce2
.Trans rules..
.
{| 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 |})))
.Example..
.
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)))
Trans rule of while.Trans rules..
.
{| 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))))
.Example..
.
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)))
Feature of computation expr
Computation expr is similar to
do notation (Haskell)
for expression (Scala)
query expression (C#)
Di�erence is computation expr has more �exibilitythan core language.
Computation expr is more powerful and friendly!
Part2:Di�erence of yield and return
~considerations~
Trans rules of yield and return
.Trans rules..
.
T (yield e, C) = C(b.Yield(e))
T (return e, C) = C(b.Return(e))
Di�erent point is only method...Today's main theme:
Why exist the same rules?
Use properly...?
use yield for yield-like and use return forreturn-like...?
use yield for collection-like, otherwise usesreturn...?
What's the xxx-like!I want to decide clearly.
Thinking about di�erence between yield
and return
Re�er the dictionary:
yield produce/provide
return give back
"return" should not be continue the followingprocess.Monad's return? I don't know:)
Di�erence between yield and return
.yield..
.
list {
yield 1
printfn "done"
}
.return..
.
list {
return 1
printfn "done"
}
Whether or not to print "done"
The case of C#
returnIE<T>
yield returnyield break
query expressionselect
I want to realize something like "yield return" and"yield break".
seq expression
"return" is not supported
Di�cult for "yield break" like C#
Let's reimplements seq expression by computationexpression!
Part3:Di�erence of yield and return
~implementations~
Problem
The trans rule is same yield and return...
Plan 1
The focus on "return" breaks remained process
Need to return value when called "return"
Throw exception that wraps returning value inReturn method and catch the exception in Runmethod
Impl by exception.Builder..
.
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>() // type function
Impl by exception
.Usage..
.
> seq2 { yield 1; yield 2 };;
val it : seq<int> = seq [1; 2]
> seq2 { return 1; return 2 };;
val it : seq<int> = seq [1]
Yes!
Impl by exception
Scala uses exception for the part of implementsreturn and break
Looks like easy
But!
Problem
.Bad Example..
.
> seq2 { yield 1; return 2; return 3 };;
val it : seq<int> = seq [2]
In C#:.C#..
.
IEnumerable<int> F() {
yield return 1;
yield break 2;
yield break 3; }
It returns the sequencce contains 1 and 2.
Re�ne version.Catch ReturnExn in Combine..
.
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>()
Impl by exception
If provide "try-with", need to catch ReturnExnin try-with and reraise it
Eventually, can't implement clearly
Disinclined for use to exception for control �ow
Could be realized at least
Plan 2
Continue or not continue
Insert the judgement of whether to call the restprocess
impl by state �eld
.Builder..
.
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()
impl by state �eld
.Usage..
.
> 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]
Yes!
impl by state �eld
simple
looks like easy
But!
Problem
builder instance has state
use the same builder instance at the same time....
.
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
Re�ne version
.Builder..
.
type SeqBuilder() =(* ... *)
let seq2 () = SeqBuilder()
.Usage..
.
> 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]
Impl by state �eld
Create the builder instance at every time
Can't forbid that the user share the instance
It's troublesome
Does not stand for practical use...
Plan 3
Problem: state sharingSolution: use the argument
Carry the state by the argument, and unwrap thestate in Run methodThe rest process is not called if the state is"Break" in Combine method
Impl by state arg
.Builder..
.
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()
Impl by state arg
.Usage..
.
> 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]
Yes!
Impl by state arg
Symmetry of the return and yield became clear
The implementation is very complex
Looks like good.
Comparison
.Impl by exception..
.
member this.Yield(x: 'T) = Seq.singleton xmember this.Return(x: 'T) =raise (ReturnExn(Seq.singleton x))
.Impl by state �eld..
.
member this.Yield(x) = Seq.singleton x
member this.Return(x) =isExit <- trueSeq.singleton x
.Impl by state arg..
.member this.Yield(x) = Seq.singleton x, Continuemember this.Return(x) = Seq.singleton x, Break
Plan 4
Impl of exception: use the exception to breakthe rest processIt is same to discard continuation
yield: call continuationreturn: discard continuation
Impl by continuation.Builder..
.
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()
.Usage..
.
> 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]
Impl by continuation
Symmetry of return and yield is clearShortest but complex (and not de�ne the Bindmethod)
The state arg version too
Speed Comparison
Write yield at 100,000 times and execute.
builder timeunsupported return 20.5msby exception 20.5msby state �eld 20.7msby state arg 21.2msby continuation 22.6msseq expr 1.18ms
The di�erence is less.But builer is slower than seq expr in the �rst place.
Part4 : Conclusion
Summary
The computation expression is powerful
"yield" and "return" have the same translationrule but the meaning is di�erent
The seq expression is not supported "return" →reimplementationImplementations:
by exceptionby state �eld (deprecated)by state argby continuation
Impl status of some libraries
Design about "return" ex) seq/list/optionTarget libraries:
FSharpxExtCoreFSharpPlusBasis.Core
As of July 21, 2014
Impl status of some libraries.Benchmark code..
.
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
}
Can compile it
It returns false-like value
Impl status of some libraries
.Expanded benchmark code..
.
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)))))
FSharpx
can't compile...
FSharpx
The type of Combine is bad..Signature of Combine...'a option * ('a -> 'b option) -> 'b option
.Expand of error point..
.
// '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()))
.Correct signature...'a option * (unit -> 'a option) -> 'a option
ExtCore
can't compile...
ExtCore
The impl of Zero is bad..Implementation of Zero..
.member inline __.Zero () : unit option =
Some () // TODO : Should this be None?
comment...
FSharpPlus
can't compile.
Not provide While
Better choice
Basis.Core
can compile.
return false-like value.
Impl status of some libraries
No Game!
Rethink about di�erence yield and return
Very few libraries implement computation exprcorrectly
There is a problem to be solved before yield andreturn
Should we give a semantic di�erence really?Should give if you want to take advantage ofcomputation exprShould not give if you provide only Bind andReturn (like FSharpPlus)
Rethink about computation expression
Should Yield and Return receive continuation?Compile-time translation is e�cient
Can implement yield and return by now rulesI want to take this �exibility
Suggestion of a Policy
The considered separately depending on the librarydesign
Case 1: provide monad/monad plus
Case 2: provide more general computing
Case 3: use computaion expr other than monad
Provide monad/monad plus
Provide monadRequired: Bind/ReturnOptional: ReturnFrom (for convinience)Optional: Run
Provide another builder that unwrap the value
Provide monad plusRequired: Bind/Return/Zero/CombineZero is mzero, Combine is mplusRequired: Delay (depends on trans rule ofCombine)
member this.Delay(f) = f ()
Provide more general computing
Separate the modules by featureBuilder module for providing Bind/ReturnBuilder module for providing Bind/Return/Comine
Combine is not mplus. Combine + Delay ismplus.
Inevitably, required Delay/Runmember this.Delay(f) = f
member this.Run(f) = f ()
Optional: ZeroSupport if-expr without else-clause
Use computaion expr other than monad
I have no comments:)
If provide Combine, think about yield and return
Use CustomOperation if necessary
Tasks
Report the bug to FSharpx and ExtCore
Create a library that is divided the module byfeature
Verify builder
Edi�cation
Thanks.