Coding Edgar
Coding Edgar

Reputation: 1455

F# how to upcast a generic

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

Answers (2)

Coding Edgar
Coding Edgar

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

Tomas Petricek
Tomas Petricek

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

Related Questions