Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make F# infer common base type?

Consider this:

module Module1 =
    type A() = class end
    type B() = inherit A()
    type C() = inherit A()

    let f x = if x > 0 then new B() else new C()

The last line yields an error about type B being expected, but type C being found instead. Ok, I can pretend to understand that: the compiler doesn't know which common base to infer in case there are many.

But guess what? Even when I specify the function type, it still doesn't work:

    let f x : A = if x > 0 then new B() else new C()

Now this gives me two errors: "A expected, B found" and "A expected, C found". WTF? Why can't it see that both B and C are implicitly convertible to A?

Yes, I do know that I could use upcast, like so:

    let f x : A = if x > 0 then upcast new B() else upcast new C()

But guess what (again)? upcast only works in the presence of the explicit function type declaration! In other words, this:

    let f x = if x > 0 then upcast new B() else upcast new C()

still gives an error.

WTF?! Do I really have to add 50% of noise to my program just to help the compiler out? What's with all that hype about F# code being clean and noiseless?

Somehow it feels like this cannot be true. So the question is: am I missing something? How do I make this both compact and working?

like image 228
Fyodor Soikin Avatar asked Dec 17 '22 02:12

Fyodor Soikin


2 Answers

Type inference and subtyping do not play well together, as Carsten's links discuss to some extent. It sounds like you are unhappy with F#'s approach and would prefer it if

if b then 
    e1 
else 
    e2

were implicitly treated more like

if b then (e1 :> 'a) else (e2 :> 'a)

with the compiler additionally inferring 'a to be the least upper bound in the type hierarchy based on the types that would otherwise be inferred for e1 and e2.

It might be technically possible to do this, and I can't definitively speak to why F# doesn't work this way, but here's a guess: if if statements behaved this way then it would never be an error to have different types in the if and else branches, since they could always be unified by implicitly upcasting them to obj. However, in practice this is almost always a programmer error - you almost always want the types to be the same (e.g. if I return a character from one branch and a string from the other, I probably meant to return strings from both, not obj). By implicitly upcasting, you would merely make the presence of these errors harder to find.

Furthermore, it's relatively rare in F# to deal with complicated inheritance hierarchies, except perhaps when interoperating with other .NET code. As a result, this is a very minor limitation in practice. If you're looking for a syntactically shorter solution than upcast, you might try :> _, which will work as long as there is something to constrain the type (either an annotation on the overall result, or a specific cast on one of the branches).

like image 124
kvb Avatar answered Dec 29 '22 14:12

kvb


there is a reason for all of this but to make it short: F# is more strong typed than C# so you have to tell where to cast to (see here):

let f x = if x > 0 then (new B() :> A) else (new C() :> A)

Here you can find further information: F# need for cast

And here is another great discussion on this.

like image 30
Random Dev Avatar answered Dec 29 '22 15:12

Random Dev