Skip to content

Should list patterns explicitly check the length or not? #2415

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

Closed
munificent opened this issue Aug 18, 2022 · 6 comments
Closed

Should list patterns explicitly check the length or not? #2415

munificent opened this issue Aug 18, 2022 · 6 comments
Labels
patterns Issues related to pattern matching.

Comments

@munificent
Copy link
Member

Forking a new issue from this comment by @eernstg:


The obvious choices are the following:

var list = [1, 2, 3];

// Safe matching: Just enforce that every binding is well defined.
switch (list) {
  case [a, b, c, d]: S1; // No match, cannot bind `c` and `d`.
  case [a, b] if list.length == 2: S2; // No match, guard fails.
  case [a, b]: S3; // Match, scrutinee supports binding every variable.
  default: S4;
}

// Strict matching: Safe matching, plus an implied length guard.
switch (list) {
  case [a, b, c, d]: S1; // No match, cannot bind `c` and `d`.
  case [a, b]: S2; // No match, fails implied guard `list.length == 2`.
  case [a, b, ...]: S3; // Match, binding succeeds, and there is no implicit length guard.
  default: S4;
}

The two approaches are obviously equivalent, so we should ask people what they think is more useful and readable.


The proposal takes the strict matching approach, and that's the one I currently favor, but I could be persuaded otherwise.

@lrhn
Copy link
Member

lrhn commented Aug 19, 2022

Reading [a, b], it looks like a two-element list, so I expect it to match a two-element list.

I like the explicit over the implicit, so I'd go with [a, b, ...].
That is equivalent to List(length: >= 2, [0]: a, [1]: b) (if we allow [0] selectors where we otherwise allow getters, which I think we should. The [a, b] pattern would be equivalent to List(length: == 2, [0]: a, [1]: b).

(I'd actually also want ... for map patterns, so {"x": var a, "y": var b} would be equivalent to Map(length: == 2, ["x"]: var a, ["y"]: var b), and you need , ... to change to length: >= 2.)

@eernstg
Copy link
Member

eernstg commented Aug 19, 2022

Reading {0: a, 1: b}, it looks like a two-element map, so I expect it to match a two-element map. Right? And Point(x: 3, y: 4) should not match a ColorPoint? ;-)

@lrhn
Copy link
Member

lrhn commented Aug 19, 2022

I agree on the map. It's a structure pattern, so I expect the structure of the pattern to match against the structure of the value. I'd want {0: a, 1: b, ...} to match parts of bigger maps.

The type+extractor pattern is different, mainly because it doesn't give away the structure of the object, just its type. (Well, it looks like a constructor, if you squint, but it might not be a constructor which actually exists. Constructors are not isomorphic with object structure.)

Maybe we should change extractor patterns to use curly braces, Point{x: 3, y: 4}, instead of parentheses. That:

  • avoids conflicts with const patterns: case Point(x: 1, y: 1): ... can be either.
  • avoids looking like a constructor in general. (Which might have been the original goal, but I care about that goal.)
  • Still doesn't allow you to omit the type since what's left looks like a map literal, but
  • Might allow var {x: 1, y: 2} to match a Point value and infer the type. Might still conflict with map patterns in some contexts.

@mnordine
Copy link
Contributor

Not sure if my vote counts, but I prefer strict matching. I wouldn't expect [a, b] to match [1, 2, 3], and the explicit ... reads well to me.

@munificent
Copy link
Member Author

Maybe we should change extractor patterns to use curly braces, Point{x: 3, y: 4}, instead of parentheses.

I've considered that a number of times, but I've never convinced myself that I think the unfamiliar syntax is worth it.

  • avoids conflicts with const patterns: case Point(x: 1, y: 1): ... can be either.

True, but these are vanishingly rare in cases today which suggests that we shouldn't give that nice syntax to the thing almost no one uses.

  • avoids looking like a constructor in general. (Which might have been the original goal, but I care about that goal.)

I do like that it looks like a constructor. In particular, I think it's very important to have some syntax that looks like a classic algebraic datatype pattern as you would see in OCaml, Haskell, Scala, Rust, etc. We could come up with a different kind of "destructor" method that classes can define (like unapply() in Scala) and then have the constructor-ish pattern syntax map to that... but that would mean that the only classes you can use this on are ones that explicitly define that member.

Giving this nice syntax to getter-based extractors lets every Dart class in the wild use it (with the caveat that the arguments have to be named).

We can still add support for user-defined destructuring later (#2104) and have it reuse the same syntax by having a user-defined destructurer take precedence over the default behavior.

  • Still doesn't allow you to omit the type since what's left looks like a map literal, but
  • Might allow var {x: 1, y: 2} to match a Point value and infer the type. Might still conflict with map patterns in some contexts.

Yeah, I think if you don't have an identifier before {, you're always going to hit corners where it collides with map patterns.

Not sure if my vote counts

Your opinion definitely matters! That's a big part of why we do this design in the public repo and issue tracker. :)

@lrhn
Copy link
Member

lrhn commented Aug 26, 2022

Seems that C# 11 will introduce list patterns too.
Syntax:

  [var x, ..]  // length one or more, capture first
  [var x, .., var z]  // length two or more, capture first and last
  [var x, .. var y, var z] // length two or more, capture first, last, and sublist in the middle.

At most one .. can occur in the pattern.

The C# list pattern works on, I think, anything with int Length, Get(int index) and TSame Slice(int, int) operations (don't know the interface names, but includes arrays, Span and probably IList). It desugars as something like:

 [var x, .. var y, var z] --becomes-- {Length: >= 2, [0]: var x, [^1]: var z, Slice(1, -1): var y} 

(or what the syntax would be for doing inspection on the object).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
patterns Issues related to pattern matching.
Projects
None yet
Development

No branches or pull requests

4 participants