Understanding F# static member constraints (Construct less than generic warning)

371 views Asked by At

This piece of code suggested by another member to allow fmap to work generically with Option and Choice compiles and works perfectly, there is however a warning I am struggling to understand and mend. Final line, under "Functor":

This construct causes the code to be less than generic. The type variable 'b has been constrained to Functor.

type Functor = Functor
    with 
    static member FMap (Functor, mapper : 'T -> 'U, opt : Option<'T>) : Option<'U> =
        Option.map mapper opt
    static member FMap (Functor, mapper : 'T -> 'U, ch : Choice<'T, _>) : Choice<'U, _> =
        match ch with
        |Choice1Of2 v -> Choice1Of2 (mapper v)
        |Choice2Of2 v -> Choice2Of2 v

let inline fmap (f : ^c -> ^d ) (x : ^a) =
    ((^b or ^a) : (static member FMap : ^b * ( ^c -> ^d ) * ^a -> ^e ) (Functor, f, x))

This sort of makes sense as ^b should always be Functor.

I would like to reformulate the code to address this warning but have been unsuccessful.

To that end, why do I need (^b or ^a) rather than just ^a as ^b will always be a Functor. I have been unable to get a compiling version without (^b or ^a) syntax.

Additionally, why is it necessary to pass Functor union case through at all. Again, something I have been unable to understand and work around.

Appreciate any clarity.

1

There are 1 answers

0
Gus On BEST ANSWER

You can solve that warning simply by adding a #nowarn "0064" or by adding an internal call function like this:

let inline fmap (f :'c->'d) (x : 'a) : 'e = 
    let inline call (mthd : ^B, x : ^A) =
        ((^B or ^A) : (static member FMap: _ * (^c -> ^d ) * ^A -> ^e) (mthd, f, x))
    call (Unchecked.defaultof<Functor>, x)

which adds additional type variables that are not solved until the call, this way for the type inference is not obvious that ˆB is ˆa, ˆA is ˆa and ˆB is Functor.

You need ^B or ^A because it needs to look for static methods in the type Functor (for pre-existing types like Option and Choice) but also for future type definitions which are represented by ^A, this allows you to add later in the code something like:

type MyType<'a> = MyType of 'a with
    static member FMap (Functor, mapper, MyType a) = MyType (mapper a)

and still will work with:

> fmap string (MyType 4) ;;
val it : MyType<string> = MyType "4"

Finally it worths mentioning that this technique was pioneered and refined by an experimental project (by observing and trying to mimic the constraints for the + operator) which now is part of the generic modules of F#+, so you can find more complex examples there including "default implementations".