csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

Proposal: list pattern

Open gafter opened this issue 4 years ago • 67 comments

Allow is [ 1, 2, 3 ] (list pattern), ~~is [length] (length pattern)~~ and is [ 1, ..var x, 5 ] (slice pattern).

Speclet: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-11.0/list-patterns.md

See https://github.com/dotnet/csharplang/pull/3245

LDM Discussions:

  • https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-12-14.md
  • https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-12-16.md
  • https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-02-03.md#list-patterns-on-ienumerable
  • https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-12.md
  • https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-06-07.md#list-patterns
  • https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-06-14.md#list-pattern-syntax
  • https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-06-21.md#list-patterns-in-recursive-patterns
  • https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-09-22.md#open-questions-in-list-patterns
  • https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-09-22.md#open-questions-in-list-patterns
  • https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-10-13.md
  • https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-10-20.md#open-questions-in-list-patterns
  • https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-09.md#ambiguity-of--in-collection-expressions

gafter avatar May 07 '20 16:05 gafter

Awesome. Is this going to build on the linked proposal (with your additional feedback) or is this to track the work of designing list/collection/dictionary/etc patterns in general?

HaloFour avatar May 07 '20 17:05 HaloFour

@HaloFour This is to track answering questions like those.

gafter avatar May 07 '20 20:05 gafter

list/collection/dictionary/etc patterns

I wonder how you'd imagine dictionary/indexer patterns work and what use cases you have in mind.

Syntax-wise it could be {[constant]: pattern} but that's useless because any common type that exposes an indexer, throws if it's out of range.

alrz avatar May 09 '20 22:05 alrz

Maybe we could use a TryGet method.

gafter avatar May 10 '20 01:05 gafter

To me, that's too specific to have a dedicated syntax for. I think we should enable patterns for all Try-like methods which is essentially what user-defined positional patterns are.

map is TryGetValue("constant key", pattern) // maybe with another name

This could made to work only on "some" methods as mentioned in https://github.com/dotnet/csharplang/issues/1047#issuecomment-440254430

Syntactical symmetry with indexer initializers makes it attractive though, however, the fact that it would not call the indexer might be confusing.

alrz avatar May 10 '20 05:05 alrz

Would this allow something like

Array: { Length: 1, [1] }

checking whether there is only one element and whether that element is 1? Also, looking at the PR it's unclear to me whether this would allow checking for just the 5th element for example? Looks like that's not possible?

HurricanKai avatar Oct 26 '20 14:10 HurricanKai

@HurricanKai

The pattern [1] already includes the check for length=1 so you don't need to add anything else.

To match Nth element you could prepend N-1 discards - it tends to get long but it is definitely possible.

