简体   繁体   中英

Create f# class which implements interface using another interface

Let's say I have the following interface ICreateEnumeratorCollection which uses another interface IEnumerable :

open System.Collections

type ICreateEnumeratorCollection =

     abstract member Enumerators : IEnumerable [] with get, set

I'm having a tough time creating a class which implements this interface in a generic way:

type ExampleClass<'a when 'a :> IEnumerable> (enum : #IEnumerable []) =

     let mutable internalEnum = enum

     interface ICreateEnumeratorCollection with
          member this.Enumerators
              with get ()     = internalEnum
              and set (enum)  = internalEnum <- enum

Trying to instantiate this class is a mess as the compiler is not able to implicitly infer the type implementing IEnumerable.

let myList = Array.singleton ["bla"]

let listDoesNotMatchIEnumerable = 
    ExampleClass
        (
            myList
        )

let cantInferType = 
    ExampleClass 
        (
            myList 
            |> Array.map (fun x -> x :> IEnumerable)
        )

let worksButIsAlotOfOverhead = 
    ExampleClass<string list>
        (
            myList 
            |> Array.map (fun x -> x :> IEnumerable)
        )

Is there a way to define this class in a way which allows the compiler to infer the type using IEnumerable?

Your class signature specifies that the enum parameter is of a generic type - ie it's not just an array of IEnumerable , no. It's an array of some type that implements IEnumerable . That's what the hash sign does.

As far as I understand from your further snippets, this is what you actually wanted: you wanted your class to take an array of any list-ish things as parameter.

But here's the problem: if you have an array of 't where 't implements IEnumerable , it's not at all the same as an array of IEnumerable .

For illustration:

let x : (int list) [] = [||]
let y : IEnumerable [] = x // Compiler error: cannot 

So when you try to return enum from the Enumerators , the return type of Enumerators must be an array of IEnumerable , and since the compiler can't convert array of some unknown 't to an array of IEnumerable , it assumes that 't must itself be IEnumerable . Hence the warning you get.

To fix this, assuming you don't want to change the API, you will have to map and cast each element down to IEnumerable :

          with get ()     = internalEnum |> Array.map (fun x -> x :> _)

But then the next problem you'll see is with set , and this one is more serious. Think about it: what if your class works with, say, lists (which do implement IEnumerable ), but then you try to assign it arrays? Or strings (which are also IEnumerable ). You can't do that: the type of internalEnum doesn't match.

And here's the next question to ask: do you really need your class to be generic? It looks to me like you're not actually using that generic-ness except in the constructor for some syntactic convenience. But syntactic convenience doesn't have to come from a constructor, you can make your class non-generic, and then just have a convenience function for creating it:

type ExampleClass (enum : IEnumerable []) =

     let mutable internalEnum = enum

     interface ICreateEnumeratorCollection with
          member this.Enumerators
              with get ()     = internalEnum
              and set (enum)  = internalEnum <- enum

let mkExampleClass (enums: #IEnumerable []) =
    ExampleClass(enums |> Array.map (fun x -> x :> _))

Usage:

let listDoesNotMatchIEnumerable = 
    mkExampleClass myList

Your enum parameter is fixed as IEnumerable [] as that's what required for the interface property.

While you can treat eg a string list [] as an IEnumerable [] (due to the fact arrays are covariant in .NET), it's really not advisable as you'd be able to stick an int list in there, which would throw an exception. eg imagine this code:

let list1 : string list [] = Array.singleton ["bla"]
let list2 : IEnumerable [] = list1 |> box |> unbox
list2.[0] <- [1] :> IEnumerable

The third line compiles, but throws an ArrayTypeMismatchException at runtime because the underlying type is string list [] .

So, armed with the knowledge above you can make your API work as you want - you need can cast the argument to IEnumerable [] . I'm not sure it's a great idea for the reasons outlined above: the IEnumerable [] returned by your property can easily throw exceptions when mutated because it's pretending to be something it's not.

type ExampleClass<'a when 'a :> IEnumerable> (enum:'a []) =

    let mutable internalEnum = enum |> box |> unbox

    interface ICreateEnumeratorCollection with
      member this.Enumerators
          with get ()     = internalEnum
          and set (enum)  = internalEnum <- enum

let result =
    ExampleClass myList

Or, a safer, create a new array with the correct type:

let mutable internalEnum = enum |> Array.map (fun x -> x :> IEnumerable)

But as the interface itself isn't generic, I'm not sure the implementation should be. For example, you might create an ExampleClass<string list> , but the result is something that has a property of type IEnumerable [] , which is not generic - you could take the Enumerators property from an ExampleClass<string list> and set it to the same property on an ExampleClass<int list> . While that works, I'm not sure it makes a lot of sense.

It probably more sensible to have a generic factory function that creates an instance of a non-generic type:

type ExampleClass (enum) =

     interface ICreateEnumeratorCollection with
          member val Enumerators = enum with get, set
          
     static member Create (source:seq<#IEnumerable>) =
         let array = source |> Seq.cast<IEnumerable> |> Array.ofSeq
         ExampleClass array

let result =
    ExampleClass.Create myList

The Create function just requires a sequence of something that implements IEnumerable . It then casts each item and creates an array, which can then be used in your implementation.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM