dart-lang / dart-lang/language

Should we support user-defined destructuring behavior and custom extractor pattern logic?

Open
#2,104 19 comments 2 reactions 0 assignees View on GitHub
feature patterns
Dominant language
TeX
Stars
2.9k
Forks
239
Avg merge
2d 18h
Merged PRs (30d)
14

Description

The [patterns proposal](https://github.com/dart-lang/language/blob/master/working/0546-patterns/patterns-feature-specification.md) has ["extractor patterns"](https://github.com/dart-lang/language/blob/master/working/0546-patterns/patterns-feature-specification.md#extractor-matcher) that look like:

```dart
Foo(a, b, c)
```

In other words, like a function or constructor call, except the arguments are subpatterns instead of subexpressions. This syntax is part of pattern matching in most other languages that have patterns. In Dart the proposal says they work like this:

1. The identifier at the beginning of the pattern should resolve to the name of a type.
2. If the value being matched is that type, then treat the rest of the pattern as a record pattern and match the value against that record pattern.
3. Record patterns can match values of arbitrary classes. If the record pattern has positional fields, the class must implement a `Destructure_n_` interface. For named fields, the pattern just directly invokes getters with the same name.

This covers the common case of algebraic datatype style matches against a family of subtypes. And, for named fields in the record pattern, the class doesn't have to add any special support at all. It just works, for free.

I think this default built-in behavior is fine, but it's fairly limited. It might be nice if users could define custom behavior for how an object is matched or destructured. F# has [active patterns](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns) and Scala has [`unapply()`](https://docs.scala-lang.org/tour/extractor-objects.html) methods for this.

An early version of the patterns actually had a design for this but for reasons I can't recall it didn't end up in the current version. I still think it's a good idea, so here's a simple proposed change:

---

Resolve the name on the extractor pattern to a type. If the name resolves to a type that declares a static method named `extract()` then:

1. The method must take a single positional parameter. It is a compile-time
error if the static type of the value being matched is not a subtype of the
parameter's type.

When the pattern is evaluated, the method is invoked with the value being
matched. The method returns `null` to indicate a failed match. Returning any
other value is a valid match, and the resulting value is then matched
against the extractor pattern's record. (Typically, the `extract()` method
will return a record, but it doesn't have to since any object can be matched
in a record pattern.)

If the return type of the method is non-nullable, then the extractor will
always match, so this `extract()` method can be used as a binder pattern.
If the return type is nullable, it can only be used as a matcher pattern.

2. Otherwise, fall back to the current "type test and record match" behavior.

A couple of questions and thoughts:

* Instead of making it a compile time error if the value's static type isn't
a subtype of the `extract()` method's parameter type, should it implicitly
insert a type test and automatically fail if the value isn't of the
parameter's type?

* Should we allow free-floating extractor functions by allowing the extractor
pattern name to resolve to a function declaration? What about resolving to
an instance method on the matched value's type?

Thoughts?

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.