2

Inspired by the implementation of the plus function (equivalent to mappend) in FSharpPlus, I decided to copy the pattern and implement a generic function incrementing numbers (it would probably be easier to use LanguagePrimitives, but it's just an exercise).

type NumberExtensions =
    static member Increment (x:float) = x + 1.0    
    static member Increment (x:int) = x + 1
    //etc

let inline increment number =
    let inline call (_:^E) (number:^N) = ((^E or ^N) : (static member Increment: ^N -> ^N) number)
    call (Unchecked.defaultof<NumberExtensions>) number //!

let incrementedFloat = increment 100.0
let incrementedInt = increment 200

It works as expected:

val incrementedFloat : float = 101.0
val incrementedInt : int = 201

Unfortunately, I don't quite get why do we need or ^N in the constraint. As far as I understand, the constraint says:

There's a static method named Increment, which takes an argument of some type (the same type as the number's) and returns another value of the same type. This method must be defined either in ^E or in ^N.

Here I thought: We know that the Increment method will be defined in ^E, so we can safely change (^E or ^N) to just ^E. Unfortunately, the change resulted in the following error in the line marked with !:

A unique overload for method 'Increment' could not be determined based on type information prior to this program point. A type annotation may be needed. Candidates: static member NumberExtensions.Increment: x:float->float, static member NumberExtensions.Increment: x:int->int

Why does adding or ^N to the constraint removes this ambiguity? Does it tell the compiler something else than the possible location of the method?

Gus
  • 25,839
  • 2
  • 51
  • 76
tearvisus
  • 2,013
  • 2
  • 15
  • 32

2 Answers2

2

There's a really good discussion about this over here.

In general, we're talking about how to access a dictionary of operators that relate to a family of types, and that types defined with 'a (ie. prime a) are handled in a stricter way.

As always, the FSharp source code is a good place to look. See:

let inline GenericZero< ^T when ^T : (static member Zero : ^T) > : ^T =

In the discussion above, see Jon Harrop's comment about the impact of the class system on Haskell performance. I can't comment about F#'s future in this regard, but, having an explicit style default comes with certain advantages (and trade-offs).

It can be useful at times like these to be patient with the quirks to see if the defaults actually help you. Then work out what conclusions you can draw from this experience. This process might take 6+ months, though. When people get opinionated, perhaps there's a good reason for it. Sometimes it's hard to realise what the set of benefits will be, should you adopt a practice, ahead of time. At the end of all that, you still don't have to agree. Your case could demand otherwise.

sgtz
  • 8,849
  • 9
  • 51
  • 91
1

The main expected effect of the right side of the or is to look for an implementation in the type of the argument itself.

This will allow for instances the users of your library to create types that implement your generic method. Here's an example:

type MyNumber = MyNumber of int with
    static member Increment (MyNumber x) = MyNumber (x + 1)

let incrementedMyNumber = increment (MyNumber 300)

So, the left side is needed for implementations of existing types (typically primitive types as in your example) and the right side for types later defined (typically custom types).

Having said that, you can use this mechanism the straight forward way, which is only with the right side and the generic function will still compile and the example I just gave you will work (but not yours), actually that's the typical use of member constraint, but the opposite is not true, if you remove the right side the compiler has all the information to resolve the type at the definition, I mean it knows that ^E will always be NumberExtensions so it substitutes the type and tries to do overload resolution, and it fails as the error message tells you.

Contrary to the other answer, I don't think that looking at the FSharp source code is of any help here, on the contrary, it might be confusing since it uses a different thechnique to achieve the same: 'simulated members' which pretends that existing types have an implementation of the method but this is only allowed in the FSharp compiler code itself.

Gus
  • 25,839
  • 2
  • 51
  • 76
  • "it knows that `N` [I think you mean `E`] will be always `NumberExtensions` so it substitutes the type and tries to do overload resolution, and it fails" - So why does adding `or N` removes this ambiguity? The compiler still needs to decide which of the two `Increment` overloads to use. How can adding information that there may be some other implementation satisfying the constraints in type `N` be helpful? – tearvisus Jun 13 '17 at 16:27
  • @tearvisus Yes, I meant ``E``, fixed. Adding ``N`` doesn't remove the ambiguity, on the contrary it increases the ambiguity to a level where the compiler gives up, kind of saying "ok, since I don't have enough information I will resolve it at the call site" – Gus Jun 13 '17 at 19:59