Nikon the Third
Nikon the Third

Reputation: 2831

Code breaks when moving recursive types around

During refactoring of some code, I noticed a situation where code breaks when moved around:

type ThingBase () = class end

and Functions =
    static member Create<'T when 'T :> ThingBase and 'T : (new : Unit -> 'T)> () = new 'T ()

and Thing () =
    inherit ThingBase ()
    static member Create () = Functions.Create<Thing> ()

// This works, but try moving the Functions type here instead.

If you move the Functions type below the Thing type, the code breaks unexpectedly:

type ThingBase () = class end

and Thing () =
    inherit ThingBase ()
    static member Create () = Functions.Create<Thing> ()
//                            ^^^^^^^^^^^^^^^^^^^^^^^^^^
// This construct causes code to be less generic than indicated by the
// type annotations. The type variable 'T has been constrained to be
// type 'Thing'.

and Functions =
    static member Create<'T when 'T :> ThingBase and 'T : (new : Unit -> 'T)> () = new 'T ()
//                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// The type ''T' does not match the type 'Thing'.

And no matter what I try, I cannot get this to typecheck. Why is the type inference so stubborn and refusing to generalize the Create method.

By the way, I also attempted F# 4.1 module rec and it also doesn't matter if Create is a function in a module either.

Any insights? To me it seems this should be something the compiler shouldn't have any troubles with.

Upvotes: 2

Views: 85

Answers (4)

Fyodor Soikin
Fyodor Soikin

Reputation: 80744

The below is incorrect. Apparently recursive definitions get two passes of type checking - once for signatures, then for implementations. I'm leaving the answer here anyway, just for reference.

Original answer

Type inference works left to right, in one pass. Once it encountered a call to Functions.Create, it has decided what the signature has to be, and later augmentations can't change that.

It's the same reason that xs |> Seq.map (fun x -> x.Foo) works, but Seq.map (fun x -> x.Foo) xs doesn't: in the first instance the type of x is known from the previous encounter of xs, and in the second instance it's not known, because xs hasn't been encountered yet.

P. S. You don't actually need a recursive group there. There are no recursive dependencies between your types.

Upvotes: 0

Brian
Brian

Reputation: 118865

It will compile if you do this

static member Create<'T when 'T :> ThingBase 
         and 'T : (new : Unit -> 'T)> () : 'T = new 'T ()
//                                       ^^^^          

where the return type is explicitly stated.

Recursive definitions are typechecked left-to-right in two passes; first function/method signatures, then bodies. You need the body (or an explicit return type annotation) to get the result type, so you either need the body to come first, or else the annotation so that it gets solved in the first of the two passes.

Upvotes: 6

sgtz
sgtz

Reputation: 9009

If you want to keep moving forward with that pattern, here's how to do it. I'm assuming you want some kind of factory pattern embedded in the base.

Incidentally, when @Fyodor says left-to-right, this also means top-down. So... you may be fighting against this, too, even though the and functionality should logically, be working. I also agree re: flatter hierarchies, but sometimes, we don't get to have the luxury of choice for various reasons.

type ThingBase () = class end

and Thing () =
    inherit ThingBase ()

and Functions() =
    static member Create<'T when 'T :> ThingBase and 'T : (new : Unit -> 'T)> () = new 'T ()

and Thing with
    static member Create () = Functions.Create<Thing> ()

// typically, I'd prefer

//type Thing with
//    static member Create () = Functions.Create<Thing> ()

// or

//module Thing = 
//  let Create() = Functions.Create<Thing> ()

references:

Upvotes: 1

kaefer
kaefer

Reputation: 5741

I have no idea why the compiler over-constrains the type parameter of the Create method when you move it up. A work-around could be an intrinsic type extension, so you can split the type definition into multiple sections. Which can help to avoid recursive dependencies.

type ThingBase () = class end
type Thing () =
    inherit ThingBase ()
type Functions =
    static member Create<'T when 'T :> ThingBase and 'T : (new : Unit -> 'T)> () =
         new 'T ()
type Thing with
    static member Create () = Functions.Create<Thing> ()

Upvotes: 1

Related Questions