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

3

7

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.

Mikelmikell answered 25/10, 2016 at 10:28 Comment(0)
S
7

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.

Subterrane answered 25/10, 2016 at 12:31 Comment(1)
Excellent and insightful, as always, thanks Tomas. Seems like we are in agreement that this is certainly an odd kind of behavior. I always thought (and normally that's correct) that DU constructors behave like functions and I chain/compose them all the time. Looking at the signature of the DU members, they "look like" functions.Mikelmikell
D
3

Type inference does not work well with subtyping (of which inheritance is one case). H&M algorithm just doesn't have a notion of subtyping in it, and the various attempts to adapt it over time haven't yielded good results. The F# compiler does try its best to accommodate subtyping where it can, in the form of special-case patches. For example, it would consider function "compatible" when actual argument is a supertype of formal parameter. But for some reason, this "patch" doesn't translate when converting union constructors to functions.

For example:

type U() = inherit exn()
type T = T of exn

let g f x = f x

let e = U()
let a = T e       // works
let b = g T e     // compile error: `e` was expected to have type `exn`, but here has type `U`

On the last line, the union constructor T is used as a free function, so it loses the subtyping patch.

Curiously enough, this works for regular functions (i.e. the ones that did not start out as union constructors):

let makeT u = T u
let a = makeT e     // works
let b = g makeT e   // also works!

And it even works point-free:

let makeT = T
let a = makeT e     // works
let b = g makeT e   // still works!

This detail suggests a workaround for you: you can just give another name to the Ex constructor, and the piping will work:

type Ex = Ex of exn
let makeEx = Ex

   static member createRes2 t =  t |> createExc |> makeEx |> Fail  // Should work now
Dermot answered 25/10, 2016 at 15:36 Comment(0)
A
2

You could make the types generic with an inheritance constraint.

open System

type MyExc (t) = inherit Exception (t)

let createExc t = MyExc (t)
type Ex<'t when 't :> exn> = Ex of 't
type Res<'t when 't :> exn> = Success of string | Fail of 't Ex with
  static member createRes1 t = Ex (createExc t) |> Fail
  static member createRes2 t =  t |> createExc |> Ex |> Fail
  static member createRes3 = createExc >> Ex >> Fail
Ardyce answered 25/10, 2016 at 10:51 Comment(1)
Yes, that's a workable solution (assuming the types are accessible). I was hoping to understand better why f x succeeds, but x |> f does not, or how to make them both succeed. The operator |> is inlined, so surely these calls ought to be equivalent?Mikelmikell

© 2022 - 2024 — McMap. All rights reserved.