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

Type patterns #4215

Open
lrhn opened this issue Dec 22, 2024 · 4 comments
Open

Type patterns #4215

lrhn opened this issue Dec 22, 2024 · 4 comments
Labels
feature Proposed language feature that solves one or more problems patterns Issues related to pattern matching.

Comments

@lrhn
Copy link
Member

lrhn commented Dec 22, 2024

"Type patterns" were considered as part of the patterns feature, but didn't make it.

Proposal

A "type pattern" is a type where a type argument is not always a plain type. It can also be a binding declaration, or a constrained binding declaration, which captures the type of the actual type argument when used to match against a type or an object. (See: "Existential open".)

Strawman syntax is that <X> or <X extends T> occurring where a type argument is expected will be a binding type pattern.
Example: if (value case Map<<K>, <V>>) { ... use<K, V>... }.
(Another possible syntax is final X/final X extends T, making it value case Map<final K, final V>: .... It just needs to be distinguishable from a type and include a type variable and bound, and the <X extends T> is how we usually introduce a type variable.)

A type pattern of <X> matches any type, and is irrefutable.
A type pattern of <X extends B> matches any subtype of B, just like the non-binding type pattern (aka. type) B.
(That is case Foo<Bar>() and case Foo<<X extends Bar>>(): matches the same types and objects, one just binds the type argument to a variable, just like int() and int x; matches the same objects, one just binds that value to a variable.)

Type patterns can be used in patterns, both refutable and declaration patterns. In the latter, they must be irrefutable (so no bounds that aren't guaranteed to be satisfied by the matched value type, which basically means no useful bounds).

Type patterns do require a feature that Dart doesn't have today: "|Existential Open", the ability to go from an instantiated generic type (including the runtime type of an object) to a type variable bound to its type argument.
This is a useful feature, but it requires actually retaining the type information at runtime. So does any reference to the type variable inside the class itself, so it's not new, it's just possible to add uses on the side, so a class can't convince itself that it never uses its type arguments for anything at runtime, and expect them to be tree-shaken. Someone else might extract the type argument.

Type patterns can only be used to extract type arguments.
It's deliberately not allowed to use a type pattern for an entire value, like switch (value) { case <T> v: ... } which should be matching against the runtime type of value. This is disallowed precisely so that the newly introduced type variables can never be bound to runtime decided type that wasn't passed as a type argument in at least one place in the program.

It also means it shouldn't leak implementation details. The runtime type of a native <int>[] is _GrowableList<int>. Since a type pattern has to be List<<T>>, you can only ask for type parameters of interfaces that you can name. It's restricted to the public interfaces of the API, private types are not leaked unless they are visible in the interface.

Potentially, we could allow destructuring function types, like case <R> Function(<P>): .... That may cause more types to be accessible at runtime and be bad for tree-shaking. It would be useful. (But structural types are not trivial, allowing (<T1>, <T2>) to match a record is the same as matching a value's runtime type. Functions might be different, because it's not the value's type. Or at least it's not any value.)

Other uses

If Dart has type patterns, they could be used in some places where you'd currently have to use a type variable, in some places where you can't use a type variable, and maybe just make some things a little shorter.

Having a List<<T>> pattern is basically the same as having a List<T> pattern with a <T> declaration on the side,
like what we do for void foo<T>(List<T> arg) { ... } /*...*/ foo(arg);, except that type arguments are statically resolved, and type patterns are runtime destructuring/opening the runtime type.

We could allow type patterns in extension declaration's on type:

extension FooList on List<<X extends Foo>> {
  // X is in scope.
}
extension KeyMap<K> on Map<K, <V>> {
  // K and V in scope.
}

It could also be used in catch clauses, on GenericError<<X>>, but errors are rarely generic.

@lrhn lrhn added feature Proposed language feature that solves one or more problems patterns Issues related to pattern matching. labels Dec 22, 2024
@rubenferreira97
Copy link

Could you explain the advantages and disadvantages of this proposal compared to #170?

@tatumizer
Copy link

tatumizer commented Dec 24, 2024

I think the 2 proposals are just 2 different syntactic forms for implementing a single feature: the ability to extract type arguments as Type objects. Today, we can say obj.runtimeType, but we can't say obj.runtimeType.typeArguments[0]. The proposals allow to achieve the same in a roundabout way - via patterns. Why not introduce a bona fide primitive type.typeArguments (obtaining the list) first? Then the proposed patterns would be much easier to explain IMO.

@lrhn
Copy link
Member Author

lrhn commented Dec 24, 2024

I agree that this is basically the same feature as #170, with a different syntax for binding, and without the union types.
(I think, twas a long text.)

Both are barely more useful than .runtimeType[0] or similar, because that returns a Type object, which you cannot use as a type argument (or use for pretty much anything, Type objects are dumb).

These type patterns allow capturing the type argument in a type variable, which can be used as a type argument, or on an is or as test.

void copyAllowed(
    List<<E>> target, List<Object?> source) {
  for (var o in source) {
    if (o is E) target.add(o);
  }
}

@tatumizer
Copy link

I think the original syntax with var X was more readable. Angle brackets become confusing at depth > 1, e.g.
Future<Map<String,List<var X>>> vs
Future<Map<String,List<<X>>>>
The latter looks more like ASCII art or a stuck key. Imagine how it may look when two types are to be captured. 😄

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 patterns Issues related to pattern matching.
Projects
None yet
Development

No branches or pull requests

3 participants