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 theHttpContext
TodoDbConnectionString
, which would be an extension property onIConfiguration
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.