The team is interested in supporting indexers, so using range indexer pattern, we could support { [5]: var elem } and generate e.Length >= 5 && e[5] is var elem (we'd only accept constant indexer args so we can calculate the minimum size).

I'm not sure if that's common enough for a dedicated support, plus it won't work with generic dictionaries which is probably a deal breaker.

alrz avatar Oct 26 '20 17:10 alrz

I'd generally much prefer [idx]: { ... } because it looks much more natural inside of more pattern matching, in fact I tried typing it out like that today cause I thought it would work 😄 ie [2]: { [5]: { Length: 5 } } on a string[][] would be quite amazing if possible.

HurricanKai avatar Oct 26 '20 17:10 HurricanKai

Does this proposal also include list decomposition? e.g. (syntax debatable):

List<int> my_list = .....;

{ int first, int second, .., int last } = my_list;

Unknown6656 avatar May 21 '21 16:05 Unknown6656

No. That's https://github.com/dotnet/csharplang/issues/4082.

333fred avatar May 21 '21 16:05 333fred

Recording issues brought up about the length pattern so far:

  • Ambiguity with the array size: int[0] is expected to match new int[0]
  • Ambiguity with the list pattern itself (presumably because this is a completely new concept and a few languages use [])

My initial position would be to ditch it and consider something else for an empty list match. Based on feedback, the intended connection to "array size" isn't quite clear and it even fails at that as the first point above demonstrates.

cc @CyrusNajmabadi @jcouv

alrz avatar Jun 02 '21 15:06 alrz

@jcouv We've been hearing quite a bit about this from several community channels (twitter, discord, etc.) Can we bring this to an upcoming meeting to discuss and make some decisions on? Thanks!

CyrusNajmabadi avatar Jun 02 '21 16:06 CyrusNajmabadi

@CyrusNajmabadi Will do.

jcouv avatar Jun 02 '21 16:06 jcouv

reading the design meeting notes, if the eventual outcome is that we can also replace:

var things = new [] { "Car", "Motorbike", "Cab" };

with

var things = ["Car", "Motorbike", "Cab"];

I'll be very pleased. It's one of the things that jars when I work with 3 different languages in my day job (C#, Typescript/json and Python)

mungojam avatar Jun 21 '21 21:06 mungojam

I really like the direction the team is going for with list patterns. Especially this part (emphasis mine)

Return to the original proposal syntax, using square brackets ([]) to denote a list pattern. This breaks with the correspondence principle, but it does have stronger parallel with other languages, has a natural base case, and we could potentially add a new creation form that achieves correspondence (and take the time to address things like ImmutableArray<T>, which cannot be initialized by collection initializers today). ... Today, square brackets are used for indexing operations and for specifying the length of an array. Nothing in C# uses them to denote a group of things that is a collection. There are proposals to use these brackets for an improved version of collection initializers though, giving us an opportunity for future fulfillment of the correspondence principle, even if it won't be fulfilled on initial release.

from LDM June 14th.

Hawing possibility to use this "simplified" bracket syntax when the type of collection targer is know would be great. If I understand correctly we could do something like this, right?:

string[] ar1 = ["foo", "bar"];
List<number> = [1,2,3];

public record Person(string FirstName, string LastName);

ImmutableArray<Person> = [ new("James","Bond"), new("John", "Smith") ]

But if this new syntax would be used without target type (like in var things = ["Car", "Motorbike", "Cab"]; example by @mungojam ) it would be also be nice to have array as "natural type" (just like planned improvements for lambda in C# 10)

Then we just need something similar for dictionaries (#414) and working with basic collections (lists and dictionaries) would be much more pleasant in C# 😁

mpawelski avatar Jun 29 '21 21:06 mpawelski

I think the natural type of the new syntax should be List<T>, not T[]. Arrays already have their own creation syntax and lists are used much more often than bare arrays.

orthoxerox avatar Jun 29 '21 23:06 orthoxerox

I was thinking about having T[] as natural type because this is the "lightest" collection, and succinct syntax will probably lead developers to use it more than the old way of creating arrays. If natural type would be List<T> then people might use Lists even for cases when array is enough.

But after second thought maybe it doesn't matter?

If we'll ever get nice dictionary literal (like described in #414) for creating and pattern matching dictionaries and also want to have "natural type" to be able to use it with var then we would probably choose <Dictionary<TKey,TValue> anyway. Then choosing List<T> for new "list syntax" would make sense (since it's also mutable collection that can grow, not like array)

mpawelski avatar Jun 29 '21 23:06 mpawelski

In my own code, I almost never want Array - it's literally the 1% case. Maybe 60% of the time I want List<T>, the rest of the time it'll be something immutable, or maybe a Set<T>

There is no natural type for list construction in .Net, because we have a rich set of well supported collection types.

This is unlike languages like Go which have a privileged collection type (eg slice) that stands alone because the language provides exclusive support that can't be extended to custom types.

theunrepentantgeek avatar Jun 30 '21 01:06 theunrepentantgeek

There is no natural type for list construction in .Net, because we have a rich set of well supported collection types.

Not yet, but I think there's a good reason to boost List<T> to that level as the single most-used collection type (and for no other reason). It would feel as legitimate to me as it did to boost the single most-used delegate type for each signature (Func and Action) as natural types for delegates.

jnm2 avatar Jun 30 '21 13:06 jnm2

Not yet, but I think there's a good reason to boost List<T>

Why not ImmutableArray<T> then? roslyn is a good example where that's the dominated collection type.

alrz avatar Jun 30 '21 14:06 alrz

Folks, remember that this issue is for list patterns. If we want to start debating what a new collection syntax means in initialization, best to either find an existing issue on the topic or open a new discussion.

333fred avatar Jun 30 '21 15:06 333fred

there is in chance to have any signature like this in future: int[] firstArray=[1,2,3]; int[] secondArray = [...firstArray,5,6,7,8,9];

sajjadarashhh avatar Jan 18 '22 11:01 sajjadarashhh

there is in chance to have any signature like this in future: int[] firstArray=[1,2,3]; int[] secondArray = [...firstArray,5,6,7,8,9];

@sajjadarashhh https://github.com/dotnet/csharplang/issues/5354

333fred avatar Jan 18 '22 16:01 333fred

In the collection-literal discussion (https://github.com/dotnet/csharplang/issues/5354) a point was raised that using .. for list-patterns was problematic if we wanted the corresponding expression syntax for splatting. Specifically, the collection literal proposal suggests the parallel [e1, ..e2] syntax to go along with the [p1, ..p2] pattern syntax.

This creates a syntactic ambiguity for collection expressions. Specifically, say someone wants a collection of ranges. they cannot write: [..i1, ..i2] as that will be interpreted as splatting, not ranges. There is a workaround of [(..i1), (..i2)] but that's somewhat unfortunate. The feedback considers switching our syntax here (for both) away from .. to .... So a list pattern would be written: [p1, ...p2] and a collection literal would be: [e1, ...e2]. A collection of ranges would need no special syntax at that point.

This might also benefit us in the future if we ever wanted to pattern match ranges themselves. e.g. range switch { >1..<10.

In a real sense, by squatting on .. for list-patterns we have a correspondence mismatch between .. in a pattern and .. in an expression, which isn't great and may be limiting in the future in other ways.

CyrusNajmabadi avatar Feb 25 '22 19:02 CyrusNajmabadi

Well, the main issue is that .. is a slice pattern, which is exactly what .. is used for today. var slice = list[..^1]; is the corresponding slice expression to the pattern [.. var slice, _].

333fred avatar Feb 25 '22 19:02 333fred

Well, the main issue is that .. is a slice pattern

Right. But that hasn't shipped yet right? So i think the issue to raise with LDM is: should we have a different syntax (like ...) for the slice pattern?

CyrusNajmabadi avatar Feb 25 '22 20:02 CyrusNajmabadi

Right. But that hasn't shipped yet right? So i think the issue to raise with LDM is: should we have a different syntax (like ...) for the slice pattern?

I think you missed my point: slicing is already in the language, as the range operator. We have symmetry here between the variable initializer form and the list slicing form. It's worth bringing up, but I think that, no matter what we do, we're going to have an inconsistency somewhere.

333fred avatar Feb 25 '22 21:02 333fred

Ok, I think I'm starting to get the rationale for the .. syntax in list patterns and why it's called "slice pattern".

I'm a great proponent of having symmetry between construction expressions and deconstruction patterns in Pattern Matching™

But for me this "slice pattern" doesn't feel that symmetric to "slice expression" (already existing in C# since Ranges were introduced). IMO it feels much more natural to think about it as "spread pattern" that is symmetric to spread_element in new "collection literals" proposal.

And because we don't have list pattern yet I suggest to look at current "slice pattern" as "spread pattern" (and possibly rename it), and change the syntax from .. to .... And for spread_element in list literal proposal I suggest to change it from .. s1 to ... s1.


And why I didn't look at current slice pattern as symmetric to current "slice expression"? Lets look at couple of examples:

construction deconstruction
var slice = arr[..^1] [.. var slice, _] => slice
var slice = arr[1..] [_, .. var slice] => slice
var slice = arr[3..] [_,_,_, .. var slice] => slice
var slice = arr[4..^2] [_,_,_,_, .. var slice, _, _] => slice
var slice = arr[4..6] not possible?, or maybe something like this: [_,_,_,_, var a, var b, ..] => {var slice = new[]{a, b};} }

If we want slice pattern to be "symmetric" then this syntax makes more sense:

construction deconstruction
var slice = arr[..^1] [.. var slice ^1] => slice
var slice = arr[1..] [1 .. var slice] => slice
var slice = arr[3..] [3 .. var slice] => slice
var slice = arr[4..^2] [4 .. var slice ^2] => slice
var slice = arr[4..6] [4 .. var slice 6] => => slice

Which is much more symmetric. But it looks very strange and unfamiliar to me. Probably because I haven't seen such pattern in any other language. Also I don't think this syntax gives us much, you probably very rarely want to pattern match to small slice in the middle of very long collection where skipping all those _,_,_,_,_ noise would be beneficial.

But if you look at ... in list pattern as a "spread pattern" symmetric to "spread expression" (spread_element in list literal proposal). Then it looks much more "symmetric"

construction deconstruction
var collection = [a, b, ...otherCollection] [var a, var b, ... var otherCollection]
var collection = [a, ...otherCollection, b, c] [var a, ... var otherCollection, var b, var c]
var collection = [...otherCollection, a, b] [... var otherCollection, var a, var b]

And I think this "symmetry" between "spread expression" and "spread pattern" is much clearer to see by an average developer than this "symmetry" between "slice expression" and "slice pattern".

mpawelski avatar Feb 26 '22 01:02 mpawelski

[(..i1), (..i2)]

Assignments aren't allowed in collection initializers and in practice it hasn't been a problem. I think this has the same degree of likelihood to be actually useful.. Is it worthwhile to break the symmetry across all three (ranges, patterns, collection literals) just to support one scenario out-of-the-box? (and the second you want to use ... for anything else, you're back to square one)

alrz avatar Feb 26 '22 16:02 alrz

I'm also concerned that by using ... we may limit future applicability of that term (like using it for splatting tuples). But that may also be an argument in support of it. A 'spread' operator that works on sensible scenarios.

CyrusNajmabadi avatar Feb 26 '22 18:02 CyrusNajmabadi

Note, I'm on the fence here. I like that .. works reasonably well. But there are def cracks and inconsistencies with it I think is worth discussing. My gut feels like we won't change anything here. But it's like to bring it up to still discuss

CyrusNajmabadi avatar Feb 26 '22 18:02 CyrusNajmabadi