Skip to content
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

Allow the type of an object pattern to be inferred when specified as _ #4124

Open
eernstg opened this issue Oct 8, 2024 · 44 comments
Open
Labels
patterns Issues related to pattern matching. small-feature A small feature which is relatively cheap to implement.

Comments

@eernstg
Copy link
Member

eernstg commented Oct 8, 2024

Ideas similar to this one have been discussed already when patterns were introduced, but I don't think there's an issue where the proposal has been presented in a concrete form.

We could make object patterns a bit more concise and convenient by using a wildcard _ to indicate the type of the object pattern when it can be inferred:

class const PairClassWithATerriblyLongName(int x, int y);

void main() {
  var pair = PairClassWithATerriblyLongName(2, 3);

  // Using the proposed feature:
  var number = switch (pair) {
    _(x: >= 0, y: >= 0) => 'first',
    _(x: < 0, y: >= 0) => 'second',
    _(x: < 0, y: < 0) => 'third',
    _(x: >= 0, y: < 0) => 'fourth',
  };

  // Compare this to the current syntax:
  number = switch (pair) {
    PairClassWithATerriblyLongName(x: >= 0, y: >= 0) => 'first',
    PairClassWithATerriblyLongName(x: < 0, y: >= 0) => 'second',
    PairClassWithATerriblyLongName(x: < 0, y: < 0) => 'third',
    PairClassWithATerriblyLongName(x: >= 0, y: < 0) => 'fourth',
  };

  print('Found a pair in the $number quadrant');
}

