knocte
knocte

Reputation: 17919

Fighting the F# compiler over generics: Type 'T does not match type Foo1

So I have this kinda simple algorithm to read a JSON file, and depending if the serialization works or not, do something with the result:

namespace Test

type IFoo =
    abstract member SomeFoo: string with get

type Foo<'T when 'T :> IFoo> =
    {
        Bar: 'T;
        Baz: string;
    }

type Foo1(someFoo: string, otherFoo: string) =
    member val OtherFoo = otherFoo with get

    interface IFoo with
        member val SomeFoo = someFoo with get

type Foo2(someFoo: int, otherFoo: string) =
    member val OtherFoo = otherFoo with get

    interface IFoo with
        member val SomeFoo = (someFoo + someFoo).ToString() with get


module TestModule =
    let Deserialize<'T when 'T:> IFoo>(): Option<Foo<'T>> =
        let json = System.IO.File.ReadAllText("someFile.json")

        let foo1:Option<Foo<Foo1>> =
            try
                Some(Newtonsoft.Json.JsonConvert.DeserializeObject<Foo<Foo1>> json)
            with
            | _ -> None

        match foo1 with
        | Some(theFoo1) -> Some(theFoo1)
        | None ->
            let foo2:Option<Foo<Foo2>> =
                try
                    Some(Newtonsoft.Json.JsonConvert.DeserializeObject<Foo<Foo2>> json)
                with
                | _ -> None
            match foo2 with
            | Some(theFoo2) -> Some(theFoo2:>Foo<IFoo>)
            | None ->
                System.Console.Error.WriteLine("No serialization format matched")
                None

    let DoStuff<'T when 'T:> IFoo>(foo: Foo<'T>): unit =
        System.Console.WriteLine(foo.Bar.SomeFoo)

    let Start() =
        let readFoo = Deserialize()
        if (readFoo.IsSome) then
            DoStuff(readFoo.Value)

As you can see, there are two possible types: Foo1, and Foo2. And both of them implement the interface IFoo. Therefore, my expectation was that if I declare a method to receive the generic constraint <'T when 'T:> IFoo> and returning Foo<'T>, then the compiler would be happy (because in both of the cases where I return something instead of nothing, both types Foo1 and Foo2 implement IFoo).

However, for Some(theFoo1) I get the compiler error:

Type mismatch, expecting a Foo<'T> but given a Foo. The type ''T' doesn't match the type 'Foo1'

Even trying to cast it to Foo the compiler doesn't like it, like what happens with Some(theFoo2:>Foo<IFoo>):

The type 'IFoo' doesn't match the type ''T'

How can I abstract this properly (without using object as return type, obviously)?

Upvotes: 1

Views: 504

Answers (1)

Fyodor Soikin
Fyodor Soikin

Reputation: 80714

Generic means "working with any type".

When you declare a function like:

let Deserialize<'T when 'T:> IFoo>(): Option<Foo<'T>>

you're saying "Come one, come all! Invoke my function! Name your type 'T, any type whatsoever (as long as it implements IFoo), and I shall return thee an Option<Foo<'T>>, whatever 'T you give me!"

To put it slightly less dramatically: the function implementation doesn't choose what 'T is. The caller chooses what 'T is.

So, when you're trying to return an Option<Foo<Foo1>>, the compiler doesn't get that: you said you would return any 'T that the caller asks you to, but in fact you're trying to return a very specific Foo1 instead of 'T.

If your aim is to return a different implementation of IFoo depending on how the computation goes, you have a few choices.

First, if you don't want the caller to care what IFoo implementation it is, just say that your function returns IFoo:

let Deserialize(): Option<Foo<IFoo>> =

and then don't forget to cast to it when constructing values:

match foo1 with
| Some { Bar = foo1, Baz = baz } -> Some { Bar = foo1 :> IFoo, Baz = baz }

yes, you do have to unwrap and rewrap the record for this to work, because, despite appearances, these are records of different types: the left one is Foo<Foo1>, the right one is Foo<IFoo>.

Second, if you do want to preserve the concrete type of Foo, and yet want to return different types depending on how it goes, then you have no choice but to return a choice type:

type DeserializationResult = 
   | One of Option<Foo<Foo1>> 
   | Two of Option<Foo<Foo2>>

let Deserialize(): DeserializationResult

and don't forget to wrap the results accordingly:

match foo1 with
| Some _ -> One foo1

Upvotes: 4

Related Questions