The design suggestion Implicit yields has been marked "approved in principle". This RFC covers the detailed proposal for this suggestion.
- Approved in principle
- Discussion
- Implementation
This allows implicit yield
in list, array, sequence and those computation expressions supporting Yield/Combine/Zero/Delay.
This makes F# a nicer "templating" language. This applies especially for "views" in F# frameworks such as Fable and Fabulous.
The feature uses a type directed rule to detect side-effect statements such as printfn
inside a computation expression,
and is subject to some possible corner-case backwards-compat concerns, discussed below.
Example:
let view onLogout (model:Model) =
div [ centerStyle "row" ] [
yield viewLink Page.Home "Home"
if model <> None then
yield viewLink Page.WishList "Wishlist"
if model = None then
yield viewLink Page.Login "Login"
else
yield buttonLink "logout" onLogout [ str "Logout" ]
]
becomes
let view onLogout (model:Model) =
div [ centerStyle "row" ] [
viewLink Page.Home "Home"
if model <> None then
viewLink Page.WishList "Wishlist"
if model = None then
viewLink Page.Login "Login"
else
buttonLink "logout" onLogout [ str "Logout" ]
]
This has a benefit that some irregularities in incrementally adjusting F# code are ironed out. For example if you start with
[ "Monday"
"Tuesday"
"Wednesday"
"Thursday"
"Friday"
"Saturday"
"Sunday"]
and want to make the last two conditional, then with the feature on you just do this:
[ "Monday"
"Tuesday"
"Wednesday"
"Thursday"
"Friday"
if includeWeekend then
"Saturday"
"Sunday"]
Without implicit yields you have to scatter yield
across the expression:
[ yield "Monday"
yield "Tuesday"
yield "Wednesday"
yield "Thursday"
yield "Friday"
if includeWeekend then
yield "Saturday"
yield "Sunday"]
Implicit yields are activated for
-
List, array and sequence expressions that have no explicit yield (i.e. you can't mix implicit and explicit yields). Using
yield!
is allowed. -
Computation expressions that have no explicit
yield
and where the builder supports the necessary methods for yielding, i.e.Yield
,Combine
,Delay
andZero
.
When implicit yields are activated,
-
for a construct
expr1; expr2
in a computation,expr1
is checked without an expected type (as today). If the resulting type of the expression unifies withunit
without warning, thenexpr1
is interpreted as a sequential expression. Otherwise, it is interpreted as an implicit yield. -
similarly, for non-computation construct
expr
in a leaf position of a computation expression,expr
is checked without an expected type (as today). If the resulting type of the expression unifies withunit
without warning, thenexpr
is interpreted as the expression followed by yielding no results. Otherwise, it is interpreted as an implicit yield of a singleton result.
There is no corresponding "implicit return".
Some existing F# code generates a warning when values are ignored. For example, currently:
[ 1; yield 2 ]
correctly gives
[ 1; yield 2 ]
------^
stdin(1,7): warning FS0020: The result of this expression has type 'int' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'.
Because an explicit yield is present, this continues to generate a warning saying the 1
is ignored and
discarded (the expression is treated like a statement).
There is still a (presumably very rare) backwards compat concern for cases where values are currently
being ignored/discarded and the list/array/sequence/computations only uses yield!
. For example
[ doSomethingThatReturnsAValueButCurrentlyDiscardsIt(); yield! someThingsToYield() ]
In this case, implicit yields are activated because there is no explicit yield
. However the
expression doSomethingThatReturnsAValueButCurrentlyDiscardsIt()
would now be interpreted as a yield. This is
likely to give rise to a type error (it's unlikely that the function returns the same element type as someThingsToYield()
),
but if there is no type error it will yield an additional element.
The initial working assumption is that such cases will be extraordinarily rare - given that the code reports a warning today.
For this reason, in the balance it seems ok to change the interpretation of these cases, subject to
a /langversion:5.0
flag.
F# fixed-list syntax can be used to generate unit values:
let xs = [ (); () ]
Explicit yield syntax can likewise be used:
let xs = [ yield (); yield () ] // result is [ (); () ]
When match
is used in a computed list expression, with or without explicit yields, the syntax -> ()
is used to indicate "don't yield anything on this branch". For example
let f x =
[ yield 1
match x with
| 1 -> ()
| 2 -> yield 2 ]
This also applies to unit-generating lists:
let f x =
[ yield ()
match x with
| 1 -> ()
| 2 -> yield () ]
f 1 // generates [()]
f 2 // generates [(); ()]
Additionally, unit-valued sequential statements can occur, with explicit yields these do not generate values:
let f x =
[ yield ()
match x with
| 1 ->
printfn "nothing"
()
| 2 -> yield () ]
f 1 // generates [()]
f 2 // generates [(); ()]
When yields are implicit, these cases are less obvious. In general, implicit yields are not recommended for use with unit-generating lists. No warning is given to this effect (though one may be added in later releases). To understand what happens if implicit yields are used with unit-generating lists, the rules to remember are
-> ()
still generates no values- Given a sequential
expr1; expr2
, if the type ofexpr1
unifies withunit
then no values are produced
Thus this makes it essentially impossible to actually generate unit-values from lists that contain control constructs and which attempt to use implicit yields:
let f x : unit list =
[ ()
match x with
| 1 ->
printfn "nothing"
()
| 2 ->
()
| _ ->
()
()
]
f 1 // generates []
f 2 // generates []
f 3 // generates []
From the results, you can see that when implicit yields and control constructs are used, the interpretation of unit-typed elements of the control structure is "execute and ignore" - that is, unit-typed expressions are treated as statements rather than expressions-with-yield-of-value-effect. Hence explicit yields should always be used when generating unit results. Thus there is a mixed-interpretation of "control" constructs in the F# syntax.
In practice, generating unit-values computationally seems not to arise, and if it does the option of using explicit yields for clarity is available.
When yields are implicit, in some cases expressions in computed list expressions which might appear to be yields can be given "statement" interpretation instead of "yield" interpretation, giving rise to computed list expressions that yield nothing. Any useful code that does this will almost always give a later type checking error. However that error can be hard to understand.
For example, consider this code, transforming a tree labelled with integers to a tree labelled with strings:
type Tree1 = Node1 of int * Tree1 list
type Tree2 = Node2 of string * Tree2 list
let rec generateThing (Node1 (n, children)) =
let things =
[ for child in children do
generateThing child ]
Node2 (string n, things)
Here the call to generateThing
in the definition of things
is given statement interpretation because its return type (otherwise unknown at that point) unifies with unit
, and an error is reported at the end of the function.
The solution here is to annotate the return type or add an explicit yield
, e.g.
let rec generateThing (Node1 (n, children)) =
let things =
[ for child in children do
yield generateThing child ]
Node2 (string n, things)
While sound, this can be confusing, which is why it's being called out here. See dotnet/fsharp#12194
See above
-
This feature may cause confusion about what is a
yield
and what is a side-effecting operation -
This feature still requires the use of
yield!
and the loss of symmetry betweenyield
andyield!
may make the use of these features confusing for beginners.Response: belief is that
yield!
is rare, and the benefits of clarity from implicit yield are greater than the potential for loss of symmetry. -
The use of a type-directed rule may cause problems
-
The decision to allow implicit yields only when there is no explicit yield means adding a single
yield
to a computation expression may cause a substantial "non-incremental" change in the interpretation and checking of the construct. -
The backwards compat corner cases mean that a
/langversion:5.0
flag is needed. Adding this flag is plan-of-record but it still needs to be done, and must be done carefully.
The main alternative is "don't do this" and continue to require explicit yield
This is a non-breaking change.
TBD