Reputation: 1749
Background:
Given the following two declarations in an F# program:
A
implements the interface Wrapped<int>
B
implements the interface Wrapped<A>
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:
B
should be compatible with the type Wrapped<Wrapped<int>>
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 typeWrapper<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
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