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

Extensible pattern-matching #1047

Open
DartBot opened this issue May 8, 2012 · 22 comments
Open

Extensible pattern-matching #1047

DartBot opened this issue May 8, 2012 · 22 comments
Labels
request Requests to resolve a particular developer problem

Comments

@DartBot
Copy link

DartBot commented May 8, 2012

This issue was originally filed by sammcca...@google.com


It would be nice to match a value against patterns with a terse syntax.

This is similar to 'switch' (and could possibly share the same syntax) but instead of exact-match, the semantics would be determined by the pattern used.

When the pattern is a RegExp, it would match strings that conform to it.
A Type would match instances of that type.
User classes should be able to implement their own pattern behaviour.
Any value without special pattern behaviour would match using ==

for example:

switch (value) {
  match Exception: throw value;
  match const RegExp('\s+') return 'whitespace';
  match 42: return 'the answer';
  default: return 'unrecognized';
}

There was some discussion here: https://groups.google.com/a/dartlang.org/group/misc/browse_thread/thread/a3e75c24c6dd4f03/

A pattern might also be able provide values which can be bound to names (e.g. the Match object corresponding to a regexp match).

@DartBot
Copy link
Author

DartBot commented May 8, 2012

This comment was originally written by sammcca...@google.com


Hopefully this could also be unified with the pattern matching of exceptions in the 'catch' construct.

@gbracha
Copy link

gbracha commented May 8, 2012

We may act on this in the future.


Set owner to @gbracha.
Removed Type-Defect label.
Added Type-Enhancement, Accepted labels.

@anders-sandholm
Copy link

Added Area-Language label.

@gbracha
Copy link

gbracha commented May 24, 2012

Added this to the Later milestone.

@DartBot
Copy link
Author

DartBot commented Jun 18, 2012

This comment was originally written by @seaneagan


issue dart-lang/sdk#3722 proposes to have the "match" keyword above accept any Predicate defined as:

typedef bool Predicate<T>(T item);

@DartBot
Copy link
Author

DartBot commented Jun 18, 2012

This comment was originally written by @seaneagan


Also, for type patterns currently we are stuck with doing "new isInstanceOf<int>" or "new isInstanceOf<BadNumberFormatException>". If types were first class and extended Predicate, then we could have:

switch(e) {
  match(int) ...
  match(String) ...
  default ...
}

and similarly for catch statements:

catch(e) {
  match(NullPointerException) ...
  match(BadNumberFormatException) ...
  default ...
}

@DartBot
Copy link
Author

DartBot commented Apr 17, 2013

This comment was originally written by @tomochikahara


I think Enum pattern matching in Haxe is easy to add to Dart with enum.

enum Card { number(num n), jack, queen, king, joker }

num toNumber(Card card) => switch(card) {
    case number(n): n;
    case jack: 11;
    case queen: 12;
    case king: 12;
    case _ : -1;
};

@kasperl
Copy link

kasperl commented Jul 10, 2014

Removed this from the Later milestone.
Added Oldschool-Milestone-Later label.

@kasperl
Copy link

kasperl commented Aug 4, 2014

Removed Oldschool-Milestone-Later label.

@gbracha
Copy link

gbracha commented Dec 23, 2014

Issue dart-lang/sdk#21847 has been merged into this issue.

@Aetet
Copy link

Aetet commented Nov 6, 2016

Is matcher ready for daily use at regular code? Or it should be placed only at tests?

@lrhn
Copy link
Member

lrhn commented Nov 7, 2016

The matcher class is definitely well tested, but it's built for testing, not performance. I don't think it'll make a good substitute for language-level pattern matching.

@ghost
Copy link

ghost commented Jan 2, 2019

That could be more beautiful

switch (value) {
  on Exception: throw value;
  on const RegExp(@­"\s+") return 'whitespace';
  on 42: return 'the answer';
  otherwise: return 'unrecognized';
}

And to make it possible you can use Unification*

@henry-hz
Copy link

henry-hz commented Apr 1, 2020

Why not pattern matching in Haskell/Erlang Style, on the function arguments ?

@He-Pin
Copy link

He-Pin commented May 11, 2020

I like the Java or Scala style.

@henry-hz
Copy link

@hepin1989 , so you prefer to solve, let's say the problem n. 1 (of the 99 problems like:

    <T> T f1(List<T> list) {
        if (list.isEmpty()) throw new NoSuchElementException("List is empty");
        List<T> elements = list.tail();
        List<T> result = list;
        while (elements.nonEmpty()) {
            result = elements;
            elements = elements.tail();
        }
        return result.head();
    }

instead of:

f1 :: [a] -> a
f1 [x] = x
f1 (_:xs) = f1 xs

:)

@kevmoo
Copy link
Member

kevmoo commented Jun 25, 2020

@leafpetersen @munificent @lrhn – should this be in the language repo?

@lrhn lrhn transferred this issue from dart-lang/sdk Jun 25, 2020
@lrhn lrhn added the request Requests to resolve a particular developer problem label Jun 30, 2020
@FranckRJ
Copy link

FranckRJ commented Jul 11, 2020

#1047 (comment) @henry-hz your first example is too long and your second is too hard to read, it's too different from what most people know. Typing less caracter doesn't make it easier.

What about the Kotlin style ?

fun f1(list: List): List = when(list) {
        list.isEmpty() -> throw Stuff
        list.tail().isEmpty() -> list.head()
        else -> f1(list.tail())
    }

