Today I noticed that the following does not compile:
open System
type MyType() =
member this.Something() =
this.F(3)
this.F("boo")
// ^^^^^
// This expression was expected to have type 'int' but here has type 'string'
member private this.F<'T> (t:'T) =
// ^^
// This type parameter has been used in a way that constrains it to always be 'int'
// This code is less generic than required by its annotations because the explicit type variable 'T' could not be generalized. It was constrained to be 'int'.
Console.WriteLine (t.GetType())
But simply change the declaration order, and there's no problem.
open System
type MyType() =
member private this.F<'T> (t:'T) =
Console.WriteLine (t.GetType())
member this.Something() =
this.F(3)
this.F("boo")
This took me a long while to figure out as I didn't expect declaration order to matter for members of a class. Is this expected behavior?
This is a subtle side-effect of how the F# type inference works. I do not think there is a better workaround for the issue than reordering your definitions.
To provide more background, the members of a class are automatically treated as mutually recursive (meaning that they can call each other). Unlike with multiple types or multiple functions in a module, you do not need to declare this explicitly using the rec
keyword.
However, the issue is not limited to classes. You can get exactly the same behaviour with simple functions. The simplest possible example is:
let rec test() =
f 3 // Warning: causes code to be less generic
f "boo" // Error: string does not match int
and f (arg:'a) = ()
In the opposite order, this works just fine:
let rec f (arg:'a) = ()
and test() =
f 3
f "boo"
The issue is that the type-checker analyzes the code from top to bottom. In the first case, it:
f 3
and infers that f
is of type int -> unit
f "boo"
and reports a type errorf (arg:'a)
and realizes it prematurely used a more specific type than needed (and reports the various warnings).In the second case, it:
f (arg:'a)
and infers that f
is of type 'a -> unit
f 3
and f "boo"
and uses appropriate type instantiationThe main reason for why the type checker cannot be more clever is that the type checker only does "generalization" (i.e. figures out that a function is generic) after it analyzes the whole body of the function (or the whole recursive block). If it encounters a more specific use inside the body, it never gets to this generalization step.
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments