dart-lang / language

Design of the Dart language
Other
2.61k stars 200 forks source link

Extensible pattern-matching #1047

Open DartBot opened 12 years ago

DartBot commented 12 years ago

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 commented 12 years ago

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 commented 12 years ago

We may act on this in the future.


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

anders-sandholm commented 12 years ago

Added Area-Language label.

gbracha commented 12 years ago

Added this to the Later milestone.

DartBot commented 12 years ago

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 commented 12 years ago

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 commented 11 years ago

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 commented 10 years ago

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

kasperl commented 9 years ago

Removed Oldschool-Milestone-Later label.

gbracha commented 9 years ago

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

Aetet commented 7 years ago

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

lrhn commented 7 years ago

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 commented 5 years ago

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 commented 4 years ago

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

He-Pin commented 4 years ago

I like the Java or Scala style.

henry-hz commented 4 years ago

@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 commented 4 years ago

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

FranckRJ commented 3 years ago

https://github.com/dart-lang/language/issues/1047#issuecomment-646088104 @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 commented 3 years ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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.