2

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'

Abel
  • 56,041
  • 24
  • 146
  • 247

1 Answers1

5

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.

Gus
  • 25,839
  • 2
  • 51
  • 76
  • So, FSharpPlus, as a compiled library, makes sure that `get_Zero` (and perhaps `get_One`, `op_Multiply`, `op_Add`) are _magically_ found for other types than just the core numeric types? And then the constraint on my type won't fail anymore? – Abel Nov 01 '15 at 19:09
  • In fact it looks for a ``Mempty`` member. The member ``get_Zero`` is used there for generic numbers, which makes more sense, so it solves problems like [this one](http://stackoverflow.com/questions/33403433/f-how-do-i-extend-a-type-with-get-zero-so-i-can-use-an-existing-type-generica). If you use F#+ forget about writing the constraint, it will be generated for you. Just define the member with the proper signature, in this case ``Mempty()``. – Gus Nov 01 '15 at 19:13
  • If I understand your edit correctly, and looking at the F#+ source, the `Unchecked.defaultof<'t>` resolves to the `static member ($) (_:Mempty, _:string) = ""` in the code, provided the first argument is of `Mempty` type (and in the case of an unknown type it results in `null` or zero). I'll probably take your advice and use F#+, but I like to understand it before mindlessly using some libraries. – Abel Nov 01 '15 at 19:28
  • 1
    Not exactly, have a look at the signature generated by ``mempty()``. When you use a binary operator, the static members get automatically inferred in such a way that any of the two types involved are included in the contraint solved. This means it will look into `Mempty` type is there is an overload for the return type, if not it will look for that member in the return type itself. More on this technique explained in my [blog](http://nut-cracker.azurewebsites.net/blog/2011/10/05/inlinefun/) – Gus Nov 01 '15 at 19:34