fslang-suggestions icon indicating copy to clipboard operation
fslang-suggestions copied to clipboard

Option to erase SRTP constraints

Open gusty opened this issue 3 years ago • 13 comments

I propose we add a way to "erase" SRTP constraint for cases where the sum of all constraints results in "the whole universe of types", or when the constraint is simply redundant.

The existing way of approaching this problem in F# is inexistent, a bunch of constraints are propagated everywhere which slows down a lot type inference, IDE and complicate type signatures.

The typical example when this feature is missed is when treating some types in a special way by adding specific overloads and having a fallback overload for the rest.

As an example I'll share the code for the nonNullTuple function which comes handy to avoid null references exception when using values injected in pattern matching, but produces a lot of constraints: https://gist.github.com/gusty/cc7bcb3803930f8a1181098c064c626b

Another example is the sscanf / trySscanf functions from F#+ which has a constraint that comes from its internal implementation but it's not relevant to use it since it resolves the return type based on the format string.

I can think of 2 ways of doing this

  • Automatically when the sum of constraints is the whole universe of types, this is ideal but probably not very realistic given the current state of the constraint solver, or it is too much work.
  • By "telling" the compiler that constraints have to be erased for a specific function signature. If we do it incorrectly we might get a compile error, although type check succeeds.

Pros and Cons

The advantages of making this adjustment to F# are:

  • Uncomplicated signatures for client code
  • Faster type checking, although compile-time will still get impacted since at some point the constraints need to be resolved
  • Better IDE experience

The disadvantages of making this adjustment to F# are depending on which way to implement this is chosen, already expressed above.

Extra information

Estimated cost XXL for the automatic derivation method and S for the manual one.

Related suggestions: #890

Affidavit (please submit!)

  • [x] This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • [x] I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • [x] 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:

  • [x] This is not a breaking change to the F# language design
  • [x] 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 :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

gusty avatar Jan 10 '22 10:01 gusty

@gusty I don't understand the specific technical proposal here :)

dsyme avatar Jun 16 '22 16:06 dsyme

@dsyme In other words, this mechanism will solve the same problem @abelbraaksma solved for the string function https://github.com/dotnet/fsharp/pull/9549 but in a more generic way, specifically these 2 issues:

  • Remove constraints from signature (here in these cases there are more than there)
  • Possible compile time speedup by typechecker, not checking and solving constraints

gusty avatar Jun 17 '22 20:06 gusty

@dsyme, I believe this is about using SRTP to tackle certain use-cases, as in the linked nonNullTuple. In such use-cases, the net sum of all the SRTP constraints is zero constraints (ie, the function can be used with any type). Hence the comparison with string, which uses type-directed SRTP, but doesn’t show these constraints on the function, as the specific syntax used in its body is understood by the compiler to accept any type.

Even if we’d create an SRTP-enabled function that ultimately resolves to no constraints on the resolved type, the type-checker (or Lang service) will still surface those inner constraints, which will confuse users of that function.

The proposal here is to define some mechanism to lift those constraints (assuming the sum is net zero), or introduce what we already have for functions like string, int etc, ie where the first part defines the general case, and the static when will deal with specific cases.

What mechanism there is that we could expose to allow this I don’t know. But I feel it’s worth considering, esp since this kind of SRTP in the wild is becoming more ubiquitous in libraries like F#+.

abelbraaksma avatar Jun 25 '22 12:06 abelbraaksma

Today I came with a simpler situation: I have an inline function which uses SRTP constraints to resolve a type and call some static methods in it, now one of those static methods is optional, for optimization only, so if it's not present it would still work, but the constraint of course get added to the main function, when in fact it's not really needed.

So, I understand why is the constraint added, but it would be nice to have a way to remove it, as in fact it doesn't make sense.

gusty avatar Sep 05 '22 17:09 gusty

Today I came with a simpler situation: I have an inline function which uses SRTP constraints to resolve a type and call some static methods in it, now one of those static methods is optional, for optimization only, so if it's not present it would still work, but the constraint of course get added to the main function, when in fact it's not really needed.

So, I understand why is the constraint added, but it would be nice to have a way to remove it, as in fact it doesn't make sense.

Can you please show an example of what do you mean? I can't quite grasp the "optional" constraint part. Constraints are "attached" to the function itself, not sure how it can optional.

vzarytovskii avatar Sep 05 '22 18:09 vzarytovskii

They can't, that's precisely the problem.

gusty avatar Sep 05 '22 18:09 gusty

@gusty would NoEagerConstraintApplication be what you're looking for?

https://github.com/fsharp/fslang-design/blob/main/FSharp-6.0/FS-1097-task-builder.md#feature-noeagerconstraintapplicationattribute

NinoFloris avatar Sep 05 '22 18:09 NinoFloris

Not really, that seems to be very useful from what I read, but not for these scenarios.

gusty avatar Sep 05 '22 18:09 gusty

Beyond overload resolution you indeed can't really have conditional or optional constraints 😅

NinoFloris avatar Sep 05 '22 18:09 NinoFloris

Well, F# lib does it internally. So technically it should be possible. Again, see the string function case.

gusty avatar Sep 05 '22 18:09 gusty

Sure if you can guarantee the constraint is an F# only constraint (and force inlining) you can get away with erasing them. That doesn't hold in the general case though.

I wrote a big comment on differences of ctor constraint handling between C# and F# here for example https://github.com/dotnet/fsharp/issues/13498#issuecomment-1215371729

NinoFloris avatar Sep 05 '22 18:09 NinoFloris

They can't, that's precisely the problem.

Yeah, what I meant is - I'm curious how do you see them working? Something like fscore's when guards + force inlining?

vzarytovskii avatar Sep 05 '22 18:09 vzarytovskii

Yes, those when guards are an example of looking statically for a method without propagating the constraint for that method.

@NinoFloris actually NoEagerConstraintApplication does help to "remove" a constraint in the sense that by not defaulting to that overload, those constraints are not shown in the caller function. You can also do that by adding dummy overloads to ambiguate the default one.

Those technique seem to solve this problem, but in the end they don't solve it completely because in some scenarios that constraint show up again. Also, for the case described initially in this issue (no constraints at all) it doesn't solve the problem.

gusty avatar Sep 05 '22 19:09 gusty