United Kingdom: +44 (0)208 088 8978

Extension members in F#

Isaac shows how to take advantage of extension members on imported types, and why this might be useful.

We're hiring Software Developers

Click here to find out more

C# developers will be familiar with the notion of Extension Methods - a feature introduced in C#3 that allows you to apply static (and typically stateless) methods to an existing type that you may or may not "own". It's really just syntactic sugar, but it can aid with writing Domain Specific Languages (DSLs), such as LINQ's set of extensions on top of IEnumerable - and it can lead to slightly more idiomatic-looking C# code.

F# developers tend to avoid members on types in general and instead prefer separation of data and behaviours into separate entities - records or unions for data, and modules with functions for behaviours:

let ctx : HttpContext = ...
let config = ctx.GetService<IConfiguration>()
let connectionString =
    config // data
    |> ConfigHelpers.getConnectionString "TodoDb" // behaviour

Here, we have a module called ConfigHelpers which has a function on it, getConnectionString whose signature is something like dbName:string -> config:IConfiguration -> connectionString:string i.e. given a config and database name, return back a connection string.

Interestingly, you can almost consider C#-style extension methods to be similar to standard F# functions insofar as you can "pipeline" any data into an F# function (as long as the types match up, of course) - the biggest difference is that the pipelined type is the final argument to an F# function (which is partially applied), whereas in C#, extension methods use the first argument as the "extension" point (indicated by the this keyword).

Extension Members

However, extension members are sometimes useful in F#, especially since you can create extension properties as well as methods in F#! Consider the previous example, which we can simplify as follows:

ctx.Configuration.TodoDbConnectionString

This would be made possible with two extension members:

  • Configuration, which would be an extension property on the HttpContext
  • TodoDbConnectionString, which would be an extension property on IConfiguration

The code for this is not complex:

type HttpContext with
    /// Gets the current IConfiguration from the service locator.
    member this.Configuration = this.GetService<IConfiguration>()

type IConfiguration with
    member this.TodoDbConnectionString =
        match this.GetConnectionString "TodoDb" with
        | null -> failwith "Missing connection string" // simply panic if no connection string is found
        | v -> v

Note that use of this is not mandated for the instance name - you could also use ctx or config as desired.

Summary

Extension members can sometimes be useful, especially for accessing properties that are frequently required - in such cases, the alternative of using pipelines everywhere can quickly become messy when working with nested members.