(I'm assuming that we have primary constructors, hopefully we will have them soon.)

It might be claimed that _ causes the code to be less readable than it would have been with the explicit type. However, it's worth noting that when the _ occurs repeatedly at the top level in cases like the above example then it is easy to see that every _ stands for the same type, and it must be the static type of the scrutinee. Similarly, when _ is used in a nested object pattern, e.g., PairClassWithATerriblyLongName(x: _(isEven: true)), the type of the nested object pattern could be rather easy to determine based on the enclosing patterns.

In any case, it will be a matter of style whether _ is an acceptably readable type on any given object pattern, and individual organizations and developers will choose a style for this, just like they are choosing a style for many other things.

The inferred type of an object pattern may include actual type arguments:

class MyClass<X> {
  final X x;
  MyClass(this.x);
}

void main() {
  var xs = MyClass(1); // Inferred as `MyClass<int>(1)`.
  switch (xs) {
    case MyClass(x: var y) when y.isEven:
      print('Got an A!');
  }
}

In the object pattern Myclass(x: var y), the type argument int is inferred. We can see that it is actually int because y.isEven is accepted with no errors, and also because y.isEven is an error when we have MyClass<num>(x: var y). We can also see that the inferred type isn't dynamic, because y.unknownMember is an error.

This should all work the same when we use the proposed feature:

void main() {
  var xs = MyClass(1); // Inferred as `MyClass<int>(1)`.
  switch (xs) {
    case _(x: var y) when y.isEven:
      print('Got an A!');
  }
}
@eernstg eernstg added small-feature A small feature which is relatively cheap to implement. patterns Issues related to pattern matching. labels Oct 8, 2024
@hydro63
Copy link

hydro63 commented Oct 8, 2024

Seems like a duplicate of #2563.
Edit: issue seems like revisiting the abandoned issue, and contributing possible new ideas.

@eernstg
Copy link
Member Author

eernstg commented Oct 8, 2024

Good catch, thanks!

It is similar, but that one was dropped because it used a syntax which is indistinguishable from record patterns. At least, I haven't heard anybody arguing in favor of the record-syntax-means-inferred-object-pattern proposal for a long, long time.

On the other hand, this comment from @samandmoore and this comment from @mraleph in #2563 already mention the syntax _(...).

This means that this issue is indeed an attempt to breathe new life into an idea which was mentioned in #2563, and also to give this idea a dedicated issue of its own.

I think this should be sufficient to justify having this issue.

@eernstg
Copy link
Member Author

eernstg commented Oct 8, 2024

@lrhn argues here that

The _ doesn't say "infer" to me. ...
I'd expect case _(foo: pattern) to match whatever the type, basically being an alias for dynamic.

I think the interpretation where an object pattern of the form _(...) means dynamic(...) is plausible, but not very useful. If you really want to use the type dynamic for the receiver of all the getter invocations implied by the contents of this object pattern then you might just as well write dynamic(...) in the first place, and allow _(...) to have a more useful semantics based on inference. I don't think we should spend useful syntactic capital on making dynamic(...) more convenient and less visually detectable than it is already. ;-)

If we interpret the _ as an "accept the default, whatever it is" mechanism then it does makes sense (to me, at least) that an object pattern has _ as a type, and it is implicitly replaced by the matched value type.

@julemand101
Copy link

Would we also be able to reduce in scenarios like:

void main() {
  Map<String, int> map = {};

  for (var MapEntry(:key, :value) in map.entries) {
    print('$key : $value');
  }
}

Where we would then be able to do:

  for (var _(:key, :value) in map.entries) {

?

@lrhn
Copy link
Member

lrhn commented Oct 8, 2024

I propose the syntax .(foo: var x).
That is, use . instead of _ to refer to the context type.

That matches the proposed .foo to refer to static members of the context type.

@tatumizer
Copy link

That is, use . instead of . to refer to the context type.

Tangentially related: if dot stands for the context type, this opens the way to defining a block expression syntax in two steps:

  1. Allow the syntax like var x = int { ...; 42}; where int {...} denotes a block expression returning int
  2. Allow dot to stand for the "inferred type" of the above, which results in var x = .{ ...; 42};

@TekExplorer
Copy link

TekExplorer commented Oct 8, 2024

I'm not really a fan of .

Its kinda small and easily forgotten, whereas _ to me reads "fill in" much like we use for "sign on the line" and... uh... homework.

Alternatively, it can also mean "infer" which could be useful for something like Foo<Bar, _> where we may want to specify some type parameters, but let the rest be inferred.

. would instead find its home in constructor invocation or enum values, such as MyEnum x = .thing; or Foo foo = .parse('...')
where it means "invoke on inferred type", or, literally, _.staticThing
(in other words, static members)

notice the difference? _ is the inferred type, while . is an invocation on that type.

final int i = .parse('1');

enum X {uno, dos}
final X x = .uno;

class const Foo(String str);

void thing() {
  // static member. `.new(...)` becomes a shorthand.
  final Foo foo = .new(switch (x) {
    // static members
    .uno => 'one',
    .dos => 'two', // implies `_.dos` which implies `X.dos`
  });

  return switch (foo /* as Foo */) {
    // infer type `_ = Foo`
    _(str: 'one') => 1,
    _(str: 'two') => 2,
    _(:final str) => str.length,
  };
}

after all, . isn't a type, its an invocation on... something. if we let that something be _ where _ is the type, then it just works

Note: turning _.staticMember into .staticMember can come later

@mateusfccp
Copy link
Contributor

mateusfccp commented Oct 8, 2024

I know this approach is probably easier to parse, but syntax wise I prefer #2563.

for (var _(:key, :value) in map.entries) {}
for (var (:key, :value) in map.entries) {}

That said, if we go with this approach of using a symbol to denote "infer this", I agree with @lrhn that _ does not a good job, even though there are languages that already use them for this kind of thing (for instance, Kind2 uses it for inferring types).

_ has already a wildcard semantic that is very loaded when used with identifiers, and now with patterns wildcards. I think we should think on alternatives.

@TekExplorer
Copy link

TekExplorer commented Oct 8, 2024

we cant use the second option because that's a record.

i don't see how _ is overloaded. AFAIK its used as 1. "pattern match any" and later as 2. non-binding variable.

Since we're talking about pattern matching, 2 is irrelevant. additionally, _(...) => is distinct from _ =>, meaning its brand new syntax.

for the case of map.entries, we really should just look to creating an extension that provides an iterable of records instead.

extension KVEntries<K, V> on Map<K, V> {
  Iterable<(K, V)> get kv => entries.map((e) => (e.key, e.value));
}

The "best" solution for this in particular is to turn MapEntry into a record in actuality, but that's a different issue

@mateusfccp
Copy link
Contributor

we cant use the second option because that's a record.

It's not impossible. If you read the discussion in the aforementioned issue, @munificent proposes disambiguation rules that can work, and IMO it would be better.

Since we're talking about pattern matching, 2 is irrelevant.

It's not irrelevant. Even if context can disambiguate syntax, pragmatically speaking, the readability of the syntax is better and easier to quickly grasp if each syntax has a single meaning. This is inevitable in some cases, but this is a case where it can be avoided.

@TekExplorer
Copy link

By that logic we can't use . either, since it would then have multiple different meanings.

By using _() it does not overlap with _-as-wildcard nor -as-nonbinding-variable because neither can have parens the way this does, and it also cleanly lines up with explicitly requesting inference in type parameters, which is proposed to use <> which has similar semantics to this, where it means "the implied type"

If we can just use record types outright, then that could be neat, but it feels problematic. It becomes unclear through scanning if the value is a record or an object of implied type.

@mateusfccp
Copy link
Contributor

mateusfccp commented Oct 9, 2024

By that logic we can't use . either, since it would then have multiple different meanings.

Yeah, I never said I think . is a good choice.

Not as terse as what was suggested here, but if we consider something like #3616, we could do

for (var .new(:key, :value) in map.entries) {}

@TekExplorer
Copy link

Thats interesting, but wouldn't work, because we aren't checking constructors, (even though it looks like it) we're pattern matching.

Therefore, .new() gives the wrong impression.

I am of the opinion that based on the currently proposed options, _ (which has synergy with explicit inference on types) or the objects-sort-of-as-record thing (which has ambiguity

If anyone has better ideas than this (or hell, worse ones - just to get those juices flowing) then let's see 'em!

@eernstg
Copy link
Member Author

eernstg commented Oct 9, 2024

I would certainly be worried about reusing the syntax for record patterns (at least for the ones with named getters) to denote an object pattern with an inferred type. It might be unambiguous for an analyzer and a compiler, but to me it looks a lot like a footgun. For example:

import 'dart:math';

void main() {
  var s = 'Hello!';
  switch (s) {
    case (length: 6): print('Reached: Object pattern');
  }

  var r = (length: 6);
  switch (r) {
    case (length: 6): print('Reached: Record pattern');
  }
  
  dynamic d = Random().nextBool() ? s : r;
  switch (d) {
    case (length: 6): print('Reached?');
  }
}

For the last one, the matched value type is dynamic, so (length: 6) is the object pattern dynamic(length: 6), and it will match in both cases (r.length will happily return 6, also for a dynamic invocation, and so will s.length).

This would presumably imply that some patterns intended as record patterns will be implicitly reinterpreted as dynamic object patterns, which again implies that they will match the records they are intended to match, plus all records that have the required getters satisfying the given requirements, plus any number of additional getters: It's not a record pattern any more, and (length: 6, width: 8) will happily return 6 if we call .length on it, and object patterns do not require that we mention every getter, so (length: 6, width: 8) will match the dynamic object pattern even though it wouldn't have matched the record pattern.

Similarly, we can have spurious matches where (length: 6) will match an instance of any class that has a length getter that returns 6. Again, this pollutes the set of matched objects, if the intention is that the pattern should only match records.

I don't think this kind of confusion will help anybody. So I'll continue to recommend that we use an identifier, preferably _, to indicate explicitly that a given pattern is an object pattern, and not a record pattern.

Note that _() can be parsed as an object pattern already today ("no new syntax"). We're just using _ to indicate that this particular bit of information has been omitted (and it will be filled in by the analyzer/compiler).

The syntax .() would probably work as well, but it is new syntax (that is, it reduces the room which is available for other syntactic enhancements in the future), and I don't think it's more readable.

@lrhn
Copy link
Member

lrhn commented Oct 9, 2024

Note that _() can be parsed as an object pattern already today ("no new syntax").

It's "no new grammar", but it changes the existing meaning of the term, treating _ as a contextual keyword instead of an identifier, so I'd argue it is "new syntax". It parses differently. (Or maybe it doesn't, and it's just interpreted differently. Not sure I like that approach. "This identifier, in this particular position of the abstract syntax tree, wasn't an identifier after all.")

I can probably get used to it, but my initial reading of _(foo: var x) is not that _ matches a specific omitted type, but that it matches any type. That's what a wildcard does.

class _ {
  int get secret => 42;
}
bool isItSecret(Object? o) {
   if (o case _(secret: 42)) return true;
   print("Not secret");
   return false;
}
void main() {
  print(isItSecret((secret: 42))); // "Not secret", "false"
  print(isItSecret(_())); // "true"
}

I'm not sure a type being named _ is a good idea, and making it not work with object patterns is not a big loss or significant breaking change.

@hydro63
Copy link

hydro63 commented Oct 9, 2024

Both .() and _() are effectively the same syntax with the only difference being the differentiating token. I would say that both are equally as unreadable for me, regardless of your specific reasoning for the differentiating token. You can argue for both, since both . and _ have their uses in similar contexts. But that doesn't make it any more readable and more understandable.

And so i would like to propose similar, but imo more readable variation of this syntax, where instead of using . or _ to differentiate the pattern type, we would use <>(). It is based on syntax used for type definition, whether in <int>[] or in generics, and i think it represents the infer type here point better than . and _.

for (var <>(:key, :value) in map.entries) {}
if (o case <>(secret: 42)) return true;

return switch (foo /* as Foo */) {
  // infer type `<> = Foo`
  <>(str: 'one') => 1,
  <>(str: 'two') => 2,
  <>(:final str) => str.length,
};

You can say that it is kind of long, but i'd say that compared to the full class name, it is still short, and it retains some readibility.

anyway, this proposal is more of what-if, to show a different possible direction for the syntax

@munificent
Copy link
Member

It is similar, but that one was dropped because it used a syntax which is indistinguishable from record patterns. At least, I haven't heard anybody arguing in favor of the record-syntax-means-inferred-object-pattern proposal for a long, long time.

I still very much like #2563 and would like to do it. I've just been too busy with other stuff to push on it.

Agree with your earlier comment that #2563 could be a footgun in a couple of places, especially when the scrutinee has type dynamic, but I suspect those places are rare in practice. I think it will do the exactly right thing most of the time.

If only ASCII had more bracket characters so we could have come up with something different for records...

@TekExplorer
Copy link

TekExplorer commented Oct 9, 2024

@lrhn I'd argue that "match anything" isnt to different to "match this".
if we did:

switch (thing as T) {
  T it => it // is T
  // vs
  _ => thing // is T
}

you find they match the same. and if we did Object? it => then its free to upcast to T because we already know the tighter type.

I also really like _ because, as mentioned before, it synergizes with the explicit inference issue (which I'm having a hard time finding, but I do know it exists)
such that

class Foo<T extends Bar<R>, R> {}
class Bar<R> {}

final foo = Foo<Bar<int>, _>(); // Foo<Bar<int>, int>

class BarString extends Bar<String> {}
final foo2 = Foo<BarString, _>(); // Foo<BarString, String>

which has very similar meaning to using _ here too.

It's a placeholder, and what's a wildcard if not a placeholder?

To me, it means "something goes here". or perhaps "anything goes here"

But because we have a type system, we happen to know that we can promote "something/anything" to the context type.

Its nothing more than an expansion of its existing usage.

ps: <>(...) is interesting, but doesn't really work. its not nearly as intuitive and implies that we're trying to specify type parameters to... a record? huh? it conflicts with <T>[], <T>{} and <K, V>{}, the other literals.

pss: class _ {} wont be possible once _ becomes non-binding as an identifier anyway (or... it should, else it would be inconsistant.)

@lrhn
Copy link
Member

lrhn commented Oct 9, 2024

class _ {} wont be possible once _ becomes non-binding as an identifier anyway (or... it should, else it would be inconsistant.)

It's inconsistent.
Or rather, it's consistent in that _ is only non-binding for local declarations, and import prefixes (which are file-local), not name-spaced names.

@TekExplorer
Copy link

class _ {} wont be possible once _ becomes non-binding as an identifier anyway (or... it should, else it would be inconsistant.)

It's inconsistent.
Or rather, it's consistent in that _ is only non-binding for local declarations, and import prefixes (which are file-local), not name-spaced names.

...Then I would first recommend making _ universally non-binding

How does that even interact with the wildcard? What happens if you have a literal type by that name? A constant value by that name?

What do you mean by "local" declarations? Isn't all usage of _ inherently local? Or do you mean that top level declarations local to the file can still use it, but classes can't have a member named such? Or is it just functions and import x as _?

@hydro63
Copy link

hydro63 commented Oct 10, 2024

Honestly speaking, i don't understand why _ is even allowed to be a valid identifier. It can cause many bugs if used incorrectly, makes understanding patterns a lot more difficult and limits the way the Dart language can be improved.

Also, i also hate the fact that $ is also a valid identifier. While it causes less problems than _, it also severely limits the available syntax to use in the future.

As such, wouldn't it be better to disallow identieffiers _ and $ from being valid identifiers (both _a and $a are still possible)? I know that it would possibly break somebodies code, but that can be solved with easy find-and-replace. Also, why are you using _ and $ as identifiers??

I think that the new possibilities for syntax gained this way completely compensate for breaking someones code, mainly because the fix is to just do a simple find-and-replace.

@eernstg
Copy link
Member Author

eernstg commented Oct 10, 2024

OK, we can fight over syntax forever, so let me ask about another thing, assuming that there is some syntactic marker to say "this is an object pattern whose type is inferred (not a record pattern)":

Would you recommend using such a feature liberally, all over the place? Or would you consider it to be detrimental to the readability of the code, only to be used when it's absolutely obvious which type is being inferred?

@lrhn
Copy link
Member

lrhn commented Oct 10, 2024

Assuming some syntax for not having to write the matched value type in an object pattern:

Would you recommend using such a feature liberally, all over the place?

I don't know if I'd recommend anything, but I would use it liberally, all over the place. You can take that as endorsement, even if it's not a recommendation.

I'd probably also only use it if it is obvious which type is being inferred, but it usually is. So, all over the place.

A switch is often one of two kinds:

  • A subtype-switch, for sealed types, and then you probably want to write a type in each pattern, except for the default case. I can see the default being case _: ... and if there is some member on the sealed supertype, then case _(: var foo) does read well as an extension on that.
  • A value match, where all the patterns have the same type, and you match on different values of that type. (Those can be combined, so a subtype switch has a number of value switches for the same subtype, but it still needs to mention the subtype then.) For a pure value match, the type being switched on is usually obvious, and having to repeat it in patterns is actually slightly confusing. It looks like it's asking if this thing is a Foo, even though it can't be anything else.

The third option is a "parsing switch" that checks a value, often of type Object?, for being one of a number of allowed types, like JSON values. Like for a subtype-switch, the matched value type is going to be useless in an object pattern, so you wouldn't use the feature there.

In declaration patters and plain destructuring, the matched value type is usually also obvious.
The canonical example is for (var _(:key, :value) in map.entries). It's obviously a MapEntry from map.

So I'd use it every time I do a destructuring or a value match, a switch where all the cases have the same type, where switching on the matched value type is meaningful.

@hydro63
Copy link

hydro63 commented Oct 10, 2024

Considering that the inferred type pattern would be used to destructure an object, i would only allow it in cases where the type is obvious, since the developer needs to directly interact with the object in question.

I wouldn't allow it in unreadable cases, purely because while you can just hover over the variable to get it's type in IDE, that doesn't work in Github, Gitlab, StackOverflow ... I want it to be readable because much of the work of developers is not done in IDE.

Edit - i basically agree with the usecases that @lrhn outlined, all of which are very easy to understand

@mateusfccp
Copy link
Contributor

Could we go even further and intersect types in our inference?

For instance:

void main() {
  final Foo foo = getFoo();
  switch (foo) {
    _(:final a) => ..., // Equivalent to Bar(:final a) || Baz(:final a) => ...
    Qux(:final b) => ...,
  }
}

sealed class Foo {}

final class Bar {
  final int a;
}

final class Baz {
  final int a;
}

final class Qux {
  final int b;
}

@hydro63
Copy link

hydro63 commented Oct 10, 2024

Could we go even further and intersect types in our inference?

I don't like that. it is imo unintuitive and you can get the same by correctly ordering the cases (just like you have to do now)

void main() {
  final Foo foo = getFoo();
  // same as Bar(:final a) || Baz(:final a), because of early Qux return
  switch (foo) {
    Qux(:final b) => ...,
    _(:final a) => ..., 
  }
}

sealed class Foo {}

final class Bar {
  final int a;
}

final class Baz {
  final int a;
}

final class Qux {
  final int b;
}

@mateusfccp
Copy link
Contributor

Could we go even further and intersect types in our inference?

I don't like that. it is imo unintuitive and you can get the same by correctly ordering the cases (just like you have to do now)

void main() {
  final Foo foo = getFoo();
  // same as Bar(:final a) || Baz(:final a), because of early Qux return
  switch (foo) {
    Qux(:final b) => ...,
    _(:final a) => ..., 
  }
}

sealed class Foo {}

final class Bar {
  final int a;
}

final class Baz {
  final int a;
}

final class Qux {
  final int b;
}

Does this code work today?

@hydro63
Copy link

hydro63 commented Oct 10, 2024

@mateusfccp yes it does (after adding all the definitions). here is the correct code

void main() {
  final Foo foo = getFoo();
  // same as Bar(:final a) || Baz(:final a), because of early Qux return
  print(switch (foo) {
    Qux(:final b) => "its Qux",
    _ => "its else"
  });
}

Foo getFoo(){
  return Qux();
}

sealed class Foo {
  int a = 1;
}

final class Bar extends Foo {
}

final class Baz extends Foo {
  Baz(){
    super.a = 2;
  }
}

final class Qux extends Foo{
  final int b = 0;
}

Run it in dartpad if you want.

@mateusfccp
Copy link
Contributor

@mateusfccp yes it does (after adding all the definitions). here is the correct code

void main() {
  final Foo foo = getFoo();
  // same as Bar(:final a) || Baz(:final a), because of early Qux return
  print(switch (foo) {
    Qux(:final b) => "its Qux",
    _ => "its else"
  });
}

Foo getFoo(){
  return Qux();
}

sealed class Foo {
  int a = 1;
}

final class Bar extends Foo {
}

final class Baz extends Foo {
  Baz(){
    super.a = 2;
  }
}

final class Qux extends Foo{
  final int b = 0;
}

Run it in dartpad if you want.

Yes, but this is completely different from what I suggested.

But thinking again, I don't think it would be too useful, useless they were unrelated types that could be exhaustively checked.

@tatumizer
Copy link

switch (foo) {
    Qux(:final b) => ...,
    _(:final a) => ..., 
}

To me, the expression _(:final a) => ... means: "any class having a getter a". Underscore already carries the vibes of "whatever", "I don't care what it is" etc, and never implies anything specific. You need another symbol or keyword that means "inferred".
Leading dot like in .(:final a) feels more appropriate to me, but I'm not sure the use case is common enough to warrant a special feature.

@TekExplorer
Copy link

TekExplorer commented Oct 12, 2024

No, "any class using getter a" would be dynamic(:final a) which would throw if a doesn't exist because we can't pattern match on the existence of methods.

It feels like some of you might be mistaking _ for dynamic.

By definition, _() can't be "any object" because we already have that in dynamic(). It's only other possible definition is for it to be the inferred type.

_-the-wildcard would be exactly equivalent to _()-the-type-match

@tatumizer
Copy link

_-the-wildcard would be exactly equivalent to _()-the-type-match

No. The underscore is not always a wildcard. Sometimes it's a valid identifier. And it would be treated as a valid, very concrete, identifier in the case statement like _(:final a) => ... . (I overlooked this myself while writing a comment, but it only adds to the argument that the underscore in this context cannot mean what you proposed it to mean)
Tested on dartpad:

class _ {
  final a=1;
}
main() {
  var x = _();
  switch(x) {
    case _(:final a): 
      print(a);  
  }  
}

@hydro63
Copy link

hydro63 commented Oct 12, 2024

DISCLAIMER: this is more of a throwaway comment

Leading dot like in .(:final a) feels more appropriate to me

No, "any class using getter a" would be dynamic(:final a) ...

Welcome to language design, where ideas are spawning so quickly, that rather than creating new issue, people just argue about it in a sort of (not really) related issue.

Welcome to language design, where we don't argue behaviour, but cosmetics.

Please let's get back to the issue at hand and discuss the allowed behaviour and the implementation so that it is as easy for developer to integrate into existing code as possible.

Also, Dart doesn't have structural typing, aka "anything with getter a", and adding it only to this single pattern, that is already more difficult to read, is a bad decision. Furthermore, there were already some proposals for structural typing, but afaik none of them have gotten to the specification stage. That means that Dart team is unlikely to add structural typing to the Dart regardless, which means that "anything with getter a" is both pointless and extremely niche proposal.

From application design, this functionality is what classes are for. They give you both the structure and the context of the data. And with that you can already do "anything* with getter a".

*anything in with the correct context / class

@tatumizer
Copy link

@hydro63 : I found your earlier post where you proposed <> syntax. I added it to a couple of my comments in other threads.
I think it's a good, undeservedly overlooked, syntax candidate.
.

@munificent
Copy link
Member

Would you recommend using such a feature liberally, all over the place? Or would you consider it to be detrimental to the readability of the code, only to be used when it's absolutely obvious which type is being inferred?

I would, yes.

A valid criticism of the current pattern syntax is that if you just want to destructure an object but don't want to do a type test, there's no real way to express that. Say you get a string and want to destructure the length:

String getThing() => ...

main() {
  switch (getThing()) {
    case String(:var length): ...
  }
}

Later, someone changes getThing():

Object getThing() => ...

Now the behavior of that switch is different. It doesn't have a compile error (because it's a switch statement, so doesn't have to be exhaustive), but now instead of simply destructuring, it also does a type test.

There's no way to say "just destructure the object but don't type test". If we had "inferred" object patterns, that would give you a way to express that. Then, when you see a named object pattern, it's clear that the intent is to type test and downcast.

So, yes, I would use inferred object patterns liberally whenever I don't want to type test.

@eernstg
Copy link
Member Author

eernstg commented Oct 16, 2024

@tatumizer wrote:

The underscore is not always a wildcard. Sometimes it's a valid identifier.

I'd very much prefer if it is never used as such. We allow it for non-local declarations (including class _ {}, top-level variables, import prefixes, etc) because it is less breaking than the alternative. However, I hope that those usages will just go away over time (and they are presumably almost non-existent already). This means that _ can be used to indicate omission of information at this time, and at some point in the future it might only be used in that manner.

Here are some cases where _ indicates omission of information:

We may omit the name of a local variable or parameter because it shouldn't be accessed anyway. We may omit the name from an identifier pattern by using the identifier _ (which makes it a wildcard pattern).

It has been suggested that an actual argument for an optional positional parameter could be omitted even if it is not the last positional argument (so we would call foo(1, _, 3), which would pass the default value to the second parameter). We could omit the type of an object pattern as a request for obtaining this type from the matched value (that's this proposal). We could omit an actual type argument as a request for obtaining that type argument by type inference (#3963).

As this illustrates, the usages of _ that have already been added to the language plus the proposals that I'm aware of match up quite nicely with the description that "_ indicates omission of information". The omission of information may be the point (as for the wildcarded parameter), or the information may be obtained in a specified manner (use the default value, the matched value type, the inferred type argument).

I think this perspective makes _ a natural choice for the abbreviated object pattern: A pattern of the form _(p) where p is derived from <patternFields> is an object pattern (so says the parser) where the type has been omitted (so says _). The most useful semantics in that situation is that we obtain the type from the matched value type, so that's what we specify it to do. In particular, I don't want to promote unsafe typing by using this syntax to mean dynamic(p).

Of course, we could use a syntax like .(p), but . on its own doesn't otherwise indicate omission of information. You could say that .identifier gives . such a role, but it isn't replaced by the omitted element, the omitted element is prepended: So if we just follow the approach used with .identifier then it means TheMatchedValueType.(p), but it should actually mean TheMatchedValueType(p). Replacing . by an omitted element doesn't happen anywhere else in the language.

Similarly, <> would very nicely indicate that a list of actual type arguments has been omitted, but it would be somewhat confusing because <>(p) could mean C<Some, Type, Arguments>(p) when the matched value type is C<Some, Type, Arguments>, or it could mean C(p) when the matched value type is a simple non-generic type. So the syntax seems to promise that we'll get some actual type arguments, but perhaps the type is non-generic, and the syntax doesn't indicate that the type itself is omitted, it only shows the type argument list.

Hence, I still prefer _.

@lrhn
Copy link
Member

lrhn commented Oct 16, 2024

I think I'd prefer to keep using _ mainly in patterns, not in expressions.

Doesn't mean you should use _ as an identifier anyway, but it's not because I want to use it for other things in expressions. (But don't take away my extension type const Foo._(Bar _) { .... } declaration. I like it short!)

I'm warming up to a pattern of _(length: 4) by seeing it as an embellishment on the pattern _ ("match the matched value at the matched value type") with extra checks ("if its .length is 4").

Then I'd still use . in expressions to represent access to an implicit context-provided something
(.enumValue on the surrounding context type, .[v1, v2] to emit into a surrounding collection, with ."...${...}..." if we get string elements).

For omitting arguments, I'd use foo(1,, 3) to omit the second positional argument. No extra syntax needed for no argument.
Not sure if I'd do the same for type arguments, but Map<String,> feels better than just Map<String>. I think it's more likely that omitted type arguments will be taken as a request for inference than a request for using a default value, even if type parameters could be optional and have default values to fall back on if nothing can be inferred.
It's also less error prone if you can't accidentally pass too few type arguments and have the rest inferred (to dynamic, no less!), but on the other hand, it would allow you to add more type arguments later if an existing Foo<int> stays valid.

Ok, _(...) is fine. Let's ship it!

And <> is right out.

@tatumizer
Copy link

tatumizer commented Oct 16, 2024

If we let _ stand for "omitted" ("inferred") type, then consider

var p = Person(name: 'Joe', ...);
switch (p) {
  case _(name: 'Joe'): ...
}

If this is a valid program, then consider

Person p = _(name: 'Joe');

Is this a valid program? If not, why not?

@eernstg
Copy link
Member Author

eernstg commented Oct 16, 2024

If this is a valid program, then consider

Person p = _(name: 'Joe');

That's tempting! 😄

We just discussed shortcuts in the language team. Several of the proposals about shortcuts would allow this:

Person p = .new(name: 'Joe'); // Call the constructor whose name is `Person` and `Person.new`.

I would actually expect .new(...) expressions to be used quite frequently in libraries where the preferred style is to have explicit type annotations.

@tatumizer
Copy link

Tempting? Then what stops you from succumbing to the temptation? 😄
Especially given your preference for the same syntax in case _(name: 'Joe').

Really, why bother with .new(...) if you can write _(...)?
My speculation is that in the absence of "shortcut" problem (let's assume the problem doesn't exist) everybody would gladly accept Person p = _(name: 'Joe');. Is this true?
Then we get a hint to the consistent syntax of the shortcuts:
_.id would stand for ContextType.id,
But to keep "the sheep safe and the wolves well-fed" (so to speak), we can allow _.id to be further abbreviated to simply .id.
Wouldn't it be a good compromise?

@mateusfccp
Copy link
Contributor

If we let _ stand for "omitted" ("inferred") type, then consider

var p = Person(name: 'Joe', ...);
switch (p) {
  case _(name: 'Joe'): ...
}

If this is a valid program, then consider

Person p = _(name: 'Joe');

Is this a valid program? If not, why not?

What if we had this?

// Library
sealed class Foo {}

final class Bar implements Foo {
  const Bar(String a);
}

final class Baz implements Foo {
  const Baz(int b);
}
// Client

Foo f = _("String"); // Imply Bar
Foo f2 = _(10); // Imply Baz

We would want it to be a static error when the constructors have conflicting signatures, though.

@hydro63
Copy link

hydro63 commented Oct 16, 2024

Foo f = _("String"); // Imply Bar
Foo f2 = _(10); // Imply Baz

I don't like this. I am against inferring subclasses. This feels like a footgun waiting to be fired.

Right now, it is somewhat easy to understand what is being inferred, but if you had more than one argument, optional argument, or named parameters in there, it would take a lot of effort to guess what is inferred. And that's not even talking about having more than 2 subclasses that extend the desired class.

You could say, that it isn't a problem if it's not obvious what is inferred, since the IDE infers can infer it for the developer, but that's a really stupid argument. Ideally, you don't want to rely on IDE too much, since significant portion of developer's time is not being spent in IDE. For example, code reviews, fix searching (StackOverflow), or reading documentation are done in a browser, not an IDE. You want the developer to be able to read stuff that's just plain text, with no hints.

@natebosch
Copy link
Member

I think either .( or _( (or even #() would end up working fine. I do think that having some syntax to express this is less confusing than having the same appearance as a record pattern.

I would personally use this frequently, and my hunch is that it would get enough usage in general to become the recommended pattern. I'd imagine arguments for or against using this will be similar to the arguments for using omit_local_variable_types or always_specify_types.

I don't think we should expand the scope of this feature, or this discussion, beyond pattern matching.

@eernstg
Copy link
Member Author

eernstg commented Oct 17, 2024

OK, I agree that we should stick to patterns in this issue. Discussions about constructor invocations using _(...) and similar forms would perhaps fit better in #357.

In any case, it's a very interesting idea that there are additional syntactic positions where we could use _ to mean "information has been omitted here, but we have a rule that specifies how to get it"! All other things equal, it's highly valuable to aim for anything that makes the language easier to reason about because it is more consistent, in some sense that fits a human brain.

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. small-feature A small feature which is relatively cheap to implement.
Projects
None yet
Development

No branches or pull requests

9 participants