Reputation: 17919
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
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