F# 4.7 was recently released and includes a number of useful additions to the language. Probably the most interesting one is the ability to omit the yield
keyword from sequence, array and list expressions (among other places).
Basic use of implicit yield
As an example, we upgraded the Farmer project to .NET Core 3.0 (which includes F#4.7) and this pull request (PR) illustrates a useful "before/after" view of the changes that were made possible. Here's a real-world snippet taken from that PR that illustrates the basic change:
Before:
AppSettings = [
yield "AzureWebJobsStorage", Storage.buildKey fns.StorageAccountName.ResourceName
yield "AzureWebJobsDashboard", Storage.buildKey fns.StorageAccountName.ResourceName
// other code elided...
if fns.OperatingSystem = Windows then
yield "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", Storage.buildKey fns.StorageAccountName.ResourceName
yield "WEBSITE_CONTENTSHARE", fns.Name.Value.ToLower()
]
After:
AppSettings = [
"AzureWebJobsStorage", Storage.buildKey fns.StorageAccountName.ResourceName
"AzureWebJobsDashboard", Storage.buildKey fns.StorageAccountName.ResourceName
// other code elided...
if fns.OperatingSystem = Windows then
"WEBSITE_CONTENTAZUREFILECONNECTIONSTRING", Storage.buildKey fns.StorageAccountName.ResourceName
"WEBSITE_CONTENTSHARE", fns.Name.Value.ToLower()
]
Note that pre-F#4.6, the yield
keyword was only actually required in comprehensions for lists and arrays when you have conditional expressions and imperative statements within them, as in the above snippet (sequences are slightly more restrictive).
Using with match expressions
You can also do the same with match
expressions, although unlike
, you will still have to provide a default branch which simply returns unit.if .. then
AppSettings = [
match fns.AppInsightsName with
| Some (External resourceName)
| Some (AutomaticallyCreated resourceName) ->
"APPINSIGHTS_INSTRUMENTATIONKEY", Ai.instrumentationKey resourceName // implicit yield
| Some AutomaticPlaceholder
| None ->
() // explicit "no-op"
]
This does not sit quite as well with me, as you now have one implicit and one explicit branch. Perhaps in the future an implicit match
expression will also be allowed (this could also be useful outside of yield
to bring up to parity with if .. then
expresions.)
Combining with nested collections
You can also combine yield!
(which yields a "nested" collection) and implicit yield
:
Dependencies = [
yield! fns.Dependencies
fns.ServicePlanName // implicit yield
fns.StorageAccountName.ResourceName // implicit yield
]
This is also useful, as the alternative would be an explict "unwrapping" with for .. in
:
Dependencies = [
for dep in fns.Dependencies do dep // implicit yield of every item in fns.Dependencies
fns.ServicePlanName
fns.StorageAccountName.ResourceName
]
However, again, mixing and matching explicit yield!
and implicit yields does not feel quite consistent, even though it is succinct and in practice works well.
Using yield instead of collect and concat
An attractive (and unexpected) benefit of implicit yield is the ability to replace use of functions such as collect
and concat
with implicit yields. This is especially useful if you have cases where you're conditionally returning single items and other times lists, and wish to unify the result:
Before
Resources =
state.Resources
|> List.collect(function
| :? StorageAccountConfig as config ->
// Must wrap this single resource in a list to unify with next branch...
[ StorageAccount (Converters.storage state.Location config) ]
| :? WebAppConfig as config ->
// Return three resources from this branch...
let outputs = Converters.webApp state.Location config
[ yield WebApp outputs.WebApp
yield ServerFarm outputs.ServerFarm
match outputs.Ai with (Some ai) -> yield AppInsights ai | None -> () ]
// other code elided...
]
After
Resources = [
for resource in state.Resources do
match resource with
| :? StorageAccountConfig as config ->
// no need to wrap in a list any more...
StorageAccount (Converters.storage state.Location config)
| :? WebAppConfig as config ->
let outputs = Converters.webApp state.Location config
WebApp outputs.WebApp
ServerFarm outputs.ServerFarm
match outputs.Ai with (Some ai) -> AppInsights ai | None -> () ]
// other code elided...
]
Of course, this was also possible before F#4.7 but required the explicit use of the yield
keyword. Without that, it becomes somehow more attractive.
Summary
We're already enjoying the conciseness and compact nature of implicit yields, and for SAFE Stack applications this is especially useful on the front-end within the context of the Elmish / MVU pattern where list comprehensions are used everywhere.
I think the biggest distinction with implicit yields is increasing the pervasiveness of the idea that the "explicit" nature of F# is being supplanted with an implicit "shortcut". In other parts of the language, this is either not possible (e.g. conversion of numerics, accessing interface implementations, unification of types on branch expressions etc.) or only available in very specific circumstances (e.g. omission of an else
branch is only permitted if the overall expression evaluates to unit
). Previously, looking at a nested snippet of code, the yield
keyword was a clear signal that you're within a list comprehension. Now you'll simply see a sequence of expressions that are "magically" handled by the compiler.
On the other hand, perhaps this is simply a change to what we're "used to". When our team are giving training courses on F# and SAFE Stack to newcomers to F#, use of the yield
keyword is repeatedly a difficult subject to explain; developers usually don't immediately understand why the keyword is sometimes required and other times not. In this context, removing the need for it lowers the barrier just a tiny bit and makes getting up and running with F# even easier.
Hope you're having (fun -> Ok)
with F#4.7!
Isaac