4

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?

Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
  • 2
    subtyping and type inference do not get along well. – Mauricio Scheffer Mar 27 '12 at 16:31
  • 3
    @OnorioCatenacci - I don't speak for Don or the team. However, I'm not sure that I'd recommend emailing fsbugs when questions about design decisions come up - I think it's really meant for bug reports and feature suggestions. – kvb Mar 27 '12 at 17:17
  • 2
    F# does try to alleviate (or steer you away from) such situations by providing alternatives like [object expressions](http://msdn.microsoft.com/en-us/library/dd233237.aspx). – Daniel Mar 27 '12 at 17:20
  • @OnorioCatenacci: I didn't really mean to suggest a design change. My initial assumption was that the design does not really have this limitation, and that there is some easy way to achieve what I want. I now see from the answers, however, that this is indeed a recognized limitation and that it is backed by some more or less legitimate reasons. This is all I wanted to know. – Fyodor Soikin Mar 27 '12 at 17:23
  • @Daniel: Perhaps I should have been clearer on this, but I did not end up in this situation in the course of regular program design. I have encountered this while trying to interoperate with the rest of .NET, where I had to return different derived objects based on some conditions. – Fyodor Soikin Mar 27 '12 at 17:26
  • @kvb You're right--I've deleted my comment. – Onorio Catenacci Mar 27 '12 at 17:28
  • @OnorioCatenacci: You shouldn't have. Now passers-by will have no idea what we're all talking about. :-) – Fyodor Soikin Mar 27 '12 at 17:30
  • @FyodorSoikin: You could give specific examples. Perhaps there are other ways to approach it. – Daniel Mar 27 '12 at 17:33

2 Answers2

10

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).

kvb
  • 54,864
  • 2
  • 91
  • 133
  • It does not have to be "any base class". As I wrote in my question, I can pretend to understand this when the common base is ambiguous. But in my second example, where I explicitly specify the type of `f` to be `A`, it would be safe to assume that this is the type I need, wouldn't it? In other words, the compiler could treat every expression as if it had that `:> _` at the end. – Fyodor Soikin Mar 27 '12 at 16:43
  • @FyodorSoikin: I may be wrong, but type annotations are intended only to _help_ type inference. If they worked as you stated they would change the semantics of the program, as they would serve to _constrain_ the type, not merely make it explicit. All that to say, it seems it would be a foundational change. – Daniel Mar 27 '12 at 16:56
  • @FyodorSoikin - perhaps. One issue may be that similar constraints are propagated from other contexts all the time (e.g. if I do `(if true then 'c' else "string").ToString()` then does the compiler insert an implicit coercion to `obj` since that's where `ToString()` comes from, and is that treated the same way as a user-entered constraint?). Overall, does complicating the inference mechanisms for a case which doesn't arise much in practice make sense? – kvb Mar 27 '12 at 17:11
  • @FyodorSoikin - in general, compromises need to be made when trying to apply type inference to a language that supports inheritance and method overloading. Even for C#, where non-trivial inheritance hierarchies are much more common, a similar problem arises: http://stackoverflow.com/questions/4087304/the-c-sharp-conditional-operator-gets-confused-but-why. – kvb Mar 27 '12 at 17:14
  • @kvb: Ok, I now see the [more or less] legitimate reasons behind this limitation. And, more importantly, I understand that this is not an oversight, but rather a conscious decision. Thank you. – Fyodor Soikin Mar 27 '12 at 17:28
2

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.

Community
  • 1
  • 1
Random Dev
  • 51,810
  • 9
  • 92
  • 119
  • Can you please make it long? What is the reason for all this? – Fyodor Soikin Mar 27 '12 at 16:04
  • Yep, I've read those. In a nutshell, they all say that this is how F# is, so deal with it. I don't see any objective reasons for this particular design decision - upcasts being not implicit. – Fyodor Soikin Mar 27 '12 at 16:14
  • Well this and the fact you want a good type interference - there is a paper making this much clearer but I just cannot find it right now ... – Random Dev Mar 27 '12 at 16:17