Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-upcast values when type information is available #849

Closed
5 tasks done
Happypig375 opened this issue Mar 10, 2020 · 12 comments
Closed
5 tasks done

Auto-upcast values when type information is available #849

Happypig375 opened this issue Mar 10, 2020 · 12 comments

Comments

@Happypig375
Copy link
Contributor

Happypig375 commented Mar 10, 2020

I propose we auto-upcast values when type information is available.

The existing way of approaching this problem in F# is adding clucky upcast keywords.

let a : obj list = [1; 2; 3] // This works
let (^^) : seq<'a> -> seq<'a> -> seq<'a> = Seq.append
let a : int seq = [1; 2; 3] ^^ [4; 5; 6] // This also works
// But this doesn't?
let a : int seq = [1; 2; 3] // error FS0001: This expression was expected to have type 'seq<int>' but here has type ''a list'    

Pros and Cons

The advantages of making this adjustment to F# are

  1. Consistency with other scenarios where upcasting is implicit
  2. Conciseness
  3. Convenience

The disadvantage of making this adjustment to F# is the possibility of more inheritance-based code that are against F# philosophy.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): S

Related suggestions:
#3
#91
#536
#792

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this
@cartermp
Copy link
Member

cartermp commented Mar 10, 2020

I'd personally love to see this for the charting scenario with XPlot as well. You need to add the :> Trace for subplots:

let trace1 =
    Scatter(
        x = [0; 1; 2; 3; 4; 5],
        y = [1.5; 1.; 1.3; 0.7; 0.8; 0.9]
    ) :> Trace

let trace2 =
    Bar(
        x = [0; 1; 2; 3; 4; 5],
        y = [1.; 0.5; 0.7; -1.2; 0.3; 0.4]
    ) :> Trace

[trace1; trace2]
|> Chart.Plot

I always forget to do that and I'm then met with this:

input.fsx (13,10)-(13,16) typecheck error All elements of a list must be of the same type as the first element, which here is 'Scatter'. This element has type 'Bar'.

@charlesroddie
Copy link

charlesroddie commented Mar 12, 2020

Will this suggestion fix your issue @cartermp ? I thought lists were one of the examples where auto-upcasting did happen.

type Trace() = class end
type Scatter() = inherit Trace()
type Bar() = inherit Trace()
let s = Scatter()
let b = Bar()

[s; b] |> Chart.Plot // Error
([s; b]:Trace list) |> Chart.Plot // Ok
Chart.Plot [s; b] // Ok

Isn't this a case of type information flowing from left to right in F#, but not (necessarily) backwards? It's valid that [s; b] by itself doesn't type-check.

@Swoorup
Copy link

Swoorup commented Mar 13, 2020

Upvote, this reduces clutter with casting to base IActionResult asp .net where in C#, this is sorta free

@cartermp
Copy link
Member

@charlesroddie the explicit type annotation at the call site works, but using non-piping style does not (same error). Supporting this particular use case would probably be complicated. Plot takes seq<#Trace>. Scatter inherits from Trace and list is compatible with seq, so in theory this could work. But F# already has a history of explicitly not walking up the type hierarchy for things due to the complexity it incurs in the typechecker.

@dsyme
Copy link
Collaborator

dsyme commented May 29, 2020

I would like to make progress on this, and will be writing notes about it in the context of widening for numeric literals and numeric types.

Will this suggestion fix your issue @cartermp ? I thought lists were one of the examples where auto-upcasting did happen.

The proposed change will not address this example. Specifically the change being proposed is presumably to embrace more algorithmic type inference and allow upcasts to the "known type" used in type inference. However in this case:

let trace1 = Scatter(...)
let trace2 = Bar(...)

[trace1; trace2] |> Chart.Plot

then at the point we check [trace1; trace2] the known type of trace1 is Scatter and not Plot, and thus the list [trace1; trace2] is Scatter list not Plot list. And Bar doesn't upcast to Scatter so the rule doesn't apply and a type error will still be given. F# likes its lists homogenous.

