United Kingdom: +44 (0)208 088 8978

Working with implicit yields in F#4.7

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 if .. then, you will still have to provide a default branch which simply returns unit.

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