I rarely have this struggle nowadays with F#, but then again type inheritance is much less common with F#, so perhaps I was just lucky. Or I am missing the obvious. Normally when the compiler complains about not knowing a certain type I reverse the order of pipes or composition operands and I'm done.
Basically, given a function call that works as g(f x)
, it also works as x |> f |> g
or (f >> g) x
. But today it doesn't...
Here's a messy proof-of-concept of what I mean:
module Exc =
open System
type MyExc(t) = inherit Exception(t)
let createExc t = new MyExc(t)
type Ex = Ex of exn
type Res = Success of string | Fail of Ex with
static member createRes1 t = Ex(createExc(t)) |> Fail // compiled
static member createRes2 t = t |> createExc |> Ex |> Fail // FS0001
static member createRes3 = createExc >> Ex >> Fail // FS0001
Normally, this works (at least in my experience). The lines with "fail" throw:
error FS0001: Type mismatch. Expecting a MyExc -> 'a but given a exn -> Ex. The type 'MyExc' does not match the type 'exn'
Not a big deal, not hard to workaround, but I happen to have to write a lot of code where composition is the easier/cleaner approach and I don't wish to write a bunch of utility functions that I have to put in everywhere.
I looked at flexible types, as I guess this is a contravariance problem, but I don't see how I can apply it here. Any ideas to keep this idiomatic?
Note, if I rearrange, i.e. as Ex << createExc >> Fail
or using the pipe-backward operator I end up with the same error on a different part.
The F# compiler behaves a bit irregularly in this case. In your example, you want to pass a value of type MyExc
to a constructor that expects exn
. Treating an object as a value of its base class is a valid coersion, but the F# compiler inserts such coersions in only very limited places.
In particular, it inserts coersion when you pass arguments to a function, but it does not insert them (for example) when creating a list or returning results from a function.
In your example, you need a coersion when passing value to a discriminated union constructor. It seems that this happens only when directly creating the union case, but it does not happen when treating the union case as a function:
// foo is a function that takes `obj` and Foo is a DU case that takes `obj`
let foo (o:obj) = o
type Foo = Foo of obj
foo(System.Random()) // Coersion inserted automatically
Foo(System.Random()) // Coersion inserted automatically
System.Random() |> foo // Coersion inserted automatically
System.Random() |> Foo // ..but not here!
So, the limited set of places where F# compiler applies coersions automatically includes various ways of calling functions, but only direct way of creating DU cases.
This is a bit funny behaviour - and I think that it would make sense to treat DU cases as ordinary functions including the automatic insertion of coersions when you use |>
, but I'm not sure if there are any technical reasons that make that hard.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With