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

Support multiple constraints on flexible types by #ILogger1 & #ILogger2 #1262

Closed
4 of 5 tasks
Lanayx opened this issue Mar 27, 2023 · 14 comments
Closed
4 of 5 tasks

Support multiple constraints on flexible types by #ILogger1 & #ILogger2 #1262

Lanayx opened this issue Mar 27, 2023 · 14 comments

Comments

@Lanayx
Copy link

Lanayx commented Mar 27, 2023

Updated (@dsyme):

It seems reasonable to support a limited form of intersection type, e.g.

let f (env: #ILogger1 & #ILogger2) = ...

or also naming the type variable:

let f (env: 'T & #ILogger1 & #ILogger2) = ...

Since the F# 7 release there is a discrepancy between generic and non-generic restrictions, ie

allowed: env: 'T when ILogger<'T>
not allowed env: 'T when ILogger

Describe the solution you'd like

I propose to add support for non-generic case as well.

Describe alternatives you've considered

We can live with the old syntax env: 'T when 'T:> ILogger
Another possible syntax (covering multiple cases) could be env: #ILogger1 and #ILogger2

Pros and Cons

The advantages of making this adjustment to F#: this will allow to be more concise when dealing with a long list of restrictions, for example when implementing dependency injection based on the article https://bartoszsypytkowski.com/dealing-with-complex-dependency-injection-in-f/

The disadvantages of making this adjustment to F# are: one can say that this encourage type-level programming, but since this is the best (and probably the only) way to do scalable DI in F#, it's not an issue for me

Extra information

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

Related suggestions: (put links to related suggestions here)
dotnet/fsharp#14477

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

For Readers

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

@dsyme
Copy link
Collaborator

dsyme commented Apr 13, 2023

I'm not in favour of adjusting the F# 7 design further along these lines - the 'T is passed to ILogger<'T> in order to be accurate and explicit.

@dsyme dsyme closed this as completed Apr 13, 2023
@Lanayx
Copy link
Author

Lanayx commented Apr 13, 2023

@dsyme Could it be that you've misread the suggestion, since I'm not suggesting to remove generic parameter from ILogger<'T>, but rather enable the shortened syntax for the case when ILogger is not generic at all

@dsyme
Copy link
Collaborator

dsyme commented Apr 23, 2023

@Lanayx Yes, I misread.

The restriction is there because the 'T is not implied when there are multiple type variables, e.g.

let f <'T, 'U when ILogger> () = ...

Which type variable does ILogger relate to? It feels like it would be inconsistent to allow it just for the single type variable case.

Also, it's not just that non-generic is not allowed - it's that any non-self-referential instantiation is not allowed, so for example

env: 'T when ILogger<int>

is not allowed.

@Lanayx
Copy link
Author

Lanayx commented Apr 23, 2023

@dsyme Thank you for the explanation, what about the other variant of this suggestion - to enable multiple flexible type restrictions?
env: #ILogger1 and #ILogger2
This doesn't seem to have drawbacks your mentioned, while serve the same purpose - to give the shorthand for multiple non-generic restrictions

@dsyme
Copy link
Collaborator

dsyme commented Apr 25, 2023

@Lanayx Interesting suggestion. I think and would be ambiguous. Perhaps this, not sure

let f (env: #ILogger1 & #ILogger2) = ...

@dsyme
Copy link
Collaborator

dsyme commented Apr 25, 2023

However it would only work in those places where flexible types are allowed, do you think that's a problem?

@Lanayx
Copy link
Author

Lanayx commented Apr 25, 2023

@dsyme and looks much more suitable for me, since it's used for joining constraints in other places. However if it's impossible to implement, & is still acceptable even it will work only for flexible types. As I mentioned in description - flexible types are essential when doing DI in fsharp (which is almost every big application), so concise syntax will improve readability.

@dsyme
Copy link
Collaborator

dsyme commented Apr 28, 2023

Ah yes, that use of and is indeed already present.

So your proposal is to allow

let f (env: 'T when 'T:> ILogger1 and 'T:> ILogger2) = ...

to be written

let f (env: #ILogger1 and #ILogger2) = ...

There is a distinction here - #ILogger1 and #ILogger2 introduces and into the syntax of types, rather than type constraints.
That feels pretty much like a form of intersection type on two related flexible types. The usual syntax for intersection types in other languages (e.g. TypeScript) is ty1 & ty2, which is one reason I suggested it.

I'm wondering if there will be other combinations, e.g.

let f (env: 'T & #ILogger) = ... // yet another way to mixin `ILogger` constraint into `T`

or

let f (env: 'T & #ILogger1 & #ILogger2) = ... // mixin both constaints into `T`

I don't think there are any variations on ty1 & ty2 that make sense in F# that aren't flexible types like this?

@Happypig375
Copy link
Contributor

@dsyme You already visited the proposal on Intersection Types and here is one of their uses already. #600

@Lanayx
Copy link
Author

Lanayx commented Apr 29, 2023

@dsyme From user perspective I'd like as little confusion as possible. & indeed looks like type intersection, so if fsharp supported it - this would be the way to go. If not - flexible types are not real types, but generic argument restrictions, so user doesn't even think about intersection types. and looks very natural and it can easily supercede current short syntax for generics. For example:
number: 'T when IZero<'T> and IOne<'T>
vs
number: #IZero<'T> and #IOne<'T>
If we go with & users will ask questions - why it is only supported for flexible types and not for any other regular types.

@dsyme
Copy link
Collaborator

dsyme commented Apr 30, 2023

Some thoughts:

  • It's fairly natural for F# in the future to support IFoo & IBar as intersection types - by erasure presumably. This was rejected in Add Intersection Types #600 but I'm thinking we should re-open it given this discussion - it's not something that's "wrong" in any sense - just considerable technical work. I've long thought the CLR should have this feature.
  • If we had intersection types there would be all sorts of restrictions - e.g. string & int is not possible nor any sealed type. Starting with "flexible types only" and working out to other intersection types doesn't seem so bad.
  • It somehow doesn't feel natural to have ty1 and ty2 in the syntax of types. I know it's in the syntax of constraints, but very few people use that and and just feels wrong given it's usually used to link things that have a leading keyword, e.g. let rec f .. and ... and type A = ... and B = ....

I will reopen this and make a note in #600 and rename to indicate it's about #IZero<'T> & #IOne<'T>

@dsyme dsyme reopened this Apr 30, 2023
@dsyme dsyme changed the title Add shorthand for generic type restrictions by non-generic interfaces Support multiple constraints on flexible types by #IZero<'T> and #IOne<'T> Apr 30, 2023
@dsyme dsyme changed the title Support multiple constraints on flexible types by #IZero<'T> and #IOne<'T> Support multiple constraints on flexible types by #ILogger1 & #ILogger2 Apr 30, 2023
@dsyme dsyme mentioned this issue Apr 30, 2023
5 tasks
@dsyme
Copy link
Collaborator

dsyme commented Apr 30, 2023

@Lanayx I'll also mark this as approved-in-principle. I'm not certain if the ty1 & ty2 syntax flies w.r.t. amigbuities - e.g. are there unresolvable ambiguities with the use of & for byrefs or pattern syntax. Maybe. Perhaps it can always be disambiguated without parantheses in this particular case because the & is followed by a #

@pblasucci
Copy link

pblasucci commented Jun 25, 2023

For what it’s worth, I’ve been using active patterns to get a “poor man’s” version of this for quite some time now. 👍

Though presumably, having the constraint logic “built-in” would provide benefits (eg: more optimized). So I’m definitely in favorite of seeing this limited-use intersectionality become a proper part of the language. 🚀

@Lanayx
Copy link
Author

Lanayx commented Sep 24, 2023

I'm closing this since merged into main. However it's worth noting that just a few days ago another solution emerged, which would probably have prevented me from creating suggestion at all if was known back then, it appears that it's possible to write

let f (env: #ILogger1 :#ILogger2) = ...

So as we now have 2 syntax options for doing the same thing, while & has a broader applicability we might want to create a suggestion to deprecate the "old" one (not sure)

@Lanayx Lanayx closed this as completed Sep 24, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants