Type inference with pipe or composition fails when a normal function call is made

I rarely encounter this fight at present with F #, but again, the type of inheritance is much less common with F #, so maybe I was just lucky. Or I have no obvious. Usually, when the compiler complains about not knowing a certain type, I reverse the order of pipes or composition operands, and I am done.

Basically, when calling a function that works like g(fx) , it also works like x |> f |> g or (f >> g) x . But today itโ€™s not ...

Here is the dirty proof 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 

This usually works (at least in my experience). Lines with a "fail" throw:

error FS0001: type mismatch. Waiting for MyExc โ†’ 'a, but considering exn โ†’ Ex. Type "MyExc" does not match type "exn"

It doesn't matter, it's not difficult to manage, but I have to write a lot of code, where composition is a simpler and more understandable approach, and I don't want to write a bunch of useful functions that I need to put everywhere.

I looked at flexible types as I think this is a contravariance problem, but I donโ€™t see how I can apply it here. Any ideas to keep this idiom?

Please note that if I reorder, i.e. like Ex << createExc >> Fail or using the return channel operator, I get the same error in another part.

+7
inheritance contravariance function-composition f # piping
source share
3 answers

In this case, the F # compiler behaves a little irregularly. In your example, you want to pass a value of type MyExc constructor that MyExc expects. Processing an object as the value of its base class is valid processing, but the F # compiler inserts such interactions in very limited places.

In particular, it inserts a coversion when passing 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 will need merging when passing a value to an arbitrary union constructor. It seems that this only happens when directly creating the join case, but this does not happen when considering the join 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! 

Thus, the limited set of places where the F # compiler applies carpet covers automatically includes various ways to call functions, but only a direct way to create DU cases.

This is a bit ridiculous behavior - and I think it would be prudent to treat DU cases as normal functions, including automatically inserting a carpet when you use |> , but I'm not sure if there are any technical reasons that make this difficult.

+7
source share

Type inference does not work with subtyping (one of which is inheritance). The H & M algorithm simply does not have the concept of subtyping in it, and various attempts to adapt it over time have not yielded good results. The F # compiler does everything possible to place subtypes where possible, in the form of special patches. For example, it will consider the function "compatible" when the actual argument is a supertype of the formal parameter. But for some reason, this โ€œpatchโ€ does not translate when converting union constructors to functions.

For example:

 type U() = inherit exn() type T = T of exn let gfx = fx 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 used as a free function, so it loses the subtype's paste.

Curiously, this works for regular functions (i.e. those that don't start as union constructors):

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

And it even works without dots:

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

This part offers a workaround for you: you can just give a different name to the Ex constructor, and the pipeline will work:

 type Ex = Ex of exn let makeEx = Ex static member createRes2 t = t |> createExc |> makeEx |> Fail // Should work now 
+3
source share

You can make generic types with inheritance restriction.

 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 
+2
source share

All Articles