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 variable binding on String patterns #4158

Open
mateusfccp opened this issue Nov 10, 2024 · 13 comments
Open

Allow variable binding on String patterns #4158

mateusfccp opened this issue Nov 10, 2024 · 13 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@mateusfccp
Copy link
Contributor

I didn't find a duplicate for this issue, but GitHub's search is terrible, so this may be a duplicate.

I was thinking that we could allow variable bindings to String patterns. For instance:

switch (myRoute) {
  '/books/${final id}/author' => 'The author of the book with ID $id',
  '/books/${final id}' => 'Book with ID $id',
  '/books' => 'Books list',
  _ => 'Invalid route',
}

We could do it in every way that it's possible today to do with other patters, and if we could even extend it to support assignment.

final '/books/${bookId}' = myRoute; // We are sure that `myRoute` have the expected pattern. If not, a runtime error is thrown.
final id = int.parse(bookId);
@mateusfccp mateusfccp added the feature Proposed language feature that solves one or more problems label Nov 10, 2024
@lrhn
Copy link
Member

lrhn commented Nov 10, 2024

Sounds more like a job for a RegExp pattern, which matches into a Match object that you can match more against.

Say:

  if (o case (int i, ~pattern([0]: var s))) ...

You can get something like that behavior using extensions.

Pattern myPattern = RegExp(r"^/books/(.*)/author$");
extension on String {
  Match? get myMatch => myPatten.allMatches(this).firstOrNull;
}
extension on Match {
  Match get $0 => this[0]!; 
  Match? get $1 => this[1];
  // ...  
}
...
  if (myRoute case String(myMatch: Match($1: var i!)) ...

A little cumbersome, but possible.

Getting to introduce a new string-pattern formalism like

 '/books/${final id}/author'

It's likely to be insufficiently powerful for a bunch of problems, and still had to answer design questions like how

 '/books/${final id}/${final name}'

should match "/books/a/b/c".

If we can leverage any Pattern, you can choose how to do your matching, and use as powerful a pattern language as you want.

@rrousselGit
Copy link

rrousselGit commented Nov 10, 2024

A simpler approach is to use Uri

switch (Uri.tryParse(myRoute)) {
  Uri(pathSegments: ['books', final id, 'author']) => 'The author of the book with ID $id',
  Uri(pathSegments: ['books', final id]) => 'Book with ID $id',
  Uri(path: '/books') => 'Books list',
  _ => 'Invalid route',
}

@mateusfccp
Copy link
Contributor Author

A simpler approach is to use Uri

switch (Uri.tryParse(myRoute)) {
  Uri(pathSegments: ['books', final id, 'author']) => 'The author of the book with ID $id',
  Uri(pathSegments: ['books', final id]) => 'Book with ID $id',
  Uri(path: '/books') => 'Books list',
  _ => 'Invalid route',
}

This is just an example. The pattern would not be something specific for uris.

@rrousselGit
Copy link

I get that. But I think the logic can be used everywhere
As in: Avoid patterns on String, and instead convert them to a more complex object.


I wonder if a general solution could be to allowed defining variables inside case. Kind of like with for:

switch (string) {
  case Regex(...).allMatched(string) ; [final Match match]:
    print(match);
}

@tatumizer
Copy link

You can use named groups in your RegExp. Write a helper function that extracts all named groups from the allMatches and puts them into a map. Then write a switch against this map.
Will it work?

@mateusfccp
Copy link
Contributor Author

You can use named groups in your RegExp. Write a helper function that extracts all named groups from the allMatches and puts them into a map. Then write a switch against this map. Will it work?

I know there are workarounds for this, but I still think that a proper pattern matching against a String (or even better, against a Regex) would be valuable.

For instance, @lrhn workaround works [sic], but you have to make an extension for every Regex that you want to use as a pattern, which is not really scalable.

If we had Regex literals (which IIRC is a non-goal), this could be way more ergnonomic.

@tatumizer
Copy link

It would be almost impossible to design this feature correctly. E.g. consider your own motivating example:

switch (myRoute) {
  '/books/${final id}/author' => 'The author of the book with ID $id',
  '/books/${final id}' => 'Book with ID $id',
  '/books' => 'Books list',
  _ => 'Invalid route',
}

Suppose your route is /books/blah/blah/blah. Then the second line matches, with id = 'blah/blah/blah'. You never said that id cannot contain '/'.

@rrousselGit
Copy link

^ technically you could do

case "/books/${final id}" where !id.contains('/'):

@mateusfccp
Copy link
Contributor Author

It would be almost impossible to design this feature correctly. E.g. consider your own motivating example:

switch (myRoute) {
  '/books/${final id}/author' => 'The author of the book with ID $id',
  '/books/${final id}' => 'Book with ID $id',
  '/books' => 'Books list',
  _ => 'Invalid route',
}

Suppose your route is /books/blah/blah/blah. Then the second line matches, with id = 'blah/blah/blah'. You never said that id cannot contain '/'.

Yes, if the route was /books/blah/blah/blah, it would be matched by the second pattern, and this is expected. My example why only a naive example to show the case, but obviously the consumer of the feature would have to know how it works, just as it is today with other kinds of patterns.

As demonstrated by @rrousselGit, it would be possible to do it. And if we had access to Regexp patterns, than it would be even easier.

@tatumizer
Copy link

tatumizer commented Nov 11, 2024

In general, you will have to invent "when" conditions of increasng complexity, which, in general, will again be regexps. This is very inefficient and defeats the purpose of the feature. Named groups is more promising idea IMO. For this, you don't need a language feature - a library function will suffice.
@lrhn: care to opine?

@mateusfccp
Copy link
Contributor Author

@tatumizer Could you exemplify a library function that would work with as good ergonomics as a language feature?

All workarounds provided in this issue, although functional, are considerably cumbersome. The raison d'etre of this issue is not to say that it is impossible to do it today, but to have something better than what we have today.

@lrhn
Copy link
Member

lrhn commented Nov 11, 2024

but you have to make an extension for every Regex that you want to use as a pattern, which is not really scalable.

I did make myPattern non-final for a reason 😉.

I want a few more features for patterns. The primary one being generalized selectors in object patterns: Foo(bar(42).baz?.qux: ...) and (relevantly) String(match(myRE): Match([2]: var s)).

The last then just requires:

extension MatchPattern on String {
  Match? match(Pattern pattern) => pattern.allMatches(this).firstOrNull;
}

and a way to create constant patterns, which you can even do for RegExp using a lazy Expando.

 ...  case String(match(const ConstantRegExp(r"....")): Match([1]: var s, [2]: var t))

@tatumizer
Copy link

@mateusfccp : when you say "language feature", what feature do you mean? You havent' designed it.
E.g., there's an example of a pattern here:
https://api.flutter.dev/flutter/dart-core/RegExpMatch-class.html

Could you show how the program can process this regexp using a hypothetical language feature?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

4 participants