For the proposed rule to be relevant to the example here, the user would need to do this:

let trace1 : Plot = Scatter(...)
let trace2 : Plot = Bar(...)

[trace1; trace2] |> Chart.Plot

But this is exactly the problem of introducing this kind of type-directed rule - suddenly type annotations become much more relevant to basic type checking.

Now, F# doesn have a rule that says "if the inferred element type of the first element of a list or array is a nominal type then all the rest of the elements are implicitly checked with subtyping flexibility against that type", e.g. this is allowed:

let trace1  = Scatter(...)
let trace2 = Bar(...)

[(trace1 :> Plot) ; trace2] |> Chart.Plot

But this is a classic "type directed" rule that still requires an annotation and, while being useful now and then, rightly feels clumsy to use in practice when combined with piping. It can however be applicable in this form:

Chart.Plot [trace1 ; trace2]

where the known desired type for the list is derived from Chart.Plot.

There is however no proposal on the plate to allow

[trace1; trace2] |> Chart.Plot

without some kind of annotation, and in general for APIs sensitive to this (particularly view DOM descriptions like the above) I'd discourage the use of piping |> as a result, and maximise the flow of type informat information from destination (here Chart.Plot) into checking of contents (here [trace1; trace2]).

@dsyme
Copy link
Collaborator

dsyme commented May 29, 2020

Putting aside the question of homogeneity of lists, I'd like to identify the most common cases where upcasts are commonly required in F# code but where it is entirely reasonable to remove them. The most common I notice are "strongly annotated" expressions like this:

let getPlot () : Plot = Scatter(...) :> _

and similarly when implementing an override:

override _.GetTrace () = Scatter(...) :> _

where in the second case override-slot inference means the compiler already knows that the return type of GetTrace() must be Plot.

Often these upcasts happen on each branch of a discriminated union match or if/then/else expression, e.g. there are examples in Linq.fs

@dsyme
Copy link
Collaborator

dsyme commented May 29, 2020

I played around with an approach to introduce more flexible coercions and widenings, it's quite promising and a relatively conservative change. I'll keep playing and send a link later.

One initial note: this has the potential to inadvertently change programming around the "obj" type, for example consider:

   let x () : obj = "abc"

Currently introducing obj requires an explcit box in the basic programming model (except in tthe places where auto-coercion/widening is available, such as method calls). Adding more such places would presumably allow the above but we could tune what we want here.

@0x53A
Copy link
Contributor

0x53A commented May 30, 2020

I'd like to identify the most common cases where upcasts are commonly required in F# code but where it is entirely reasonable to remove them.

I am using BabylonJS from Fable. It has a type hierarchy of Node -> TransformNode -> AbstractMesh -> Mesh.

In many cases, properties take a value of Node, for example for parenting nodes I often have to use this code:

// Node.parent has type 'Node option'

mesh.parent <- Some (parentMesh :> _)

I imagine this is similar for other UI frameworks which have a base Control class (WinForms / WPF)

@Swoorup
Copy link

Swoorup commented Nov 30, 2020

Experimenting with erased union types, this feature to upcast when type information is available would certainly have helped:
@dsyme
dotnet/fsharp#10566

For example, when creating the cases for union type, it works out the common ancestor type which is the type all cases would be up-casted to.

I'll stick to explicit style, since that's the least way I can possibly break things, and wait for your changes

@dsyme
Copy link
Collaborator

dsyme commented Jan 21, 2021

RFC is here: https://github.com/fsharp/fslang-design/blob/master/preview/FS-1093-additional-conversions.md

@DalekBaldwin
Copy link

I've noticed that in many cases F# function types already automatically handle argument type contravariance but not return type covariance, presumably because inserting an upcast on a return value could spoil a proper tail call. Should tail calls be addressed in the RFC?

@dsyme
Copy link
Collaborator

dsyme commented Sep 25, 2021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants