language icon indicating copy to clipboard operation
language copied to clipboard

Should we extend the named field shorthand syntax to handle null-check ("?") patterns?

Open munificent opened this issue 3 years ago • 6 comments

When destructuring a named field from an object, it's common to want to capture the resulting value in a variable with the same name:

var point = Point(1, 2);

var (x: x, y: y) = point;

To avoid the redundancy of having to say the same name twice (once for the field and once for the getter) many languages with name-based destructuring offer a shorthand syntax to let you elide one and infer it from the other. For example, in JavaScript:

const user = {
    id: 42,
    isVerified: true
};

const {id, isVerified} = user; // Same thing as:
// const {id: id, isVerified: isVerified} = user;

The patterns proposal for Dart has a similar shorthand (last paragraph). It looks like:

var (x:, y:) = point;

It's like a named field but with no subpattern. The : is needed to disambiguate it from a positional field.

However, using that shorthand doesn't work with what is likely to be a very common pattern:

  1. Destructure a value from a getter on an object that returns a nullable type.
  2. Test to see if the value is null.
  3. If not, bind it to a variable with the same name but a non-nullable type.

Using the current explicit syntax, for example:

class MaybeStringBox {
  String? string;
}

test(MaybeStringBox box) {
  switch (box) {
    case (string: var string?): print('Actual $string');
  }
}

Here, because of the ? null-check pattern, you can't use the shorthand syntax. Should we extend the shorthand syntax to allow that? One option is:

case (string:?): ...

So just allow a bare ? as the subpattern and treat that as implicitly defining a null-check pattern containing a variable subpattern whose name is the same as the field.

Or maybe:

case (:string?): ...

So instead of eliding the variable subpattern and inferring it from the field, we elide the field name and infer it from the subpattern. Hence, the : appears at the beginning instead of the end. Then we allow that subpattern to be a variable pattern or a null-check pattern contaning a variable pattern.

This syntax would likely extend to other patterns more gracefully, like:

case (:string!): // Null-assert pattern.
case (:string as String): // Cast pattern.

Basically, we'd allow any subpattern as long as it contains a variable subpattern whose name could be used to infer the named field from.

munificent avatar May 10 '22 23:05 munificent

I do have some problems reading the pattern foo? as meaning bind foo if the value is not null.

If anything, it looks like it matches the previous pattern or null, so you can do List(length: >3) x? which matches either a list of length 3, or null. (Although I'd prefer List(length: >3)? x for that).

Or use !? instead (read as *not null").

With that out of the way:

The (string:) pattern is short for (string: var string). It's a named property extractor plus a direct binding to that value, with no other shenanigans. If we start allowing further conditions in the test, why restrict it to ?.

Moving the : first does that. It makes :id whatnot equivalent to id: id whatnot, which allows anything after the id : id. (But how much can you put after the id of a binding?)

Maybe if you could take any pattern of the form prepattern id postpattern which checks something and binds the matched value to id, and add a colon to get prepattern id: postpattern, then that's equivalent tot id: prepattern id postpattern. It would mean that in a matcher, you need to write var x: instead of just x:, because x is not a binding matcher in a match context. It would allow the full generality of List<var T>(length: >4, [0]: var first)? elements:. Full generality sounds good :)

lrhn avatar May 11 '22 14:05 lrhn

I think it might be at least as important to have a non-null test (?) and non-null check ('!') pattern which is applicable to a composite pattern, and the ability to use it with an identifier could be secondary.

It is easy to use a guard in order to verify that a given variable declared by a pattern has a non-null value (so we can make a switch case fail and try the next one when the variable is null, if that's what we want).

Similarly, if we want a non-null check (!) we can use myVariable! after a declaration using a pattern, or in a switch case where we would have the ! somewhere in the body of the case.

But in the case where we want to perform the non-null test or non-null check during navigation into the structure, it might be a lot less convenient to obtain those tests/checks if there's a potential null along the way (and we don't have a non-null test/check pattern):

class C { C? next; }

void foo(C c) {
  var C(next: C(next: c1)) = c; // Won't work, each `next` could be null.
  var C(!next: C(!next: c2)) = c; // Dynamic checks, we _know_ they're not null.

  // Corresponding getter navigation.
  var c3 = c.next.next; // Compile-time error.
  var c4 = c!.next!.next; // OK, assuming it's true that they are not null.
}

So the point is basically that patterns are similar to getter navigation expressions, and they may very well need a null test or a null check along the way. We might also want one at the very end (c!.next!.next!), but the internal ones are actually more crucial than the one at the end.

eernstg avatar May 11 '22 19:05 eernstg

I think it might be at least as important to have a non-null test (?) and non-null check ('!') pattern which is applicable to a composite pattern, and the ability to use it with an identifier could be secondary.

I probably wasn't very clear in the issue description, but the proposal has this now and I have no plans to change it. You can already do:

// A nullable list of nullable records of nullable ints.
test(List<{int? x}?>? obj) {
  switch (obj) {
    case [(x: var x?)?]?: print('The list, record, and int were all not null.');
  }
}

But the proposal also has a shorthand for binding a variable to a named field with the same name in a record pattern:

// Long form:
case {someLongName: var someLongName, anotherLongOne: var anotherLongOne}: ...
// Equivalent shorthand:
case {someLongName:, anotherLongOne:}: ...

This issue is only about that latter shorthand. Since the shorthand syntax is desugaring and synthesizing an implicit variable subpattern, there's no way to wrap that variable subpattern in a null-check or null-assert pattern. This issue is about extending the shorthand to allow that.

After spending some time talking to @lrhn about it, we're leaning towards my second suggestion above:

This syntax would likely extend to other patterns more gracefully, like:

case (:var string!): // Null-assert pattern.
case (:var string as String): // Cast pattern.

Basically, instead of writing the field name and inferring the variable subpattern from it, you omit the field name (but not the :) and write the variable subpattern. The field name is then inferred from the variable. In addition, we allow that variable pattern to be wrapped in other patterns like ? or as that contain a single subpattern. In other words, if there's a single variable pattern embedded in the subpattern after :, you can omit the field name and infer it from that variable.

I think that's probably the right approach because it means that in refutable patterns in cases, you also have control over whether the variable is final or var, etc.

munificent avatar May 18 '22 23:05 munificent

I think that's probably the right approach

Yep, thanks!

eernstg avatar May 19 '22 08:05 eernstg

This seems reasonable, but I do wonder a bit how clear it is where to draw the line for inferring the field name. For example, presumably case (:var (a, b)) is an error. Presumably the pattern var string? parses as (var string)? right? So it's not quite the case that the rule is that the thing after the : must be a direct variable pattern, it's more like some subset of the patterns which name the actual value (possibly with some additional tests implied). So as patterns, type test patterns, nullability patterns... what else?

leafpetersen avatar May 25 '22 22:05 leafpetersen

I've got a pitch for this as part of this large overhaul of the pattern syntax. The rule is that you can omit the field name (but write the :) when the subpattern is a variable pattern or cast pattern, which may be wrapped in any number of null-check or null-assert patterns.

So basically, if the pattern is a variable binder wrapped in unary patterns, then it's fine. Anything else is forbidden.

munificent avatar Jul 21 '22 20:07 munificent

The revamped syntax handles this.

munificent avatar Aug 18 '22 22:08 munificent