« Discriminated unions in F# | Main | Codemania: How to not write a for loop »

February 27, 2012

Discriminated unions and interfaces

I wrote earlier about F# discriminated unions, and Chris in comments rightly reminded me that discriminated unions, like C# classes, can have member methods.  While I generally reckon separate functions are more idiomatic than members, there is one case where member methods are invaluable: when you want the discriminated union to implement an interface.

This isn’t as common as C# programmers might expect.  Because discriminated unions generally represent simple containers with minimal behavioural logic, the main interfaces you might think to implement are to do with things like equality and comparison – and F# implements those for you.  (Of course, sometimes you don’t want the default equality or ordering implementations, and that’s another scenario where you need to implement an interface – but in those cases there’s a bit more to it.)  Still, it does come up now and again, particularly with system interfaces or if you’re interoperating with C# code that might be more interface-oriented.

The way you implement an interface on a F# discriminated union is exactly the same as the way you implement it on a F# class: use the interface IWhatever with member… construct.  Let’s take a look.

Suppose we want a discriminated union that we can format for display in different ways, similar to the system DateTime or numeric types.  This capability is represented by the system IFormattable interface.  IFormattable has just one method, ToString, which makes it ideal for short demos.  Here’s a discriminated union that implements IFormattable:

type Booze =
   | SauvignonBlanc of string
   | MethylatedSpirit
   interface IFormattable with
     member this.ToString(format, formatProvider) =
       match this with
       | SauvignonBlanc rgn -> if format = "G" then
                                 "Sauvignon Blanc"
                               elif format = "R" then
                                 sprintf "%s Sauvignon Blanc" rgn
                               else
                                 failwith "unknown format"
       | MethylatedSpirit   -> "Amber Nectar"

There’s two bits to this definition: first, the definition of the discriminated union data type, and second, the interface implementation.  The data type definition – the first three lines – doesn’t contain any surprises.  What’s new is that after defining the various ‘cases’ of the union, we keep going with an interface implementation.  The syntax may look a bit weird if you’re a C# programmer but all this is doing is saying that the type implements IFormattable and here is the implementation of the IFormattable.ToString member.  The implementation itself is standard F# pattern matching and hopefully you can see roughly what it’s doing even if you find pipe characters daunting.

Notice that even though this is a member method we can’t define it separately on each ‘case’ the way we would with a virtual member in C#.  The individual cases of discriminated unions are dumb: everything has to be defined on the union type itself.  (A corollary of this is that interfaces can only be implemented on the union type, not on cases.  You couldn’t implement IDrinkable on the SauvignonBlanc case; you’d have to implement it on the Booze type, and live with the deadly consequences.)

When we put the interface implementation into action, there’s a surprise in store:

> let antifreeze = SauvignonBlanc "Austria";;
val antifreeze : Booze = SauvignonBlanc "Austria"
> antifreeze.ToString("R", CultureInfo.InvariantCulture);;
error FS0501: The member or object constructor 'ToString' takes 0 argument(s) but is here given 2. 
The required signature is 'Object.ToString() : string'. 

In C# terms, F# has gone for an explicit interface implementation instead of C#’s usual implicit interface implementation.  That means the interface member can be accessed only through a reference of the interface type:

> (antifreeze :> IFormattable).ToString("R", CultureInfo.InvariantCulture);;
val it : string = "Austria Sauvignon Blanc" 

You can get around this by defining the ToString() member directly on Booze as a member method, and having IFormattable.ToString() delegate to that member method – this is a common pattern for explicit interface implementation in C#, and it’s idiomatic in F# as well.

Implementing interfaces on discriminated unions isn’t all that common; but if you need to do it, now you know how!

February 27, 2012 in Software | Permalink

TrackBack

TrackBack URL for this entry:
https://www.typepad.com/services/trackback/6a00d8341c5c9b53ef0167630e5c28970b

Listed below are links to weblogs that reference Discriminated unions and interfaces:

Comments