Vandroiy
Vandroiy

Reputation: 6223

Why is automatic generalization sometimes inferring overly specific types?

Some function usages don't compile when one side of an operator has a known type and the other doesn't. One example is with units of measure:

let inline multiplyWithFive x = 5. * x

type [<Measure>] myUnit
let test = multiplyWithFive 3.<myUnit> // Compiler error

5. * 3.<myUnit> is clearly a valid expression, so this is surprising, especially considering that inline functions generalize maximally in other cases:

let inline multiply a b = a * b
let test = multiply 5. 3.<myUnit> // Valid

This isn't limited to units of measure though. Say, I make a type that supports asymmetric multiplication with a float. It's incompatible with the multiplyWithFive function, which arbitrarily infers its parameter as float:

type BoxFloat =
    { Value : float }

    static member inline (*) (lhs : float, rhs : BoxFloat) =
        { Value = lhs * rhs.Value }

let boxThree = { Value = 3. }

let test2 = multiplyWithFive boxThree // Compiler error

Again, 5. * boxThree is a valid expression. But the automatic generalization doesn't seem to acknowledge that.

I can "fix" the first case with unit-of-measure-aware type annotations, but this restricts the underlying type for no apparent reason. If I actually need a more general function, I don't know how to stop the compiler from making up restrictions. If I explicitly name a generic parameter, it just refuses to keep it generic:

// Warning: This construct causes code to be less generic than indicated...
let inline multiplyWithFive (x : 'T) = 5. * x 

What can I do about this? Is there some way to say that I do want the more generic version?

Upvotes: 4

Views: 94

Answers (1)

Tomas Petricek
Tomas Petricek

Reputation: 243051

To answer your first question, I think the F# compiler does not automatically generalize the generic type to include possible units, so that's why your first example ends up taking float. It gets generalized over unit if you specify a float taking a unit in a type annotation:

let inline multiplyWithFive (x:float<_>) = 5. * x

type [<Measure>] myUnit
multiplyWithFive 3.         // Works fine without units
multiplyWithFive 3.<myUnit> // Works fine with units

As for making this work with any type that supports the multiplication operator - I think the F# compiler is special casing the multiplication operator so that it gives reasonable default behaviour in the typical case where you really just want to work with float or float<_> values. If you define this using explicit member constraint, the warning is quite clear about that:

// warning FS0077: Member constraints with the name 'op_Multiply' 
// are given special status by the F# compiler 
let inline amultiplyWithFive (x:^T) : ^R =
  (^T : (static member (*) : float * ^T -> ^R) (5.0, x))

// no warning since we're requiring operator **
let inline multiplyWithFive (x:^T) : ^R =
  (^T : (static member ( ** ) : float * ^T -> ^R) (5.0, x))

You can probably workaround this by using static member constraints in a more fancy way - there is a trick that lets you define overloaded operators using static memebrs. However, I think that's stretching the mechanism a bit and it will likely have usability consequences in other parts of your code.

Upvotes: 5

Related Questions