Basics 23 - Types (Part 2) - Extending Types

Learning-FSharp/Ch23-ExtendingTypes/Program.fs

Check out all the comments that are placed in the source file.

Extending Your Types

Here's an example of a discriminated union with static and instance members:

// Discriminated union of Int, String
type IntOrString =
    | I of int
    | S of string

    // Static member
    static member CreateI i = I i

    // Static member
    static member CreateS s = S s

    // Instance member
    member this.ToStringValue() =
        match this with
        | I i -> $"Current value is Int {i}"
        | S s -> $"Current value is String {s}"

    // + operator for IntOrString
    static member (+)(x, y) =
        match x, y with
        | I i1, I i2 -> (i1 + i2).ToString()
        | S s1, S s2 -> s1 + s2
        | I i, S s -> $"{i}{s}"
        | S s, I i -> $"{s}{i}"

Static or instance members can be created for records the same way.

Adding Members To Existing Modules

// Adding member to Result module
module Result =
    // toStringValue becomes a member of Result module
    // Like Result.map and Result.bind, you can call Result.toStringValue
    let toStringValue res =
        match res with
        | Ok ok -> $"Current value is Ok {ok}"
        | Error err -> $"Current value is Error {err}"

This technique is typically used to add members to existing modules such as List, Seq, Array, Option, Result etc. Adding members to these existing modules ensure naming consistency. For instance, if you are adding apply function for option type, it is better accessed as Option.apply instead of SomeOtherModule.apply.

Optional Type Extensions

// Optional type extensions for Result<'T, 'TError>
type Result<'T, 'TError> with

    // ToStringValue becomes an optional type extension
    // for Result<'T, 'TError>
    // <instance>.ToStringValue()
    member this.ToStringValue() = Result.toStringValue this

Here's how to add optional type extensions. Optional type extensions become instance members.

C#-Style Extensions

// C#-style Extension methos
// Notice the type level attribute
[<Extension>]
type MyExtensions =
    // Notice the member level attribute
    // ToStringValueExtension1 becomes a C#-style extension method
    [<Extension>]
    static member ToStringValueExtension1 this = Result.toStringValue this

    // Same as previous member, ToStringValueExtension1
    // Single paramemer function can be created with or without () for params
    [<Extension>]
    static member ToStringValueExtension2(this) = Result.toStringValue this

    // Multi-parameter function must use () for parameters
    [<Extension>]
    static member ToStringValueExtension3(this, append) =
        (Result.toStringValue this) + append

    // This member won't appear as an extension...
    // Multi-parameter function must use () for parameters.
    [<Extension>]
    static member ToStringValueExtension4 this append =
        (Result.toStringValue this) + append

Here's how to add C#-style extensions. C#-style extensions become instance members.

Optional Type Extensions Vs. C#-Style Extensions

Looking at the examples you may wonder what's the difference between optional type extensions and C#-style extensions. There are various minor differences, however, the two major differences are:

  1. Optional type extensions are available in F# only. You can't use optional type extensions outside F#.

  2. Generic constraints. Optional type extensions don't have extensive support for generic constraints. When generic constraints are required, you'll have to use C#-style extensions.

If you have reached so far, congratulations.

Keep reading!