snickers10m
snickers10m

Reputation: 1749

F# nested generic types not compatible with implemented type

Background:

Given the following two declarations in an F# program:

We say that type A is compatible with Wrapped<int> and type B is compatible with Wrapped<A> - compatible, to my understanding, meaning that an A can be passed into a function requiring a Wrapped<int>.

Problem:

From my experience with programming, I would expect the following to also be true, given the above two statements:

since B has A as the type parameter where Wrapped<int> should go, and A and Wrapped<int> are compatible.

This is not the case. The following implementation:

type Wrapper<'a> = abstract member Value : 'a

type A =
    | A of int

    interface Wrapper<int> with member this.Value = (let (A num) = this in num)

type B =
    | B of A

    interface Wrapper<A> with member this.Value = (let (B a) = this in a)


let f (x:Wrapper<Wrapper<int>>) =
    x.Value.Value

let testValue = f (B (A 1))

has a compile error on B (A 1) stating

The type B is not compatible with the type Wrapper<Wrapper<int>>

Question:

Since I was able to logically make the compatibility jump, am I doing something wrong while implementing this? Or does F# not have this "nested compatibility" feature, and if that's the case, is there a particular reason for not having it?


There is a workaround to this:

type B =
    | B of A

    interface Wrapper<Wrapper<int>> with member this.Value = (let (B a) = this in a :> Wrapper<int>)

That will remove the compile error, though it feels a little bit wrong. I ask myself "What if I ever write a function to work on Wrapper<A> types? (if I ever add more Wrapper<A> implementers)

Upvotes: 4

Views: 491

Answers (1)

Asti
Asti

Reputation: 12667

The feature you're asking for is covariant types.

Covariance permits a return type which is a subtype rather than that exactly defined by the generic type parameter (not that this is applicable only to interfaces, not concrete types). This allows you to downcast IEnumerable<string> :?> IEnumerable<object> as string :?> object.

Declaration is possible in the other .NET languages. Here's your example in C#:

interface Wrapper<out T> { }
class A : Wrapper<int> { }
class B : Wrapper<A> { }          

var b = new B();
Action<Wrapper<Wrapper<int>>> unwrap = _ => { };
unwrap(b); //compiles

F# does not provide support for declaring covariant types, nor does it coerce types without explicit declaration. The reason for this is mostly that covariance leads to degraded type inferencing.

Covariance in F# is possible with flexible types. Here's an example in F# on the seq type which is defined as IEnumerable<out T>.

let s = [1..10] 
let r =  s |> Seq.map(fun _ -> s)

let print1 (v: seq<seq<int>>) = printfn "%A" v
let print2 (v: seq<#seq<_>>) = printfn "%A" v

print1 r //does not compile
print2 r //compiles

There is a chance you could make this work if the generic parameters were marked covariant and used flexible types. You could have the interface declarations in C# and reference the assembly in F#.

There is also mausch/VariantInterfaces which modifies the assembly based on a naming convention to add covariant / contravariant declarations, so if you had your type declarations in a separate assembly, you could run it in post-build.

Upvotes: 6

Related Questions