Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type inference with piping or composition fails, where normal function call succeeds

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.

like image 725
Abel Avatar asked Oct 25 '16 10:10

Abel


1 Answers

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.

like image 134
Tomas Petricek Avatar answered Oct 07 '22 19:10

Tomas Petricek