Partial type inference proposal #7286
-
Improving type inference
SummaryAllow a user to specify only necessary type arguments of
by introducing the Introduces
of generic object creation. MotivationThe current method type inference works as an "all or nothing" principle.
object person = ...
int age = person.GetFieldValue("Age"); // Error: T can't be inferred
public static class ObjectExtensions {
public static T GetFieldValue<T>(this object target, string field) { ... }
}
using System.Collections.Generic;
var element = Foo(new List<int>()); //Error: TElem can't be inferred
TElem Foo<TList, TElem>(TList p) where TList : IEnumerable<TElem> {...}
var number = new Complex {RealPart = 1, ImaginaryPart = true}; // Error: TReal and TImaginary can't be inferred
public class Complex<TReal, TImaginary>
{
public TReal RealPart {get; set;}
public TImaginary ImaginaryPart {get;set;}
} Introducing improved method type inference involving the features above would bring breaking changes into the next C# version which we try to avoid. For the first problem we would like to replace unsufficient type inference by giving the compiler hints about ambiguous types and letting the compiler infer the obvious ones.
var temp = ToCollection<List<_>, _>(1); // We are specifying the generic class, but its type argument can be inferred by the compiler
TList ToCollection<TList, TElem>(TElem p1) where TList : IEnumerable<TElem> {...} The example is goal-directed and introducing For the second part of the problem, we can introduce constructor type inference, where we can try improved type inference by adding new type constraints. Possible extensionsWorth to mention other options which could be accomplished in the future regarding default and named type arguments. class Foo<T1, T2 = int> {}
class Foo<T1, T2 = int, T3 = string> {}
new Foo<T3: _, T2: string, T1 = _>(); // Assuming that T1 can be inferred and T3 is default.
new Foo(T2: string)(); // T1 and T3 are defaults
new Foo<_,_>(); // Choosing Foo<T1, T2> based on the arity Method type inference(including object creation) is not the only place where we can use the Wrapper<_> wrapper = ... // I get an wrapper, which I'm interested in, but I don't care about the type arguments, because I don't need them in my code.
wrapper.DoSomething(); At the end, casting could use the Foo<int, string> myvar = (Foo<_,_>)myobject; // Hint the type arguments based on target or other potential source of type information like default or named type arguments. An interesting thing would be to allow the static class C1<T1> {
static class C2 <T2> {
public (T1, T2) Foo(T1 t, T2 t) {}
}
}
var a = C1<_>.C2<_>.Foo(1, 1); But in combination with default parameters, It might be useful in cases, where we use entity as a global provider of something, which we determine by type. static class Factory<T = Default> {
public static T Create(){...}
}
int a = Factory<_>.Create(); // Calls Factory<int>.Create();
var b = Factory<_>.Create(); // Calls Factory<Default>.Create(); Although it is unlikely that it would be added into C# because of implementation complexity and hard readebility of code. ScopePartial type inference can be solved in various ways. DesignChoosing the placeholderWe base our choice on the usages specified below.
Diamond operator
Foo<>(arg1, arg2, arg3); // Doesn't bring us any additional info
new Bar<>(); // Many constructors which we have to investigate for applicability
new Baz<>(); // Its OK, we know what set of constructors to investigate.
class Bar { ... }
class Bar<T1> { ... }
class Bar<T1, T2> { ... }
class Baz<T1,T2> {...}
Wrapper<> temp = ...
<>[] temp = ...
<> temp = ... // equivalent to `var temp = ...` Whitespace seperated by commas
Foo<,string,List<>,>(arg1, arg2, arg3);
new Bar<,string,List<>,>(arg1, arg2) { arg3 };
Bar<,string,List<>,> temp = ...
[] temp = ...
Foo<,[],>(arg1, arg2)
temp = ... _ seperated by commas
Foo<_, string, List<_>, _>(arg1, arg2, arg3);
new Bar<_, string, List<_>, _>(arg1, arg2, arg3);
Bar<_, string, List<_>, _>(arg1, arg2);
_[] temp = ...
_ temp = ... var seperated by commas
Foo<var, string, List<var>, var>(arg1, arg2, arg3);
new Bar<var, string, List<var>, var>(arg1, arg2, arg3);
Bar<var, string, List<var>, var>(arg1, arg2);
var[] temp = ...
var temp = ... Something else seperated by commas Doesn't make a lot of sense because it needs to assign new meaning to that character in comparison with Conslusion I prefer Nullable AnnotationSince we have nullable analysis, we will permit to specify nullability like this Partial method type inferenceFor every generic method or function call, we will enable to use Implementation
We will treat
So we have a type argument list and enter the overload resolution.
A description of type inference will be presented in the type inference of constructors.
Type arguments, which don't contain any
The condition for entering into type inference is similar to the second point. Examples
// Inferred: [TCollection = List<MySuperComplicatedElement<Arg1, Arg2>>, TElem = MySuperComplicatedElement<Arg1, Arg2>]
// Use case: Specifying a type of collection because other arguments can be inferred. Sometimes, the `where` constraints are crucial for the type inference. In that case, we will use the hint because type inference is not so powerful.
var temp1 = ToCollection<List<_>, _>(new MySuperComplicatedElement<Arg1, Arg2>());
// Inferred: [TResult = MyResult, TAlgorithm = MyAlg, TOptions = MyAlgOpt, TInput = MyInput]
// Use case: Most of the type arguments can be inferred from arguments. Sometimes the return type contains a type parameter as well and it can be crucial for the type inference. In that case, we will use the hint because type inference is not so powerful.
MyResult temp2 = Run<_,_,_,MyResult>(new MyAlg(), new MyAlgOpt(), new MyInput());
// Inferred: [TPressision = double]
// Use case: Type parameters can be used for internal usage. In that case, we would like to provide the compiler hint.
Result temp3 = Computation<double, _, Result, _>(new Data(), new Opts());
// Definitions
TCollection ToCollection<TCollection, TElem>(TElem p1) where TCollection : IEnumerable<TElem> { ... }
TResult Run<TAlgorithm, TOptions, TInput, TResult>(TAlgorithm alg, TOptions opts, TInput input) { ... }
Result Computation<TPressision, TData, TResult, TOpts>(TData data, TOpts opts) { ... }
F1<_, string>(1); // Inferred: [T1 = int, T2 = string] Simple test
F2<_,_>(1,""); // Inferred: [T1 = int, T2 = string] Choose overload based on arity
F3<int, _, string, _>(new G2<string, string>); // Inferred: [T1 = int, T2 = string, T3 = string, T4 = string] Constructed type
F4<_, _, string>(x => x + 1, y => y.ToString(),z => z.Length); // Inferred: [T1 = int, T2 = int, T3 = string] Circle of dependency
F5<string>(1); // Inferred: [T1 = string] Expanded form #1
F5<_>(1, ""); // Inferred: [T1 = string] Expanded form #2
F5<_>(1, "", ""); // Inferred: [T1 = string] Expanded form #3
B1<int> temp1 = null;
F6<A1<_>>(temp1); // Inferred: [ T1 = A1<int> ] Wrapper conversion
B2<int, string> temp2 = null;
F6<A2<_, string>>(temp2); // Inferred: [ T1 = A2<int, string> ] Wrapper conversion with type argument
C2<int, B> temp3 = null;
F6<I2<_, A>>(temp3); // Inferred: [ I2<int, A> ] Wrapper conversion with type argument conversion
dynamic temp4 = "";
F7<string, _>("", temp4, 1); // Inferred: [T1 = int] Error: T1 = string & int
F7<_, string>(1, temp4, 1); // Inferred: [T1 = int] Warning: Inferred type argument is not supported by runtime (type hints will not be used at all)
temp.F7<string, _>(temp4); // Inferred: [T1 = int] Warning: Inferred type argument is not supported by runtime (type hints will not be used at all)
F1<_,_>(""); // Error: Can't infer T2
F1<_,int>(""); // Error: Can't infer T2
F1<_,byte>(257); // Error: Can't infer T2
#nullable enable
string? temp5 = null;
string temp6 = "";
C2<int, string> temp7 = new C2<int, string>();
C2<int, string?> temp8 = new C2<int, string?>();
C2<string, int> temp9 = new C2<string, int>();
F8<int, _>(temp5); // Inferred: [T1 = int, T2 = string!]
F8<int, _>(temp6); // Inferred: [T1 = int, T2 = string!]
F8<int?, _>(temp5); // Inferred: [T1 = int?, T2 = string!]
F8<int?, _>(temp6); // Inferred: [T1 = int?, T2 = string!]
F9<int, _>(temp5); // Inferred: [T1 = int, T2 = string?]
F9<int, _>(temp6); // Inferred: [T1 = int, T2 = string!]
F9<int?, _>(temp5); // Inferred: [T1 = int?, T2 = string?]
F9<int?, _>(temp6); // Inferred: [T1 = int?, T2 = string!]
F10<I2<_, string?>>(temp7); // Inferred: [T1 = I2<int, string?>!] Can convert string to string? because of covariance
F10<C2<_, string?>>(temp7); // Error: Can't convert string? to string because of invariance
F10<I2<_, _>>(temp7); // Inferred: [T1 = I2<System.Int32, System.String!>!]
F10<C2<_, _>>(temp7); // Inferred: [T1 = C2<System.Int32, System.String!>!]
F10<I2<_, _>>(temp8); // Inferred: [T1 = I2<System.Int32, System.String?>!]
F10<C2<_, _>>(temp8); // Inferred: [T1 = C2<System.Int32, System.String?>!]
F10<I2<_, string>>(temp8); // Error: Can't convert string? to string because of covariance
F10<C2<_, string>>(temp8); // Error: Can't convert string? to string because of invariance
F10<I2<string?, int>>(temp9); // Inferred: [T1 = C2<System.Int32, System.String?>!] Can convert string to string? because of contravariance
void F8<T1, T2>(T2? p2) { }
void F9<T1, T2>(T2 p2) { }
void F10<T1>(T1 p1) {}
#nullable disable
//Definitions
void F1<T1, T2>(T1 p1) {}
void F2<T1, T2>(T1 p1, T2 p2) {}
void F2<T1>(T1 p1, string p2) {}
void F3<T1, T2, T3, T4>(G2<T2, T4> p24) {}
class G2<T1, T2> {}
void F4<T1, T2, T3>(Func<T1, T2> p12, Func<T2, T3> p23, Func<T3, T1> p31) { }
void F5<T>(int p1, params T[] args) {}
void F6<T1>(T1 p1) {}
class A {}
class B : A{}
class A1<T> {}
class A2<T1, T2> {}
class B1<T> : A1<T> {}
class B2<T1, T2> : A2<T1, T2> {}
interface I2<in T1, out T2> {}
class C2<T1, T2> : I2<T1, T2> {}
void F7<T1, T2>(T1 p1, T2 p2, T1 p3) {}
void F11<T1, T2>(T2 p2) { }
//Seperated Assembly
F1<_> (null); // Inferred: [T1 = _] class `_` turned the inference off
F1<T1>(T1 p1) {}
class _ {} Type inference of constructorAs we mentioned in the motivation, we will experiment with improving type inference in object creation.
Beside mentioned partial type inference, we will include information about target type and initializer list together with type parameter constraints. Implementation
We will treat
The inference is entered when there is an empty type argument list (diamond operator
Type arguments, which don't contain any
We change the best common type of set of expressions by adding new constraints from type argument list of array creation (e.g.
For the rest of the points, we will use the following diagram to better describe the process.
Information about the target can come from two places.
There can be situations when the argument is another object creation expression that needs type inference.
This step is a little bit complicated because of method overloads.
After we infer the type parameters and choose the right constructor, we have to try to bind "UnconvertedExpressions" again with target type info and convert it into proper bound nodes.
Then we proceed as usual to bind the initializer list.
We will extend the API to enable obtaining info about target type, type argument hints (e.g. We will also create another type of type variable which would be Because from now on the bounds can contain unfixed type variables. We then run the first phase as usual. During the second phase, we have to be careful about dependencies. We can't fix a type variable that is Fixing is done in the usual way with one exception. When the type variable has a shape bound. We have to keep the type exactly the same and just check if it is ok with other constraints. After the Type inference, we receive inferred type parameters
Because in the current C# version, there is no constructor overloading, there is no need for rewriting the types generated from constructors. Examples
using System.Collections.Generic;
// Inferred: [T = int] Assuming that there are no other generic type with `List` name
// Use case: We want to determine the type of the element by the initializer list.
var temp1 = new List<>{ 1, 2, 3};
// Inferred: [TKey = string, TValue = int]
// Use case: Doesn't matter how the add method looks like
var temp2 = new Dictionary<_,_>{ {"key", 1} };
// Inferred: [T = int]
// Use case: Type parameters can be determinded by target type
IEnumerable<int> temp2 = new List<>();
// Inferred: [Tuple<string, int>[]]
// Use case: Information about target type can be "forwarded" into the nested expressions
IEnumerable<Tuple<string, int>> temp3 = new[] { new("",1 ) };
// Inferred: [T = int]
// Using type hints in type argument list
new C1<_>[] {new C2<int>()};
// Inferred: [T = int]
// Use case: Information about the target is propagated even in generic calls
Foo(new List<>(), 1);
//Inferred: [TKey = string, TValue = int]
// Use case: Using indexers to determine type parameters
var temp4 = new Dictionary<_,_>()
{
["foo"] = 34,
["bar"] = 42
};
// Inferred: [TCollection = List<int>, TElem = int]
// Use case: Using where constraint to determine other type parameters.
var temp5 = new Bag<_,_>(new List<int>());
// It is possible to combile info from several soruces (target type, initializer list, type arguemnt list, constructor, where constraint)
//Declarations
class Dictionary<T1, T2, T3> {}
void Foo<T>(IEnumerable<T> p1, T p2) {}
class Bag<TCollection, TElem> where TCollection : IEnumerable<TElem>
{
public Bag(TCollection collection) {}
}
new C1<_, string>(1); // Inferred: [T1 = int, T2 = string] Simple test
new C2<_,_>(1,""); // Inferred: [T1 = int, T2 = string] Choose overload based on arity
new C3<int, _, string, _>(new G2<string, string>); // Inferred: [T1 = int, T2 = string, T3 = string, T4 = string] Constructed type
new C4<_, _, string>(x => x + 1, y => y.ToString(),z => z.Length); // Inferred: [T1 = int, T2 = int, T3 = string] Circle of dependency
new C5<string>(1); // Inferred: [T1 = string] Expanded form #1
new C5<_>(1, ""); // Inferred: [T1 = string] Expanded form #2
new C5<_>(1, "", ""); // Inferred: [T1 = string] Expanded form #3
B1<int> temp1 = null;
new C6<A1<_>>(temp1); // Inferred: [ T1 = A1<int> ] Wrapper conversion
B2<int, string> temp2 = null;
new C6<A2<_, string>>(temp2); // Inferred: [ T1 = A2<int, string> ] Wrapper conversion with type argument
C2<int, B> temp3 = null;
new C6<I2<_, A>>(temp3); // Inferred: [ I2<int, A> ] Wrapper conversion with type argument conversion
dynamic temp4 = "";
new C7<string, _>("", temp4, 1); // Inferred: [T1 = int] Error: T1 = string & int
new C7<_, string>(1, temp4, 1); // Inferred: [T1 = int] Warning: Inferred type argument is not supported by runtime (type hints will not be used at all)
F1<_,_>(""); // Error: Can't infer T2
F1<_,int>(""); // Error: Can't infer T2
F1<_,byte>(257); // Error: Can't infer T2
#nullable enable
string? temp5 = null;
string temp6 = "";
GC2<int, string> temp7 = new GC2<int, string>();
GC2<int, string?> temp8 = new GC2<int, string?>();
GC2<string, int> temp9 = new GC2<string, int>();
new C8<int, _>(temp5); // Inferred: [T1 = int, T2 = string!]
new C8<int, _>(temp6); // Inferred: [T1 = int, T2 = string!]
new C8<int?, _>(temp5); // Inferred: [T1 = int?, T2 = string!]
new C8<int?, _>(temp6); // Inferred: [T1 = int?, T2 = string!]
new C9<int, _>(temp5); // Inferred: [T1 = int, T2 = string?]
new C9<int, _>(temp6); // Inferred: [T1 = int, T2 = string!]
new C9<int?, _>(temp5); // Inferred: [T1 = int?, T2 = string?]
new C9<int?, _>(temp6); // Inferred: [T1 = int?, T2 = string!]
new C10<I2<_, string?>>(temp7); // Inferred: [T1 = I2<int, string?>!] Can convert string to string? because of covariance
new C10<C2<_, string?>>(temp7); // Error: Can't convert string? to string because of invariance
new C10<I2<_, _>>(temp7); // Inferred: [T1 = I2<System.Int32, System.String!>!]
new C10<C2<_, _>>(temp7); // Inferred: [T1 = C2<System.Int32, System.String!>!]
new C10<I2<_, _>>(temp8); // Inferred: [T1 = I2<System.Int32, System.String?>!]
new C10<C2<_, _>>(temp8); // Inferred: [T1 = C2<System.Int32, System.String?>!]
new C10<I2<_, string>>(temp8); // Error: Can't convert string? to string because of covariance
new C10<C2<_, string>>(temp8); // Error: Can't convert string? to string because of invariance
enw C10<I2<string?, int>>(temp9); // Inferred: [T1 = C2<System.Int32, System.String?>!] Can convert string to string? because of contravariance
class C8<T1, T2>
{
public C8(T2? p2) { }
}
class C9<T1, T2>
{
public C9(T2 p2) { }
}
class C10<T1>
{
public C10(T1 p1) {}
}
#nullable disable
//Definitions
class C1<T1, T2>
{
public C1(T1 p1) {}
}
class C2<T1, T2>
{
public C2(T1 p1, T2 p2) {}
}
class C2<T1>
{
public C2(T1 p1, string p2) {}
}
class C3<T1, T2, T3, T4>
{
public C3(G2<T2, T4> p24) {}
}
class G2<T1, T2> {}
class C4<T1, T2, T3>
{
public C4(Func<T1, T2> p12, Func<T2, T3> p23, Func<T3, T1> p31) {}
}
class C5<T1>
{
public C5(int p1, params T1[] args) {}
}
class C6<T1> {
public C6(T1 p1) {}
}
class A {}
class B : A{}
class A1<T> {}
class A2<T1, T2> {}
class B1<T> : A1<T> {}
class B2<T1, T2> : A2<T1, T2> {}
interface I2<in T1, out T2> {}
class GC2<T1, T2> : I2<T1, T2> {}
class C7<T1, T2>
{
public C7(T1 p1, T2 p2, T1 p3) {}
}
class C11<T1, T2>
{
public (T2 p2) { }
}
//Seperated Assembly
new C1<_> (null); // Inferred: [T1 = _] class `_` turned the inference off
class C1<T1>
{
C1(T1 p1) {}
}
class _ {}
using System.Collections.Generic;
// Inferred: [T = int]
// Target type(type of variable declaration) is passed to type inference of constructor
C1<int> temp1 = new C2<_>();
// Inferred: [T = int]
// Target type is passed to type inference of constructor after overload resolution of `F1<T>`
F1(new C2<_>(), 1);
//Inferred: [T = int]
//It works for constructors and initializers as well
new C3<_>(new C2<_>(), 1);
// Inferred: [T = int]
IEnumerable<int> temp2 = new[1];
//Inferred: [T = C2<int>]
IEnumerable<C1<int>> temp3 = new C2<_>[1];
// Inferred: [T1 = C1<int>, T2 = int]
// Using constraints to determine type parameters
new C4<_,_>(1);
// Inferred: [T1 = int, T2 = string]
// Using Object intializer list to determine type parameters
new C5<_,_> {Prop1 = 1, Prop2 = ""};
//Inferred: [T1 = 1, T2 = string]
//Using Collection Initializer list to determine type parameters
new C6<_,_> {{1, ""}};
// Error: Can't infered because Add method has overloads.
new C7<_,_> {1, "" };
//Inferred: [T1 = int, T2 = string]
// Using indexers to determine type parameters
new C8<_,_>
{
["A"] = 1;
};
// Inferred: [T1 = int, T2 = int, T3 = int, T4 = int, T5 = int, T = int]
// Combinining type constraints from target, constructor, type argument list, object initializer and where clause to determine type of parameters
F1(new C9<_,_,_,int,_>(1) {Prop1 = 1},1);
class C1<T> {}
class C2<T> : C1<T> {}
void F1<T>(C1<T> p1, T p2) {}
class C3<T>
{
public C3(C1<T> p1, T p2) {}
}
class C4<T1, T2> where T1 : C1<T2>
{
public C4(T2 p1) {}
}
class C5<T1, T2>
{
public T1 Prop1 {get;set;}
public T2 Prop2 {get;set;}
}
class C6<T1, T2> : IEnumerable
{
...
public void Add(T1 p1, T2 p2) {}
}
class C7<T1, T2> : IEnumerable
{
...
public void Add(T1 p1) {}
public void Add(T2 p2){}
}
class C8<T1, T2>
{
public T1 this[T2 p1] {get {throw new NotImplementedException();} set {throw new NotImplementedException();}}
}
class C9<T1, T2, T3, T4, T5> : C1<T3> where T5 : C1<T4>
{
public C9(T1 p1) {}
public T2 Prop1 {get;set;}
} |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 1 reply
-
@TomatorCZ thanks for the detailed proposal, it's clear you've put a lot of thought into this. At this point, #1349 is in need of an approved specification, which means we need to start with filling out filling out https://github.com/dotnet/csharplang/blob/main/proposals/proposal-template.md with the specification for how the feature behaves. You've made a start of that here, but it's mixed in with implementation details that aren't necessary at this stage and isn't reviewable by the LDM in the current state. A spec proposal needs to include references to the spec sections that it is proposing changing, and how they would change. For this feature, I would expect to see changes in the syntax specification and changes in the type inference section (likely by saying that type inference behaves in some modified combination state with some fixed set of inputs). I would also encourage you to start with specing just the Finally, a note about timelines: right now the LDM and compiler teams are heads down on our committed C# 12 work, and any specification is unlikely to be looked at before the fall. You'll need to work with the proposal champion (@RikkiGibson) to get the specification into a state where it's ready to go before the LDM at that point. I would encourage you to take that time to fix a few more compiler bugs to get more familiar with our codebase and coding practices; language features require a good amount of investment from the entire team, even for community-contributed features, and we do prefer that larger features (which this undoubtedly is) come from users that are experienced with the codebase to alleviate some of that pressure. Thanks! Let me or Rikki know if you have any questions on making a specification. You can also find us on either the .NET Evolution Discord Server in the #csharp-lang-design channel, or on the C# Community Server in the #roslyn channel. |
Beta Was this translation helpful? Give feedback.
-
New version of the proposal can be found here |
Beta Was this translation helpful? Give feedback.
@TomatorCZ thanks for the detailed proposal, it's clear you've put a lot of thought into this. At this point, #1349 is in need of an approved specification, which means we need to start with filling out filling out https://github.com/dotnet/csharplang/blob/main/proposals/proposal-template.md with the specification for how the feature behaves. You've made a start of that here, but it's mixed in with implementation details that aren't necessary at this stage and isn't reviewable by the LDM in the current state. A spec proposal needs to include references to the spec sections that it is proposing changing, and how they would change. For this feature, I would expect to see changes in the synt…