(it may contain errors, i'm on mobile and don't do Kotlin often)

You have to type more than your second example, but i think any programmer can understand this piece of code, unlike yours.

@devtronic
Copy link

I would prefer the C# pattern matching style.

interface IVehicle {}
class Car : IVehicle {}
class Motorcycle : IVehicle {}

IVehicle myVehicle = getVehicle();

if (myVehicle is Car car)
{
    // car is available as a Car in this scope
}
else if (myVehicle is Motorcycle motorcycle)
{
    // motorcycle is available as a Motorcycle in this scope
}

// Inverse usage
if (!(myVehicle is Car car))
{
    // car is not available in this scope
}
else
{
    // car is available as a Car in this scope
}

@munificent
Copy link
Member

We do have pattern matching enabled in the bleeding edge SDK, but it isn't user extensible in the sense that, say, F# active patterns are. We don't have patterns for RegExp matching, and the semantics aren't extensible to let you do that in a library, so I'm going to leave this open. I am interested in extending pattern matching to support user defined matching behavior, but I'm not sure what kind of priority it will have.

@lrhn
Copy link
Member

lrhn commented Apr 14, 2023

Please don't, but:

extension REMatch on String {
  bool operator <(Pattern pattern) => pattern.allMatches(this).isNotEmpty;
  bool operator >(String regexp) => RegExp(regexp, multiLine: false).hasMatch(this);
  bool operator >=(String regexp) => RegExp(regexp, multiLine: true).hasMatch(this);
}

void main() {
  switch ("abc") {
    case >= r"^[abc]*$": print("Contains only 'a', 'b' and 'c's");
    case < "ab": print("Contains 'ab'");
  }
}

All you need is matched value type with no built-in <, >, <= or >= operator, and a constant pattern to apply it to.

Not sure whether that suggests a way to extend patterns, or it suggest which way not to go :)

It's also only a true/false match, which doesn't allow you to inspect the match itself.

If we allowed object pattern getters to be general selectors (which we should, #2433), and constant RegExps (or non-constant pattern expressions), then you could do:

extension StringFirstMatch on String {
  Match? firstMatch(Pattern pattern) => pattern.firstMatch(this);
}

switch ("abc") {
  case String(firstMatch(const RegExp(r"(?:(abba)|[abc])*$")): Match([0]: var all, [1]: var abba?)?):
    print("All a's, b's and 'c's, and at least one 'abba'.");
  ...

You can do this today as well, you just need to write an extension per regexp:

extension AbbaMatch on String {
  static final _abbaRE = RegExp(r"(?:(abba)|[abc])*$");
  RegExpMatch? get abbaMatch => _abbaRE.firstMatch(this);
}
extension MatchCaptures on Match {
  String get $0 => this[0]!;
  String? get $1 => _tryGroup(1);
  String? get $2 => _tryGroup(2);
  String? get $3 => _tryGroup(3);
  String? get $4 => _tryGroup(4);
  String? get $5 => _tryGroup(5);
  String? get $6 => _tryGroup(6);
  String? _tryGroup(int i) => this.groupCount >= i ? this[i] : null;
}
// ...

switch ("abc") {
  case String(abbaMatch: RegExpMatch($0: var all, $1: var abba?)?):
    print("All a's, b's and 'c's, and at least one 'abba'.");
  ...

@Nhowka
Copy link

Nhowka commented Jun 19, 2023

Could we repurpose typedefs to act as active patterns? A translation from this F# example code:

let (|Integer|_|) (str: string) =
   let mutable intvalue = 0
   if System.Int32.TryParse(str, &intvalue) then Some(intvalue)
   else None

let (|ParseRegex|_|) regex str =
   let m = Regex(regex).Match(str)
   if m.Success
   then Some (List.tail [ for x in m.Groups -> x.Value ])
   else None
   
let parseDate str =
   match str with
   | ParseRegex "(\d{1,2})/(\d{1,2})/(\d{1,2})$" [Integer m; Integer d; Integer y]
          -> new System.DateTime(y + 2000, m, d)
   | ParseRegex "(\d{1,2})/(\d{1,2})/(\d{3,4})" [Integer m; Integer d; Integer y]
          -> new System.DateTime(y, m, d)
   | ParseRegex "(\d{1,4})-(\d{1,2})-(\d{1,2})" [Integer y; Integer m; Integer d]
          -> new System.DateTime(y, m, d)
   | _ -> new System.DateTime()

Could look like this in Dart:

typedef Integer = int? Function(String input) => int.tryParse(input);

typedef ParseRegex = List<String?>? Function(String regex, String input) {
  final exp = RegExp(regex);
  final match = exp.firstMatch(input);
  return match?.groups(List.generate(match!.groupCount -1 , (index) => index + 1));
}

DateTime parseDate(String date) => switch(date){
    ParseRegex(r'(\d{1,2})/(\d{1,2})/(\d{1,2})$', [Integer(m), Integer(d), Integer(y)]) => DateTime(y + 2000, m, d),
    ParseRegex(r'(\d{1,2})/(\d{1,2})/(\d{3,4})', [Integer(m), Integer(d), Integer(y)]) => DateTime(y, m, d),
    ParseRegex(r'(\d{1,4})-(\d{1,2})-(\d{1,2})', [Integer(y), Integer(m), Integer(d)]) => DateTime(y, m, d),
    _ => DateTime.now()
  };

The last argument would be where the value being tested would be applied and where the pattern would be checked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests