[Proposal]: Collection Expressions Next (C#13 and beyond) #8660
Replies: 82 comments
-
I'd like to see some way of calling the constructor of a type and/or setting some property during initialization. The first thing that came to mind is the comparer of a dictionary, but I'm sure there are other use-cases. Either way, collection expressions are amazing, and support for natural types and inline expressions would make them that little bit better! |
Beta Was this translation helpful? Give feedback.
-
@KennethHoff that's in the list, as part of hte dictionary-expression exploration work. Thanks :) |
Beta Was this translation helpful? Give feedback.
-
I'm not sure if the following proposal is too crazy, so I will describe it here quickly, as it's related to this topic: Imagine that I have a Ideally I would like to spread the array of ints into the array of strings while also calling If But since there is no such conversion, I would like to write something like Spread operator as a first-class citizen.So basically the spread operator The spread unary operator may be applied to any other expression, resulting in a spread_expression. When that happens, an implicit The spread_expression then behaves, for the purposes of member-lookup and overload-resolution, as an expression whose type is the element-type of the original enumerable. Because of that, one may invoke any members the element type might have, as well use the spread_expression on a method that takes element-type as argument. These invocations will then be inserted inside the invisible The result of a member invocation performed on, or taking the spread_expression as an argument, is also itself a spread_expression, whose element-type is the return type of the invoked member, if not Finally, one will want to capture the result of all these method invocations on each element of the original collection. Therefore, any spread_expression can be used regularly as a spread_element in a collection expression, with the existing rules. Problems:
|
Beta Was this translation helpful? Give feedback.
-
C# already has a query comprehension syntax, LINQ. |
Beta Was this translation helpful? Give feedback.
-
True, but, that's not really an argument. If you would argue against all proposals saying "it can already be done in some way", then none would ever be accepted. Lists could already be created with LINQ or with initializers, yet collection expressions were introduced. And they have the added benefits of duck typing/nice syntax/good performance. LINQ, on the other hand, is interface-based and makes use of delegates and anonymous objects. If one could perform some simple transformations through the use of spread operator, everything would be inserted directly in the caller method, with no delegates or closures. There isn't any optimization better than that, LINQ would almost be obsolete. |
Beta Was this translation helpful? Give feedback.
-
See: #7634 |
Beta Was this translation helpful? Give feedback.
-
That's where supporting extension methods would help. As you could write:
|
Beta Was this translation helpful? Give feedback.
-
I assume you could also do this? [ ..[1, 2, 3].Select(i => i.ToString()) ] |
Beta Was this translation helpful? Give feedback.
-
@KennethHoff yes. |
Beta Was this translation helpful? Give feedback.
-
@KennethHoff Yes, or |
Beta Was this translation helpful? Give feedback.
-
I suggest widening support for collection builders to accept other buildable collections. For example I can choose to wrap a HashSet or a List or an Array or whatever. It feels so weird to accept a Span for such types. |
Beta Was this translation helpful? Give feedback.
-
@En3Tho Can you give an example? |
Beta Was this translation helpful? Give feedback.
-
public class ArrayWrapperBuilder
{
// This works
public static ArrayWrapper<T> Create<T>(ReadOnlySpan<T> values)
{
return new(values.ToArray());
}
// I want this to work instead. Array is already a creatable collection, so just let compiler create it and use directly here
// Imagine if it was a List<T> or HashSet<T> wrapper. Even more allocations while compiler is perfectly able to create this kind of collection directly.
public static ArrayWrapper<T> Create<T>(T[] values)
{
return new(values);
}
}
[CollectionBuilder(typeof(ArrayWrapperBuilder), nameof(ArrayWrapperBuilder.Create))]
public readonly struct ArrayWrapper<T>(T[] array) : IEnumerable<T>
{
public T[] Array => array;
public IEnumerator<T> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public static class ArrayWrapperCreator
{
public static ArrayWrapper<T> GetWrapper<T>() => [default, default, default];
} |
Beta Was this translation helpful? Give feedback.
-
I'm glad the lack of Something like foreach (int i in [1, 2, 3]) was literally the first thing I tried with collection literals and I was surprised why it was failing to compile with CS9176 saying there was no target type. It basically provides exactly the same amount of information as foreach (var i in (IEnumerable<int>)[1, 2, 3]) which happily compiles today (though I would rather use some old-school array initialization instead of the bulky cast). I think this could be added even independently from natural types. |
Beta Was this translation helpful? Give feedback.
-
One addition that we would like to see is support for multi-dimensional collection literals. These are important when working with tensor libraries, e.g. here's how simple tensors are defined using the pytorch library: data = [[1, 2],[3, 4]]
x_data = torch.tensor(data) The above form isn't feasible using today's C# collection literals, so supporting it without some kind of language support seems unlikely. One possibility is that we could reuse nested collection literal syntax to construct multi-dimensional collections. TL;DR it should be possible to extend the collection builder pattern to recognize factory methods such as public static T[,] Create<T>(ReadOnlySpan<T> values, int nDim, int mDim); and then be able to define 2-D arrays like so T[,] values = [[1, 0], [0, 1]]; In principle, it should be possible for the compiler to infer the rank and dimensions and detect shape mismatches (e.g. something like What's more interesting though is that by reusing nested collection syntax there is no inherent limit on the supported number of dimensions, and the number of dimensions doesn't need to be fixed for a given type. We could for instance support builder methods of shape public static Tensor<T> Create<T>(ReadOnlySpan<T> values, params ReadOnlySpan<int> dimensions); Which should let you specify the following 2x2x2 tensor: Tensor<int> tensor = [[[0, 0], [0, 0]], [[1, 0], [0, 1]]]; |
Beta Was this translation helpful? Give feedback.
-
@julealgon earlier in this thread it was already established that infinite sequences are not supported by collection expressions. They always materialize collections and this is by design: |
Beta Was this translation helpful? Give feedback.
-
I never implied infinite sequence expressions were supported. I was just making a point that, from the outside, one could look into that syntax and think it was an infinite sequence. I was making the argument for readability only, in the sense that it could be misleading potentially. |
Beta Was this translation helpful? Give feedback.
-
In C++ there is the std::initializer_list which is the resulting type of an expression like this: I can imagine that due to the static type system a similar approach could also work for C#. |
Beta Was this translation helpful? Give feedback.
-
This might not fit into how C# natural types normally work, but here's a wild suggestion: How about making the natural type of a collection expression into just "a collection expression of the element type" in the sense that it is the type that it's used in. Here's an example: var coll = [1,2,3]; // Currently typed as simply "collection expression of ints" - basically just a recipe.
PrintNumbers(coll); // From this point on, because it was used as a `int[]` it's now an `int[]`.
void PrintNumbers(int[] numbers) { ... } The previous example is identical to the following, except you also get to keep the reference in the PrintNumbers([1,2,3]);
void PrintNumbers(int[] number) { ... } var coll = [1,2,3]; // Also just an "collection expression of ints"
foreach (var num in coll) // First used in a foreach loop, so we'll use the most efficient type here, which is `ReadOnlySpan<int>`.
{
...
} var coll = [1,2,3]; // Never used, so this never materializes. This natural type would be decided at compile time, so you can always hover over it in the IDE to see what it chose. I'm not sure what it means for the last example though; An unused local. |
Beta Was this translation helpful? Give feedback.
-
@KennethHoff what if you never pass it to another method where there is a target type? What happens when you declare the I don't think your proposal would work well because you don't always have an obvious target-type to base the decision when coding. |
Beta Was this translation helpful? Give feedback.
-
@KennethHoff That's an approach we've considered, and it still might come in handy. However, it brings up questions of teleporting the materialization to a specific type when there are multiple usages, which can be hard to reason about. |
Beta Was this translation helpful? Give feedback.
-
I don't understand this case. Could you give an example? Assuming you mean "what if you never pass it to another method" then it's the last example in my original comment; It's a noop similar to how primary constructors work for non-records.
If you don't use it anywhere it doesn't have a type and therefore doesn't have any members. If you do use it elsewhere then it has the type of whatever "elsewhere" is, and you could use those members. var coll1 = [1,2,3];
var coll2 = [2,4,6];
var coll3 = [3,6,9];
coll1.ToString(); // Compile error. Cannot call ToString on an object of type "collection expression of ints"
coll2.ToString(); // Calls ToString on List<int>.
coll3.ToString(); // Calls ToString on IEnumerable<int>, which forwards to the synthesized type for IEnumerable<int> that currently exists. Partially UB.
PrintNumbers1(coll2); // As this is the first time coll2 is referenced after its declaration, coll2 is now retroactively typed as List<int>.
PrintNumbers2(coll3); // As this is the first time coll3 is referenced after its declaration, coll3 is now retroactively typed as IEnumerable<int>.
PrintNumbers1(coll3); // Compile error. Cannot implicitly convert IEnumerable<int> to List<int>
void PrintNumbers1(List<int> numbers) { ... }
void PrintNumbers2(IEnumerable<int> numbers) { ... } This does suffer from the ol' "spooky action at a distance" problem where changing something can break something seemingly unrelated. Say you changed the This interpretation also naturally adds some optimizations that you maybe otherwise couldn't've done, like simply never materializing it if it's not needed: var coll = [1,2,3];
foreach (var item in coll)
{
Console.WriteLine(item);
} Because this was only used inside a foreach loop, we can simply compile this as this: Console.WriteLine(1);
Console.WriteLine(2);
Console.WriteLine(3); |
Beta Was this translation helpful? Give feedback.
-
var v = [1, 2, 3];
v.Add(4); |
Beta Was this translation helpful? Give feedback.
-
Compile error. Cannot call Add on "Collection expression of ints". This however would work: List<int> DoThing()
{
var v = [1,2,3];
v.Add(4);
return v; // By virtue of being the return value - which is clearly defined - `v` is retroactively typed as List<int>
} I'm not saying this is particularly clear semantics - especially for human readers - but I do think it's at least consistent; First unambiguous reference defines the type, retroactively. That would also mean that changing the previous example to any of the following would indeed make it no longer compile: List<int> DoThing()
{
var v = [1,2,3];
v.Add(4);
DoThingWithArray(v);
return v; // int[] is not implicitly convertible to List<int>
}
void DoThingWithArray(int[] numbers) { ... } IEnumerable<int> DoThing()
{
var v = [1,2,3];
v.Add(4); // There is no `Add` on `IEnumerable<int>`
return v;
} |
Beta Was this translation helpful? Give feedback.
-
This would lead to terrible dev experience. I would be ok if the compiler used context to decide the type, but there should be a type right away for when context is not known upfront. If someone is just typing code, like this: var stuff = [1, 2, 3];
stuff. They should get intellisense for something. Sometimes, context will just be completely insufficient: var stuff = [1, 2, 3];
var something = stuff.ToString(); What is the type of You are making assumptions that there will always be some target collection type that can be inferred from usage, which is really not true at all. |
Beta Was this translation helpful? Give feedback.
-
@KennethHoff: Would this work in your model? void Imply(this List<int> a, List<int> b) {} // Public extension method visible to DoThing
List<int> DoThing()
{
var a = [1];
var b = [2];
b.Imply(a); // Method not known yet
var c = [3];
c.Imply(b); // Method not known yet
return c; // Implies c: List<int>
// -> c.Imply(b) implies b: List<int>
// -> b.Imply(a) implies a: List<int>
} As much as I like type inference, this just feels like a half-assed solution that feels bad for both the Hindley-Milner crowd and the Grug brained devs. |
Beta Was this translation helpful? Give feedback.
-
Just want to say; I do not think my suggestion is good. It has terrible DX. When it comes to @nuiva's question: void Imply(this List<int> a, List<int> b) {} // Public extension method visible to DoThing
List<int> DoThing()
{
var a = [1]; // a is unknown.
var b = [2]; // b is unknown.
b.Imply(a); // b is unknown. a will be List<int> if b turns out to be List<int>.
var c = [3]; // c is unknown.
c.Imply(b); // c is unknown. b will be List<int> if c turns out to be List<int>.
return c; // c is List<int>, so b is List<int>, so a has to be List<int>.
} So yes, you understood my (way too implicit/magical) thought experiment correctly :s |
Beta Was this translation helpful? Give feedback.
-
You're (presumably) calling System.Object.ToString(), which implicitly involves a conversion to System.Object. Because of that, it's really the same question as asking what this does: object stuff = [1, 2, 3];
// stuff.GetType(), stuff.ToString(), etc If the type is determined later and the only thing you're doing later is I'm not terribly bothered by My hope is for reasonable defaults, and then in cases where you want something else, you say what you want just like with every other var-declared local. |
Beta Was this translation helpful? Give feedback.
-
@jnm2 My main concern is with the bad experience of zero intelissense until the IDE/compiler can figure out what the type will be. It will behave the same as if the type was Not a big fan. The only way I would support something like this was if it was used to improve the selected type. This would mean it would start with a simple native type, say |
Beta Was this translation helpful? Give feedback.
-
@julealgon Another thing we explored was defining basic members for the " |
Beta Was this translation helpful? Give feedback.
-
Collection Expressions Next
Summary
This issue is intended to be the umbrella tracking item for all collection expression designs and work following the core design (#5354) that shipped in C#12.
As this is likely to be a large item with many constituent parts, it will link out to respective discussions and designs as they occur.
Roughly, here are the items we would like to consider, as well as early notes on the topic: https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2024-01-23.md
["a", .. b ? ["c"] : []]
andforeach (bool? b in [true, false, null])
. Collection expressions: inline collections in spreads and foreach #7864Memory<T>
,ArraySegment<T>
etc.)IEnumerable
, etc.)Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-08.md - Iteration types of collections
https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2024-01-23.md - WG meetup
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-10.md - Conversions vs construction
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-05.md#collection-expressions-inline-collections
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-06.md#collection-expressions-next-c13-and-beyond
Beta Was this translation helpful? Give feedback.
All reactions