-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Champion: "Partial Type Inference" #1349
Comments
Pros/cons for 2 vs 3? Both are DRY but I prefer 2... you don't buy much visually with 3. |
1 is okay but verbose. I prefer 2 because I would like this potential progression in levels of needed disambiguation for |
I'm pretty sure there was an issue regarding recursive generic, but I can't find it. Without recursive type parameters, I think 2 is better because is less verbose, since verbosity is part of the issue here. But with recursive type parameters, I think 1 would fit better. |
I think that either 2 or 3 is the best one. 2 is certainly the least verbose (which is nice), but 3 looks more complete and full. I don't think the slight verbosity of 3 would really be an issue because it still requires no thinking of what the type name is, and is only three keystrokes. I slightly prefer 3, but 2 is also perfectly fine. 1 is a bit too verbose. |
I support 1 even with the inclusion of 2 or 3. It helps to document the parameters. |
I believe, F# is using
(So we could consider using it too.) |
Isn't 2 already used in other constructs? typeof(IDictionary<,>) |
@qrli |
@zippec |
@jnm2 IIRC typeof expressions cannot have partially-open generics but you can construct such Types via reflection. |
@federicoce-moravia I don't believe either is possible. Can you show me? Btw, closing over |
You can do it by grabbing the generic type parameter from the open generic type: Type type = typeof(Dictionary<,>);
Type[] args = type.GetGenericArguments();
Type type2 = type.MakeGenericType(typeof(string), args[1]); |
@HaloFour That's |
It's something that cannot be expressed in C#. It is |
@HaloFour I understand that it inexpressible in C#. Here's the dillema: it is fully constructed to close over public static class Program
{
public static void Main()
{
new Foo<string, int>().CloseForwards();
new Foo<string, int>().CloseBackwards();
}
}
public class Foo<TKey, TValue>
{
public void CloseForwards()
{
// This does the equivalent of:
_ = typeof(Foo<TKey, TValue>);
var openGeneric = typeof(Foo<,>);
var fooTypeArguments = openGeneric.GetGenericArguments();
var closeOverFooTypeArguments = openGeneric.MakeGenericType(fooTypeArguments[0], fooTypeArguments[1]);
Console.WriteLine(openGeneric == closeOverFooTypeArguments); // true (!)
Console.WriteLine(closeOverFooTypeArguments.IsConstructedGenericType); // false (!)
}
public void CloseBackwards()
{
// This does the equivalent of:
_ = typeof(Foo<TValue, TKey>);
var openGeneric = typeof(Foo<,>);
var fooTypeArguments = openGeneric.GetGenericArguments();
var closeOverFooTypeArguments = openGeneric.MakeGenericType(fooTypeArguments[1], fooTypeArguments[0]);
Console.WriteLine(openGeneric == closeOverFooTypeArguments); // false
Console.WriteLine(closeOverFooTypeArguments.IsConstructedGenericType); // true
}
} So I'm not talking about being able to instantiate from a separate class. I'm talking a level more abstract: what can you specify using |
I think we're walking in a weird gray area not well handled by the BCL and more academic than practical. The documentation for
Whereas the remarks of
Per the documentation it would seem that it wouldn't be legal for those two properties to both return Either way, I don't think this should hold up any conversation about a placeholder syntax (or lack thereof) to denote a generic type argument that is inferred. |
Certainly not. 😁 |
@gafter Is omitting the type args altogether another option? F# supports the syntax |
@FunctionalFirst We infer the type arguments to methods, not to types. This is about method type argument inference. |
Any thoughts on methods that have more than 2 type parameters and omitting multiple trailing ones? My personal feeling: No, since they're not optional like default-parameters
Also:
For that, I'm leaning more towards 3 than 2 (while 2 is the current way of specifying an open generic with |
Great that language team is considering improving type inference in C#. This is something I've been waiting for a long time. Basically I think C# should Type inference in F# was quite intuitive for me. I just discovered it naturally. In C# we recently got Anyway, I like F# syntax a lot and for me it's good if similar features have similar syntax in both languages. Partial type inferenceF#: let testGeneric<'T1,'T2>(arg1 : 'T1) =
printfn "T1: %s T2: %s" (typeof<'T1>).FullName (typeof<'T2>).FullName
() //returns nothing
testGeneric<_, string>(1) result printed: The inspired syntax in C# could look like that: class Program
{
static void testGeneric<T1, T2>(T1 arg1)
{
Console.WriteLine("T1: {0} T2: {1}", typeof(T1).FullName, typeof(T2).FullName);
}
static void Main(string[] args)
{
//doesn't work but could (now I need to write testGeneric<int, string>(1);)
testGeneric<_, string>(1);
}
} Because some people mentioned other possible type inference features I'll try to show how F# type inference could work in C#. Initialization based on later methods callF#: let list = new System.Collections.Generic.List<_>()
list.Add(1)
list.Add(2)
let list2 = System.Collections.Generic.List()
list2.Add(1)
list2.Add(2)
//list2.Add("foo")
//error if commented out
//This expression was expected to have type 'int' but here has type 'string' let list2 = System.Collections.Generic.List<_>()
// Error:
// Value restriction. The value 'list2' has been inferred to have generic type
// val list2 : Collections.Generic.List<'_a>
// Either define 'list2' as a simple data term, make it a function with explicit arguments or, if you do not intend for it to be generic, add a type annotation.
Notice the errors when you call generic methods with object of different type or don't call any method that helps compiler know what to infer. Hypothetical C# syntax: // Should be the same syntax (just use `var` instead of let and don't skip `new` keyword) Constructors type inferenceType inference for calling constructors should be the same as for calling function. F# syntax: let arrayOfNumbers = [| 1;2;3;4|]
let listOfNumbers = new System.Collections.Generic.List<_>(arrayOfNumbers);
//or don't use "new" (more common in F#) and skip passing wildcard ("_") as type parameter
let listOfNumbers2 = System.Collections.Generic.List(arrayOfNumbers);
Hypothetical C# syntax: var arrayOfNumbers = new[] { 1, 2, 3, 4 };
var listOfNumbers = new System.Collections.Generic.List(arrayOfNumbers); It's very annoying to have to specify this type in constructor. For example why I can't write this?: var listOfAnonymousTypes = new[] { 1, 2, 3, 4 }.Select((number, index) => new { number, index });
var hashSetOfAnonymousTypes = new HashSet(listOfAnonymousTypes); Inference from Object and Collection InitializersF# doesn't have the same Object and Collection Initializers syntax as C#. However, F# has similar syntax to object initializer when initializing immutable record objects and we can also initialize mutable properties of class similar to how we set properties for attributes in C# (link) F# syntax (record): type SomeTypeWithImmutableProperties<'T1,'T2> = {
Foo1 : 'T1;
Bar2 : 'T2;
}
let immutableObject = { Foo1 = 1; Bar2 = "bar" } type of F# syntax (class): type SomeClassWithMutableProperties<'T1,'T2>() =
member val Foo3 : 'T1 = Unchecked.defaultof<'T1> with get, set
member val Bar3 : 'T2 = Unchecked.defaultof<'T2> with get, set
let someMutableObject1 = SomeClassWithMutableProperties(Foo3 = 1, Bar3 = "bar");
let someMutableObject2 = SomeClassWithMutableProperties<_,_>(Foo3 = 1, Bar3 = "bar"); type of Hypothetical C# syntax: class SomeClassWithMutableProperties<T1,T2>
{
public T1 Foo { get; set; }
public T2 Bar { get; set; }
}
class Program
{
static void Main(string[] args)
{
var someObject1 = new SomeClassWithMutableProperties
{
Foo = 1,
Bar = "bar"
};
var someObject2 = new SomeClassWithMutableProperties<_,_>
{
Foo = 1,
Bar = "bar"
};
}
}
C# has also nice syntax for collection initializers, why not allow this code to work?: var list1 = new List { 1, 2, 3 };
var list2 = new List() { 1, 2, 3 };
var list3 = new List<_> { 1, 2, 3 };
var list4 = new List<_>() { 1, 2, 3 }; Or with index initializers it could look like this (and infer variables to be var dict1= new Dictionary()
{
[1] = "Please",
[2] = "implement",
[3] = "this :)"
};
var dict2 = new Dictionary
{
[1] = "It",
[2] = "would",
[3] = "be awesome!"
};
var dict3 = new Dictionary<_, _>()
{
[1] = "Let's",
[2] = "make",
[3] = "C#"
};
var dict4 = new Dictionary<_, _>
{
[1] = "great",
[2] = "again!",
[3] = ";-)"
}; This one should be an error: var dict5 = new Dictionary
{
[1] = "",
["foo"] = 1,
}; Probably type inference should work similar as type inference for implicitly-typed arrays ( Return Type InferenceMentioned in #92 F#: let row<'T1>(name : string) : 'T1 =
let objToReturn = Unchecked.defaultof<'T1>
objToReturn
let id: int = row("id")
let name: string = row("name")
let birthDate: DateTime = row("birthDate") why not allow the same in C#: (example from #92) int id = row.Field("id"); // legal, generic type argument inferred to be int
var id = row.Field("id"); // not legal, circular inference
row.Field("id"); // not legal, no assignment to a type All of the example are working in F# not because it's fundamentally different language and it won't fit in C# style. I believe all of this proposed features would fit nicely in C# and greatly improve day-to-day work of C# programmer. |
Would this partially mitigate #129? I've been running up against this one a lot. |
I propose next implementation:
public interface IAdd<T>
{
T Zero { get; }
T Add(T a, T b);
}
public static class SumAddedd
{
public static T Fold<TAdd, T>(this IEnumerable<T> en)
when TAdd : struct, IAdd<T>
=> en.Aggregate(default(TAdd).Zero, (a, p) => default(TAdd).Add(a, p));
} Simple inference: public struct IntAdd : IAdd<int>
{
public int Zero => 0;
public int Add(int a, int b) => a + b;
}
public struct LongAdd : IAdd<long>
{
public long Zero => 0;
public long Add(long a, long b) => a + b;
}
var a : IEnumerable<int>;
var b : IEnumerable<long>;
var c : IEnumerable<T>;
// This can be inferred
var fa = a.Fold(); // Take <IntAdd, int>
var fb = b.Fold(); // Take <LongAdd, int>
var fc = c.Fold<IntAdd>(); // Take <IntAdd, int>
var fc = c.Fold<long>(); // Take <LongAdd, long> Complex inference: public struct IntAdd : IAdd<int>
{
public int Zero => 0;
public int Add(int a, int b) => a + b;
}
public struct IntMul : IAdd<int>
{
public int Zero => 1;
public int Add(int a, int b) => a * b;
}
public struct LongAdd : IAdd<long>
{
public long Zero => 0;
public long Add(long a, long b) => a + b;
}
var a : IEnumerable<int>;
var b : IEnumerable<long>;
var c : IEnumerable<T>;
// This can be inferred
var fa = a.Fold(); -- error
var fa = a.Fold<IntAdd>(); // Take <IntAdd, int>
var fa = a.Fold<MulAdd>(); // Take <MulAdd, int>
var fb = b.Fold(); // Take <LongAdd, int>
var fc = c.Fold<IntAdd>(); // Take <IntAdd, int>
var fc = c.Fold<long>(); // Take <LongAdd, long> |
I'm in favor of 1 and 3, but 3 only if something like F#'s |
The only reason I suggested var was (in my head) it's similarity to using var in variable declaration, and the low cognitive change. "'var' means the inferred type" var myInt = 3;
// potentially
IEnumerable<var> myEnumerable = new [] { myInt };
var list = new List<var>(myEnumerable);
var casted = list.Select<var, long>(x => x); |
I mean if there's multiple conflicting types included, that's what Let's just make this simple for folks so we're not repeating generic parameters over and over again everywhere, it's a big pain. |
True. Today the compiler will let you know whenever there is ambiguity. If you add a new |
Thank zod, someone who can actually see the forest for the trees. |
@IanKemp I've mentioned this already, but please behave in a respectful and constructive fashion. |
That is still being accomplished with approach 2 from the original post, just it would be typed as Another use case: I spend a fair amount of time porting Java code to C#, and due to Java's type erasure, it's often convenient to have a non-generic base class with the same name as the generic type. |
How is agreeing with someone disrespectful and/or unconstructive? Also you should try reading your own Code of Conduct sometime:
You haven't complied with either of the bolded sections. |
@IanKemp Are you that excited to receive an official warning? |
Please let's not allow the thread to continue further off topic while we wait for a moderator to be online. |
@IanKemp i cannot send you private messages as I have no other means to contact you. Regarding 'providing clarity': My initial message specified what the problem is here. We expect people to engage in a friendly and constructive manner here similar to any professional setting.
Consider the difference between statements like: 'i agree with that' vs something like 'finally someone smart who gets it'. Both are statements of agreement. However one is inherently belittling to others. In this case, your statement seemed purposefully setting out to disparage others for not 'see[ing] the forest for the trees'. This is all I'll say here on moderation. If you'd like to continue this further, you can reach me at cyrusn@microsoft.com. Thanks! |
This comment has been minimized.
This comment has been minimized.
Have you considered implementing the feature yourself and submitting a pull request to Roslyn? Why wait for other people to do the work? |
@IanKemp Final warning. If you want to take about this further, email me as I mentioned. Discussions about the code of conduct or moderation need to end here. |
You have a fundamental misunderstanding here. Nothing has held this feature hostage. We already have champions for it. We just need a proposal here. We don't have one yet as the champions are working on other features and the entire team is overloaded with the current amount of work just to ship c#10. |
New features generally ship yearly (or sometimes multiple times a year). However, the team itself decides what it things is important to front load and what isn't. Currently, while this does have champions, no one particularly feels that this particular issue was important enough for c#10. Will that change for c#11? Depends on how we view this over the thousands of other areas we can make language changes that people have been wanting as well. |
Just for transparency on this idea: do not do this. An unrequested pr implementing a language change will be summarily rejected. What you can do though is work to create a viable proposal that, with team permission, could be approved and made into a compiler change. Community members have done this before. However the time commitment and bar is just as high as for anyone directly on the team. So please discuss with team first about taking this path. Thanks! |
As long as the first form exists as a fallback I'll be a very happy person. For most cases simple wildcard usage would be best but, much like named method arguments, the merits of the first syntax don't really show through until you are dealing with a very large number of them. Incidentally, those cases when you have many generic parameters are the cases that benefit the most from expanded inference. Very long real world examplePersonally, I work with reflection.emit very often, and I have a set of helper types and extension methods that use generics to do basic verification that the stack is balanced and the right types are going to the right place at compile time. This usually looks something like this: Return<int> method = new ILHelper(methodBuilder)
.LdcI4(53)
.LdcI4(13)
.Add()
.Ret();
//Signature of the Add method
static S<int, TStack> Add<TStack>(this S<int, S<int, TStack>>) where TStack : IStack {}
The stack state is stored with a generic type, in this case the type after the 2 ldcI4s but before the add is This all works great until you want to emit a call and type inference breaks because you need to supply each part of the function signature individually to the method as generic parameters to keep stack checking, but you also need to distinguish between an Return<int> method = new ILHelper(methodBuilder)
.LdcI4(1)
.LdcI4(2)
.LdcI4(3)
.LdcI4(4)
.Call<S<int, S<int, S<int, S<int, Empty>>>>, int, int, int, int, int, SigReturn<int>>(StaticFuncs.Add4)
.Ret();
//Or
Return method = new ILHelper(methodBuilder)
.LdcI4(1)
.LdcI4(2)
.LdcI4(3)
.LdcI4(4)
.LdcI4(5)
.Call<S<int, S<int, S<int, S<int, S<int, Empty>>>>>, int, int, int, int, int, NoSigReturn>(StaticFuncs.Print5)
.Ret(); With wildcards alone (option 2 and 3 basically) it would improve, but still require that extra generic param that denotes the return or lack thereof. Looking something like this: Return<int> method = new ILHelper(methodBuilder)
.LdcI4(1)
.LdcI4(2)
.LdcI4(3)
.LdcI4(4)
.Call<var, int, int, int, int, int, SigReturn<int>>(StaticFuncs.Add4)
.Ret();
//Or
Return method = new ILHelper(methodBuilder)
.LdcI4(1)
.LdcI4(2)
.LdcI4(3)
.LdcI4(4)
.LdcI4(5)
.Call<var, int, int, int, int, int, NoSigReturn>(StaticFuncs.Print5)
.Ret(); With syntax 1 though, that last arg could be dropped entirely: Return<int> method = new ILHelper(methodBuilder)
.LdcI4(1)
.LdcI4(2)
.LdcI4(3)
.LdcI4(4)
.Call<In1: int, In2: int, In3: int, In4: int, Out: int>(StaticFuncs.Add4)
.Ret();
//Or
Return method = new ILHelper(methodBuilder)
.LdcI4(1)
.LdcI4(2)
.LdcI4(3)
.LdcI4(4)
.LdcI4(5)
.Call<In1: int, In2: int, In3: int, In4: int, In5: int>(StaticFuncs.Print5)
.Ret(); Technically, this is more code to write out than just wildcards still, but only by a small amount and the overall clarity of the code is much much better. Additionally, being able to drop that last argument makes the implementation of the Call method way easier to deal with. Note: Type inference is just one of many headaches when trying to make something like this work. For clarity I've omitted a fair few additional generic arguments that are needed to handle byref and pointer types (Cannot use those normally in generics), and also totally removed all the spooky tuples needed to do branches. Other tangential note: Implementing something like the above also is quite possibly the fastest possible way to bump your head into every single limitation on generics that exists in C#. If that sounds like fun then I highly recommend it... I'm also partial to MyClass<var,int,int,var,int> _ = new();
MyClass<,int,int,,int> _ = new();
MyClass<_,int,int,_,int> _ = new(); As I recall, using |
i think reason of this proposal could be this: static void Run<t, o, i1, i2, i3>(Func<t, o> f, i1 Input1, i2 Input2, i3 Input3)
{ }
Run <MyApp,,,,> (x => x.ToString(), 1, 2, 3); |
I'm not sure this related but, is this issue also involve partial generic for Is this issue also made this below possible? object obj = new Dictionary<string,string>();
if(obj is Dictionary<string,> dict) // allow any type of dictionary's value, just need to have string as a key
{
string key = dict.Keys.FirstOrDefault();
return dict[key]; // default to object ?
} |
No. Partial type inference only means that the compiler fills in part of the generic type arguments automatically, but the types are known at compile time. The runtime doesn't permit use of open generic types in that manner you've described, and it couldn't use variance either in that case. |
I would prefer |
VB.net also seems to support partial type inference, at least in a limited fashion. I don't know the specifics and can't seem to find a reference easily, but I know I am able to do the following with extension methods: Public Module TestExtension
<Extension>
Public Function ConvertOutput(Of InT, OutT)(ByVal source As IEnumerable(Of InT)) As IEnumerable(Of OutT)
Return source.Select(Function(input) DirectCast(Convert.ChangeType(input, GetType(OutT)), OutT))
End Function
End Module
...
Dim strings As IEnumerable(Of String) = {"1", "2", "3"}
Dim ints As IEnumerable(Of Integer) = strings.ConvertOutput(Of Integer)() It is possible this is limited to the types that are provided by the Dim ints As IEnumerable(Of Integer) = strings.ConvertOutput(Of String, Integer)() |
Re-iterating through this proposal, I'm very fond of the idea of inferring obvious type parameters, both for generic methods and type constructors, especially given that generic inference does not apply to constructors. Gotta note that enabling named type parameters with inference for the others, there will have to be support for handling ambiguities for types with multiple arities. That aside, it seems like a very promising QoL improvement, one step closer to reducing some inconveniences with generics. |
One thing that I ran into which annoys me and I think this could perhaps over, is when a Type parameter constraint actually ends up defining Type parameter so to speak. Given: interface ISomeThing<T1, T2> { }
class SomeThingA : ISomeThing<int,string> {}
T Get<T, T1, T2>() where T: ISomeThing<T1, T2> => ...; When I call Get, given any type. T1 and T2 is already given by the first type due to the constraint. Yet I have to repeat that information anyways, which seems very convoluted, I wished I could just call Get as: Get<SomeThing1>(); // (T1 = int, T2 = string, we know this from SomeThing1, yet I have to provide that info)
Get<SomeThing1, int, string>(); // But that is sooo redundant, This example might highlight it better:
Get<ISomeThing<int, string>, int, string>(); Seems like this proposal would not save me from specifying the Type Arguments on the Get method, but at least it would save me from specifying them on the call to get which is where the biggest annoyance is. |
@jeme |
Normally that would mean only one of them would be an implicit implementation which TECHNICALLY means you could argue you could infer it to that, but that would be a little crazy i guess, so I would rather that it gave the classic:
As it's no longer obvious, therefore it cannot be implicitly inferred. The other example however is not so crazy in my opinion, it's not like the compiler is unaware, after all, it knows exactly which types I can use as it gives an error if I don't specify exactly matching types in this simple case. Now Covariance / Contravariance will change this a bit as you can then specify a different type parameter than what is implemented. E.g:
However, I don't really see that as an issue for the inference, if the two second parameters are not provided, it would assume B and B. |
Hi! |
See #1348
Currently, in an invocation of a generic method, you either have to specify all of the type arguments or none of them, in the latter case depending on the compiler to infer them for you. The proposal is to permit the programmer to provide some of the type arguments, and have the compiler infer only those that were not provided.
There are (at least) three possible forms this could take:
M<TElement: int>(args)
M<int, >(args)
var
(Proposal: Allow partial generic specification #1348), e.g.M<int, var>(args)
Design Meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-07.md#partial-type-inference
The text was updated successfully, but these errors were encountered: