14

I'm trying to call a .NET method accepting a generic IEnumerable<T> from F# using a seq<U> such that U is a subclass of T. This doesn't work the way I expected it would:

With the following simple printer:

let printEm (os: seq<obj>) = 
    for o in os do
        o.ToString() |> printfn "%s"

These are the results I get:

Seq.singleton "Hello World"  |> printEm // error FS0001; 
//Expected seq<string> -> 'a but given seq<string> -> unit

Seq.singleton "Hello World"  :> seq<obj> |> printEm // error FS0193;
//seq<string> incompatible with seq<obj>

Seq.singleton "Hello World"  :?> seq<obj> |> printEm // works!

Seq.singleton 42 :> seq<obj> |> printEm // error FS0193
Seq.singleton 42 :?> seq<obj> |> printEm // runtime InvalidCastException!
//Unable to cast object of type 'mkSeq@541[System.Int32]'
// to type 'System.Collections.Generic.IEnumerable`1[System.Object]'.

Ideally, I'd like the first syntax to work - or something as close to it as possible, with compile time type checking. I don't understand where the compiler's finding a seq<string> -> unit function in that line, but apparently covariance for IEnumerable isn't working and that somehow results in that error message. Using an explicit cast results in a reasonable error message - but it doesn't work either. Using a runtime cast works - but only for strings, ints fail with an exception (nasty).

I'm trying to interoperate with other .NET code; that's why I need specific IEnumerable types.

What's the cleanest and preferably efficient way of casting co- or contravariant interfaces such as IEnumerable in F#?

Eamon Nerbonne
  • 47,023
  • 20
  • 101
  • 166
  • 1
    As desco says, the cleanest solution is to alter (or remove) the type declaration on `os` (if feasible). On an unrelated note, `o.ToString |> printfn "%s"` can be written more concisely as `o |> printfn "%O"`. – kvb Nov 17 '10 at 15:49
  • @kvb I think @Eamon doesn't have problem with `printfn` function. – Przemysław Lewandowski May 29 '13 at 11:20

3 Answers3

12

Unfortunately F# doesn;t support co\contravariance. That's why this

Seq.singleton "Hello World"  :> seq<obj> |> printEm 

doesn't work

You can declare parameter as seq<_>, or limit set of parameter types to some specific family by using flexible types (with hash #) this will fix this scenario:

let printEm (os: seq<_>) = 
for o in os do
    o.ToString() |> printfn "%s"

Seq.singleton "Hello World"  |> printEm 

Considering this lines:

Seq.singleton 42 :> seq<obj> |> printEm // error FS0193
Seq.singleton 42 :?> seq<obj> |> printEm

Variance only works for classes, so similar code will not work in C# too.

You can try casting sequence elements to required type explicity via Seq.cast

desco
  • 16,642
  • 1
  • 45
  • 56
  • 1
    However, in C# the equivalent of `Seq.singleton "Hello World" |> printEm` will work and be statically checked - which is the more interesting case, to me. – Eamon Nerbonne Nov 17 '10 at 12:40
10

Use Seq.cast for this. For example:

Seq.singleton "Hello World"  |> Seq.cast |> printEm

Admittedly, this gives up type safety:

type Animal() = class end
type Dog() = inherit Animal()
type Beagle() = inherit Dog()

let printEm (os: seq<Dog>) =  
    for o in os do 
        o.ToString() |> printfn "%s" 

Seq.singleton (Beagle())  |> Seq.cast |> printEm // ok
Seq.singleton (Animal())  |> Seq.cast |> printEm // kaboom!

but it is expedient.

Alternatively, you can use flexible types:

type Animal() = class end
type Dog() = inherit Animal()
type Beagle() = inherit Dog()

let printEm (os: seq<#Dog>) =  // note #Dog
    for o in os do 
        o.ToString() |> printfn "%s" 

Seq.singleton (Beagle()) |> printEm // ok
Seq.singleton (Animal()) |> printEm // type error

which is just shorthand for the generic "forall types 'a when 'a :> Dog".

And finally, you can always map the upcast, e.g.

let printEm (os: seq<obj>) =  
    for o in os do 
        o.ToString() |> printfn "%s" 

Seq.singleton "Hello" |> Seq.map box |> printEm // ok

where box upcasts to obj.

Brian
  • 117,631
  • 17
  • 236
  • 300
1

You can use Covarsky.

You mark some generic parameters with custom attributes, and it modifies the compiled assemblies to make them co(ntra)variant in the eyes of .NET (you still have to manually cast the types in F# though).