proposals/type-parameter-inference-from-constraints.md
Champion issue: https://github.com/dotnet/csharplang/issues/9453
Allow type inference to succeed in overload resolution scenarios by promoting generic constraints of inferred type parameters to "fake arguments" during type inference, enabling the bounds of type variables to participate in the inference process. An example is:
List<int> l = [1, 2, 3];
M(l); // Today: TElement cannot be inferred. With this proposal, successful call.
void M<TEnumerable, TElement>(TEnumerable t) where TEnumerable : IEnumerable<TElement>
{
Console.WriteLine(string.Join(",", t));
}
Currently, C# type inference can fail in scenarios where the compiler has all the information it needs to determine the correct type parameters through constraint relationships. This leads to verbose code requiring explicit type arguments or prevents valid overloads from being considered. This has long been a thorn in the side of C# users: no less than 9 different issues/discussions on it have come up over the past decade on csharplang.
There was even one implementation of a proposed change, https://github.com/dotnet/roslyn/pull/7850, but LDM looked at this in 2016 and decided that it would be too potentially
breaking. Since then, C# has taken larger breaking change steps; most notably for this proposal, adding natural types to lambdas and method groups in overload resolution, but
also adding things like target-typing for ternary expressions, adding span conversions as first-class conversions in the language, the field keyword, and others. Given this,
now is an excellent time to re-examine the concern on the breaking change here, and potentially move forward with the proposal.
Credit to @HellBrick for the original proposed mechanics of the design. This proposal has been further refined from their original starting point.
private static void M<T, X>(T Object) where T : IEnumerable<X>, IComparable<X>
{
}
private class MyClass : IComparable<String>, IEnumerable<String>
{
}
private static void CallMyFunction()
{
var c = new MyClass();
M(c);
}
We modify the type inference process described in §12.6.3 to include constraint relationships in the dependence relationship between type variables.
The following text from §12.6.3.6 Dependence is modified:
12.6.3.6 Dependence
An unfixed type variable
Xᵢdepends directly on an unfixed type variableXₑif one of the following holds:
- For some argument
Eᵥwith typeTᵥXₑoccurs in an input type ofEᵥwith typeTᵥandXᵢoccurs in an output type ofEᵥwith typeTᵥ.Xᵢoccurs in a constraint forXₑ.
Xₑdepends onXᵢifXₑdepends directly onXᵢor ifXᵢdepends directly onXᵥandXᵥdepends onXₑ. Thus "depends on" is the transitive but not reflexive closure of "depends directly on".
The following text from §12.6.3.12 Fixing is modified:
12.6.3.12 Fixing
An unfixed type variable
Xᵢwith a set of bounds is fixed as follows:
- The set of candidate types
Uₑstarts out as the set of all types in the set of bounds forXᵢ.- Each bound for
Xᵢis examined in turn: For each exact bound U ofXᵢall typesUₑthat are not identical toUare removed from the candidate set. For each lower boundUofXᵢall typesUₑto which there is not an implicit conversion fromUare removed from the candidate set. For each upper-bound U ofXᵢall typesUₑfrom which there is not an implicit conversion toUare removed from the candidate set.- If among the remaining candidate types
Uₑthere is a unique typeVto which there is an implicit conversion from all the other candidate types, thenXᵢis fixed toVand a lower-bound inference is performed fromVto each of the types inXᵢ's constraints, if any.- Otherwise, type inference fails.
The primary concern with this proposal is that it introduces potential breaking changes in overload resolution. Code that currently compiles and calls one overload might start calling a different overload after this feature is implemented.
Example breaking change:
void M(object obj)
{
Console.WriteLine("Called non-generic overload");
}
void M<T, U>(T t) where T : IEnumerable<U>
{
Console.WriteLine("Called generic overload");
}
// Call site:
M("test"); // Currently prints "Called non-generic overload", would print "Called generic overload"
This is somewhat similar to the breaks that occurred with lambda and method group natural types; the most common change there was that type inference failed on an instance method, and then fell back to an extension method instead. And, similarly to that proposal, the likelihood is that the new overload chosen is actually the more "correct" one; it's more likely to be what the user intended.
There are options to mitigate this break if we so choose; we could do two runs of overload resolution, first without constraint promotion, then if that fails to find a single applicable overload we could rerun with constraint promotion. This would be significantly more complex, but it could be done, and would mitigate the breaking change.
There are a couple of other options:
void M<TEnumerable, infer TElement>(TEnumerable t) where TEnumerable : IEnumerable<TElement>, and only
perform the promotion for such parameters. While this is doable, and entirely mitigates the breaking change, it immediately because the "default" that everyone should use,
and not doing so is a bug on the author's part, and thus is not good for the future of the language._ or an empty identifier to avoid
restating what can be inferred from the signature, and only state what cannot be inferred. While this is a decent idea, it doesn't fully solve this issue, as we want to avoid
needing to specify any type parameters in this case when they can be inferred.TBD