Reputation: 1455
I cannot make an upcast of a generic with a valid sub type
type IRequest =
abstract build: string -> unit
type ReqA =
interface IRequest with
member this.build _ = ()
type Request<'a when 'a :> IRequest> = { Req: 'a }
type Request = Request<IRequest>
let fn (x: Request<ReqA>): Request =
// The type 'Request' does not have any proper subtypes and need not be used as the target of a static coercion
// Type constraint mismatch. The type 'Request<ReqA>' is not compatible with type 'Request'
x :> Request
let fn2 (x: Request<ReqA>): Request =
// No problem
{
Req= x.Req
}
Event though Request<ReqA>
is a valid specialization of Request<IRequest>
I can't cast it using :>
, what's the right way to cast this example?
Upvotes: 2
Views: 409
Reputation: 1455
I found the proper way to do this:
My error was using a record as an interface, but if I declare a full interface for the Request then it works fine.
In this Example, now the Record just has a field that is the type of the interface:
module Example1 =
type IProps =
abstract build: string -> unit
type IReq =
abstract Req: IProps
type PropsA =
{ a: string }
interface IProps with
member this.build _ = ()
type Request =
{ Req: IProps }
interface IReq with
member this.Req = this.Req :> IProps
// type Request = Request<IProps> <-- not possible, not necesary
// let fn (x: Request<PropsA>): IReq = <-- not possible
let fn (x: Request): IReq =
// It works
x :> IReq
{ Req = { a = "" } }
|> fun x -> x.Req.a // <-- but here Req is IProps, so I cannot accesss the concrete type PropsA
|> ignore
but it makes it too generic, another try:
module Example2 =
type IProps =
abstract build: string -> unit
type IReq =
abstract Req: IProps
type PropsA =
{ a: string }
interface IProps with
member this.build _ = ()
type Request<'a when 'a :> IProps> =
{ Req: 'a }
interface IReq with
member this.Req = this.Req :> IProps
type Request = Request<IProps> // <-- Possible, but not necessary
let fn (x: Request<PropsA>): Request =
// Type constraint mismatch. The type 'Request<PropsA>' is not compatible with type 'Request'
x :> Request
let fn2 (x: Request<PropsA>): IReq = // <-- notice the type alias is not needed, as now IReq is a proper interface
// It works, proper way to do it.
x :> IReq
{ Req = { a = "" } }
|> fun x -> x.Req.a // Works, Req:: PropsA
|> ignore
Now Record<PropsA>
is a full concrete type that is compatible with IReq
and casting just works ™️, this allows the use of flexible types and the rest of generic capabilities to work as expected, I was under the misconception that a generic record would behave as an interface.
Another more specialized version that is also possible is:
module Example3 =
type IProps =
abstract build: string -> unit
type IReq<'a when 'a :> IProps> =
abstract Req: 'a
type PropsA =
{ a: string }
interface IProps with
member this.build _ = ()
type Request<'a when 'a :> IProps> =
{ Req: 'a }
interface IReq<'a> with
member this.Req = this.Req // <-- notice the cast is no longer needed
type Request = Request<IProps> // <-- Possible, but not necessary
let fn2 (x: Request<PropsA>): IReq<_> =
// It works, proper way to do it.
x :> IReq<_>
{ Req = { a = "" } }
|> fun x -> x.Req.a // Works, Req:: PropsA
|> ignore
The benefit of this version is that the cast is not needed and gives more flexibility to IReq
.
Upvotes: 0
Reputation: 243041
This is not actually an allowed cast - while you can construct Request<IRequest>
from Request<ReqA>
, this may not in general be true for all generic types.
This issue with generics is what's known as "covariance" and "contravariance". Both .NET and C# support this (see the documentation for C#), but there is no way to define a covariant generic type in F# and even for types defined in C#, the F# compiler does not support this rule.
To see that this actually does not work, you can try defining fn
using a box and unsafe cast:
let fn (x: Request<ReqA>): Request =
box x :?> Request
fn { Req = ReqA() }
This type checks, but when you try to run the code, you get an exception at run-time:
System.InvalidCastException: Unable to cast object of type 'Request`1[FSI_0036+ReqA]' to type 'Request`1[FSI_0035+IRequest]'.
The summary is - if F# supported covariance and contravariance, you could define Request
as a generic type with covariant type parameter and then the above cast would be valid. But without that, the cast is not valid.
Upvotes: 4