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:
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() ]
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
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
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:
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... ]
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.
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!