Type extensions

Definition

Members of a type defined outside its main type block.

Each of these members is called augmentation or extension.

3 categories of extension :

  • Intrinsic extension

  • Optional extension

  • Extension methods

Intrinsic extension

Declared in the same file and namespace as the type

Use case: Features available in both companion module and type → E.g. List.length list function and list.Length member

How to implement it following top-down declarations?

1. Implement in type, Redirect module functions to type members → More straightforward

2. Intrinsic extensions: → Declare type "naked", Implement in module, Augment type after → Favor FP style, Transparent for Interop

Example:

namespace Example

type Variant =
    | Num of int
    | Str of string

module Variant =
    let print v =
        match v with
        | Num n -> printf "Num %d" n
        | Str s -> printf "Str %s" s

// Add a member as an extension - see `with` required keyword
type Variant with
    member x.Print() = Variant.print x

Optional extension

Extension defined outside the type module/namespace/assembly

Use cases:

  1. Types we can't modify, for instance coming from a library

  2. Keep types naked - e.g. Elmish MVU pattern

module EnumerableExtensions

open System.Collections.Generic

type IEnumerable<'T> with
    /// Repeat each element of the sequence n times
    member xs.RepeatElements(n: int) =
        seq {
            for x in xs do
                for _ in 1 .. n -> x
        }

// Usage: in another file ---
open EnumerableExtensions

let x = [1..3].RepeatElements(2) |> List.ofSeq
// [1; 1; 2; 2; 3; 3]

Compilation: into static methods → Simplified version of the previous example:

public static class EnumerableExtensions
{
    public static IEnumerable<T> RepeatElements<T>(IEnumerable<T> xs, int n) {...}
}

Another example:

// Person.fs ---
type Person = { First: string; Last: string }

// PersonExtensions.fs ---
module PersonExtensions =
    type Person with
        member this.FullName =
            $"{this.Last.ToUpper()} {this.First}"

// Usage elsewhere ---
open PersonExtensions
let joe = { First = "Joe"; Last = "Dalton" }
let s = joe.FullName  // "DALTON Joe"

Limits

  • Must be declared in a module

  • Not compiled into the type, not visible to Reflection

  • Usage as pseudo-instance members only in F♯ ≠ in C♯: as static methods

Type extension vs virtual methods

☝ Override virtual methods:

  • in the initial type declaration ✅

  • not in a type extension

type Variant = Num of int | Str of string with
    override this.ToString() = ... ✅

module Variant = ...

type Variant with
    override this.ToString() = ... ⚠️
    // Warning FS0060: Override implementations in augmentations are now deprecated...

Type extension vs type alias

Incompatible❗

type i32 = System.Int32

type i32 with
    member this.IsEven = this % 2 = 0
// 💥 Error FS0964: Type abbreviations cannot have augmentations

💡 Solution: use the real type name

type System.Int32 with
    member this.IsEven = this % 2 = 0

Corollary: F♯ tuples such as int * int cannot be augmented, but they can be extended using C♯-style extension methods 📍

Type extension vs Generic type constraints

Extension allowed on generic type except when constraints differ:

open System.Collections.Generic

type IEnumerable<'T> with
//   ~~~~~~~~~~~ 💥 Error FS0957
//   One or more of the declared type parameters for this type extension
//   have a missing or wrong type constraint not matching the original type constraints on 'IEnumerable<_>'
    member this.Sum() = Seq.sum this

// ☝ This constraint comes from `Seq.sum`.

Solution: C♯-style extension method 📍

Extension method (C♯-style)

Static method:

  • Decorated with [<Extension>]

  • In F♯ < 8.0: Defined in class decorated with [<Extension>]

  • Type of 1st argument = extended type (IEnumerable<'T> below)

Example:

open System.Runtime.CompilerServices

[<Extension>] // 💡 Not required anymore since F♯ 8.0
type EnumerableExtensions =
    [<Extension>]
    static member inline Sum(xs: seq<_>) = Seq.sum xs

let x = [1..3].Sum()
//------------------------------
// Output in FSI console (verbose syntax):
type EnumerableExtensions =
  class
    static member
      Sum : xs:seq<^a> -> ^a
              when ^a : (static member ( + ) : ^a * ^a -> ^a)
              and  ^a : (static member get_Zero : -> ^a)
  end
val x : int = 6

C♯ equivalent:

using System.Collections.Generic;

namespace Extensions
{
    public static class EnumerableExtensions
    {
        public static TSum Sum<TItem, TSum>(this IEnumerable<TItem> source) {...}
    }
}

Note: The actual implementations of Sum() in LINQ are different, one per type: int, float... → Source code

Tuples

An extension method can be added to any F♯ tuple:

open System.Runtime.CompilerServices

[<Extension>]
type EnumerableExtensions =
    [<Extension>]
    // Signature : ('a * 'a) -> bool when 'a : equality
    static member inline IsDuplicate((x, y)) = // 👈 Double () required
        x = y

let b1 = (1, 1).IsDuplicate()  // true
let b2 = ("a", "b").IsDuplicate()  // false

Comparison

Feature
Type extension
Extension method

Methods

✅ instance, ✅ static

✅ instance, ❌ static

Properties

✅ instance, ✅ static

Not supported

Constructors

✅ intrinsic, ❌ optional

Not supported

Extend constraints

Not supported

Support SRTP

Limits

Type extensions do not support (sub-typing) polymorphism:

  • Not in the virtual table

  • No virtual, abstract member

  • No override member (but overloads 👌)

Extensions vs C♯ partial class

Feature
Multi-files
Compiled into the type
Any type

C♯ partial class

✅ Yes

✅ Yes

Only partial class

Extension intrinsic

❌ No

✅ Yes

✅ Yes

Extension optional

✅ Yes

❌ No

✅ Yes

Last updated

Was this helpful?