Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are the rules for automatic upcasting of functions?

Tags:

f#

In my code I have a scenario where I'd like F# to automatically upcast values of one function type (e.g. IBase -> unit) to another, explicitly specified function type (IDerived -> unit). While this seems to be generally supported, I found cases where it fails for no obvious reason.

In the code below, case1 to case8 seem equivalent to me: In each case, there's an expression of type IBase -> unit on the right hand side that is bound to a name of type IDerived -> unit. So why are cases 4, 5 and 7 not allowed?

type IBase = interface end
type IDerived = inherit IBase

let foo (x: IBase) = ()
let bar = fun (x: IBase) -> ()

type T = T with static member (!!) (_: T) = foo
let baz = !!T

let case1: IDerived -> unit = foo                               // OK
let case2: IDerived -> unit = bar                               // OK
let case3: IDerived -> unit = baz                               // OK
let case4: IDerived -> unit = !!T                               // Expecting 'IDerived -> unit' but given 'IBase -> unit'
let case5: IDerived -> unit = upcast !!T                        // Type 'IBase -> unit' is not compatible with type 'IDerived -> unit'
let case6: IDerived -> unit = let z = !!T in z                  // OK
let case7: IDerived -> unit = fun (x: IBase) -> ()              // Expected x to have type 'IDerived' but here has type 'IBase'
let case8: IDerived -> unit = let z = fun (x: IBase) -> () in z // OK

EDIT: To clarify, I'm mostly wondering why the 8 cases are not treated equally by the compiler. For example, I'd expect the binding let z... in case8 to be superfluous, yet it makes a difference (compared to case7). Why?

EDIT: Here's some code to demonstrate what I'm trying to achieve: https://dotnetfiddle.net/AlpdpO It also contains a solution/workaround, so I'm really asking more for technical details/reasons rather than alternative approaches. I thought about raising an issue on GitHub already, but feel like the issue is most likely just a misunderstanding on my side.

like image 424
SvenEV Avatar asked Jan 25 '19 20:01

SvenEV


1 Answers

F#'s type inference doesn't seem to be quite smart enough to handle some of those cases, but you can help it out by making your definitions of a little bit more explicit. Try this:

type IBase = interface end
type IDerived = inherit IBase

let foo (x: IBase) = ()

type T = T with static member (!!) (_: T) : #IBase -> unit = foo

let case4: IDerived -> unit = !!T
let case5: IDerived -> unit = upcast !!T
let case7: IDerived -> unit = fun (x: #IBase) -> ()

...and, of course, your original passing cases will continue to pass as well. The issue is that F# has been a little bit too strict about the type of the (!!) function and the type of the lambda, so we can help it out with an explicit type annotation that says "IBase or any of its derivatives are fine here".

By all means, file a bug for these syntactic cases. It's not a terribly bad thing for a compiler to err on the side of caution when it comes to types, as the F# compiler has done here. Far worse things can happen if it's too lenient. Here, the worst effect is that you've got to do some of the typing-work for it.

I should point out that this really has nothing to do with casting and there's no casting that happens (the upcast is ignored). The real issue is strictness of interpretation. Try this code out and you'll see the warning that indicates that.

like image 199
Rhyme Avatar answered Sep 28 '22 00:09

Rhyme