Search code examples
genericsf#monadsmonoids

Getting or implementing String.Zero and bool.Zero generically for use with monoids


I am trying to refactor some existing code into a more monodic approach. Existing code contains interfaces IXInterface and numerics like int and bool. The numerics already have Zero by default, the interfaces have it as a property gettor, but bool and string do not. One way out is to wrap bool and string in an interface, but this is cumbersome.

I figured if the F# language manages to extend the types for numerics, perhaps I can do it too for strings and bools for my particular situation.

module MyZero =
    let inline get_zero () : ^a = ((^a) : (static member get_Zero : unit -> ^a)())

    type System.String with
        static member get_Zero() = System.String.Empty

type XR<'T when 'T : (static member get_Zero : unit -> 'T)> =
    | Expression of (SomeObj -> 'T)
    | Action of (int -> 'T)
    | Value of 'T
    | Empty

    member inline this.Execute(x: SomeObj) : 'T =
        match this with
        | Value(v) -> v
        | Expression(ex) -> ex x
        | Action(a) -> a x.GetLocation
        | Empty -> get_zero()

    static member map f x=
        match x with
        | XR.Empty -> XR.Empty
        | XR.Value v -> XR.Value <| f v
        | XR.Action p -> XR.Action <| fun v -> f (p v)
        | XR.Expression e -> XR.Expression <| fun x -> f (e x)

    // etc

The above compiles fine, as long as I don't try to use it with strings or bools:

type WihtBool = XR<int>         // succeeds
type WihtBool = XR<IXInterface> // succeeds
type WihtBool = XR<bool>        // fails 
type WithString = XR<string>    // fails

The error is clear and correct (I have an extension method, which is not recognized for obvious reasons), I just don't know a non-intrusive way to get rid of it:

fails with "the type bool does not support the operator 'get_Zero'
fails with "the type string does not support the operator 'get_Zero'


Solution

  • F# manages to extend numeric types using static optimizations which is a feature that is disabled outside the F# Core Library.

    AFAIK the only way to get a similar mechanism is by using overloads and static member constraints.

    Indeed what you are trying to do it's already implemented in F#+

    #nowarn "3186"
    #r @"FsControl.Core.dll"
    #r @"FSharpPlus.dll"
    
    open FSharpPlus
    
    let x:string = mempty()
    // val x : string = ""
    
    type Boo = Boo with
        static member Mempty() = Boo
    
    let y:Boo = mempty()
    // val y : Boo = Boo
    

    It works on the same principle as the F# math operators where the static constraint can be satisfied by the type of any argument.

    Here's the part of the source code that makes this magic.

    Currently an instance for bool is missing, but you can add an issue suggesting it or a pull request, it will be a one-liner (or two).

    Anyway if you want to capture this functionality try this quick-standalone code:

    type Mempty =
        static member ($) (_:Mempty, _:string) = ""
        static member ($) (_:Mempty, _:bool) = false
    
    let inline mempty() :'t = Unchecked.defaultof<Mempty> $ Unchecked.defaultof<'t>
    
    let x:string = mempty()
    // val x : string = ""
    
    let y:bool = mempty()
    // val y : bool = false
    
    type Boo = Boo with 
        static member ($) (_:Mempty, _:Boo) = Boo
    
    let z:Boo = mempty()
    // val z : Boo = Boo
    

    You can rename Mempty to get_Zero but I think get_Zero is not the best name for a monoid, remember that the number one under multiplication is also a monoid and get_Zero is already used in F# Core libraries for generic numbers.

    But honestly if you are going in this direction I strongly advise you to consider that library since there are many issues you may find when scaling your code already resolved there, you get for free other monoid related functions, like mconcat and mfold and you get nicer signatures on your types.