fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
344 stars 21 forks source link

Support explicit generic constraints of multiple specializations of the same generic interface #1036

Open Happypig375 opened 3 years ago

Happypig375 commented 3 years ago

Support explicit generic constraints of multiple specializations of the same generic interface

I propose we allow:

type I<'T> = abstract U : unit
let f1 (_: I<int>) = ()
let f2 (_: I<string>) = ()
let f3<'T when 'T :> I<int> and 'T :> I<string>>(x: 'T) = f1 x; f2 x

You currently cannot do this in F#. The reason is given here: https://github.com/dotnet/fsharp/issues/10432#issuecomment-738324344

There is also a relevant F# type inference limitation that may be coming into play - I haven't looked closely at the repo above but it is definitely relevant to #10516

The important thing to know is this:

When F# sees 'a :> IA<ty1> and 'a :> IA<ty2> constraints for a type inference variable 'a then it unifies ty1 and ty2.

This is by design, and one of the reasons why multiple instantiations of generic interfaces was not supported in F#.

The reason this is done is that it is very, very common to have 'a :> seq<ty1> and 'a :> seq<ty2> for partially inferred types ty1 and ty2. This happens everywhere in F# code and in this situation it's crucial to flow type equalities between ty1 and ty2.

That is, the F# type inference system simply assumes that generic code is not constrained over multiple different interface instantiations.

Type definitions that implement multiple instantiations can be used but you can't write F# generic code over this code or structure.

It would be a separate RFC to enable this in some limited cases (e.g. when full type annotations are given), though I'd also recommend just not trying to do this

This may or may not be the cause of the above. On a quick glance it is the cause of #10516

When you do dostuff mything or mything |> dostuffrev the constraint of IA<?T> = IA<int> is encountered before the constraint IA<?T> = IA<string> and int is chosen to solve the inference type ?T.

When you do 5 |> dostuff mything or "blah" |> dostuff mything the solution ?T = int or ?T = string is pre-chosen and it is obvious (I believe via the feasibly-equivalent relationship in the compiler and language spec) that the compiler can apply the appropriate C :> IA<int> or C :> IA<string>

Given the constraint C :> IA<?T> the compiler will not continue with ?T unresolved.

So this is just how it works. If you're going to use this feature you'll need type annotations and/or flowed type information.

Pros and Cons

The advantages of making this adjustment to F# are

  1. Consistency with C#
  2. Convenience
  3. Correctness

The disadvantage of making this adjustment to F# is that this cannot be used together effectively with type inference for the reasons as mentioned.

Extra information

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

Related suggestions: https://github.com/dotnet/fsharp/issues/10432 https://github.com/dotnet/fsharp/issues/10516 https://github.com/dotnet/fsharp/issues/11659

Affidavit (please submit!)

Please tick this by placing a cross in the box:

Please tick all that apply:

For Readers

If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

dsyme commented 3 years ago

So is the proposal to allow the declaration, but have inference be quirky, presumably relying on type-directed inference resolutions in some way?

Note "consistency with C#" is not itself a design goal for F#.

Happypig375 commented 3 years ago

How about "interoperability with C#"?

dsyme commented 3 years ago

Yes, interop with .NET libraries is the design goal - which more or less means all of C# external surface area these days. There are related design goals like learnability. Sometimes it's easier to learn things if they are the same as something else. That was relevant to interpolated strings, for example.

That said I don't know of interop scenarios where making these particular declarations in F# is particularly needed. I'm not particularly opposed to allowing people to make them for interop purposes, the problem is that if they are made routinely to encode type-level logic then people may expect different behaviour from F# type inference.

dsyme commented 3 years ago

Concretely, I suppose this would give an easy solution to allow the technique in F#:

[<SomeAttributeSayingThisIsIntendedToSupportMultipleInstantiations>]
type I<'T> = abstract U : unit

F# type inference would be adjusted to work differently when this attribute is present. However this would only work for interfaces declared with the attribute, and wouldn't apply to arbitrary interfaces from C#.

charlesroddie commented 3 years ago

Consistency with C# Convenience Correctness

Is there an argument for any of these conclusions? At a minimum, description of C# behaviour, what makes the proposed behaviour convenient, and what makes current behavior incorrect.

Happypig375 commented 3 years ago

Consistency/Interoperability with C# - see C# code of https://github.com/dotnet/fsharp/issues/11659 Convenience - No need to have intermediate nongeneric interfaces inheriting generic specializations just to work around this. Correctness - Still, the above workaround has its flaws when exposed to C# or when the types implementing these interfaces are outside of your control. Having the correct abstraction eliminates this extra code and incorrect mental model.

En3Tho commented 3 years ago

@Happypig375 Thank you for creating a language suggestion. I don't believe the proposed attribute solution is looking great simply because people won't use it in outside world. For example, take IEquatable of T interface. There won't ever be any F# specific attribute on a System.* interface. I found wierd that F# lets types implement multiple variants of generic interface but forbids consuming them in a similar manner (like in functions or types). I stumbled upon this when playing around with env-style dependencies in functions. For now it only works via wrapping every dependency in a separate interface instead of having unified generic one. Also, I think we might stumble upon this again when static abstract methods in interfaces come up.

dsyme commented 3 years ago

Do you mean IQuatable or IEquatable

We could in theory have a pragma or something that adjusts F# type inference locally. Or some other way to inform on a pre-type and/or per-file basis. I don't particularly like doing things that way but it is possible technically.

En3Tho commented 3 years ago

@dsyme I meant IEquatable. Fixed. About attribute approach - can we maybe use attribute on class/member/function to indicate a different kind of inference? Not using it on interface declaration but on consumers. Although people will probably have to splat this attribute all over the place if they use this approach...

charlesroddie commented 3 years ago

This suggestion seems logical to me overall, and it seems reasonable to prioritize ability do to expected things explicitly over quality of type inference which is just a convenience feature. It would be good to have a good motivating example to justify the work: a use case where the types involved are natural.