Type Extensions

Type extension is a feature that allows you to add members to a previously defined object type. It is also known as augmentation.

In F#, the following type extension features are available:

Intrinsic Type Extension

Example:

namespace DasDocs

type Shape = 
    | Square of int
    | Rectangle of int * int

module Shape = 
    let area shape = 
        match shape with
        | Square side -> side * side
        | Rectangle (length, breadth) -> length * breadth

type Shape with
    member this.Area() = Shape.area this

Advantages of intrinsic type extension:

How compiler sees Intrinsic type extension?

Optional Type Extensions

For example, let's say you add a library DasDocs to your project which has Shape type defined as above. You can extend Shape as follows:

module Geometry

open DasDocs

type Shape with
    member this.Area() = Shape.area this

Another example is extending IEnumerable interface defined in System.Collections.Generic module with a custom method:

open System.Collections.Generic

type IEnumerable<'T> with
    /// Print all elements
    member xs.PrintElements() =
        for x in xs do
            printfn $"{x}"

let nums1to10 = seq { for i in 1..10 do yield i }

nums1to10.PrintElements()
// prints numbers 1 to 10 in a new line

How do compilers see Optional Type Extensions?

Generic limitation when using Intrinsic/Optional Type Extensions

As seen above in the case of extending IEnumerable with PrintElements method, it is possible to extend a generic type. The requirement for extending a generic type is that the constraint of the extension declaration matches the constraint of the declared type.

However, when the constraints differ, the code does not compile:

type IEnumerable<'T> with
    member inline this.Sum() = Seq.sum this

This will give the following compilation error:

error FS0670: This code is not sufficiently generic. The type variable  ^T when  ^T: (static member get_Zero: ->  ^T) and  ^T: (static member (+) :  ^T *  ^T ->  ^T) could not be generalized because it would escape its scope.
error FS0339: The signature and implementation are not compatible because the type parameter in the class/signature has a different compile-time requirement to the one in the member/implementation

To overcome this limitation, we need to use extension methods.

Extension methods

Extension methods are useful when you want to define extensions on a generic type that will constraint the type variable:

// required for IEnumberable
open System.Collections.Generic
// required for [<Extension>]
open System.Runtime.CompilerServices

[<Extension>]
type IEnumerableExtensions =
    [<Extension>]
    static member inline Sum(xs: IEnumerable<'T>) = Seq.sum xs

In F# interactive (FSI), you get the following type signature for Sum method:

type IEnumerableExtensions =
  static member
    inline Sum: xs: IEnumerable< ^T> ->  ^T
                  when  ^T: (static member (+) :  ^T *  ^T ->  ^T) and
                        ^T: (static member get_Zero: ->  ^T)

You can now use this Sum method as before:

let nums1To10 = seq { for i in 1..10 do yield i }

printfn "%d" (nums1To10.Sum())
// prints 55

© 2022 Sumeet Das