language icon indicating copy to clipboard operation
language copied to clipboard

Type aliases and type variable binding in pattern matching.

Open lrhn opened this issue 3 years ago • 1 comments

tl;dr: Don't allow var X in type argument position of a type alias unless the variable occurs precisely once in the full expansion, and it occurs in type argument position of a generic interface type. Only allow extracting type arguments of classes, not arbitrary component types of composite types.

In the current patterns proposal, you can write:

SomeType(propertyMatches) x = o;

to match o against SomeType and extract properties from it. It even allows matching type arguments:

List<var T> list = someList;

This extract the actual element type of someList, not its currently known static element type. That's an amazingly powerful feature. Very powerful. Like, really, really powerful. (I want to emphasize that that's awesome, with a risk of possibly being just a little too awesome!)

You can use a type alias as the type of the property matcher. That's a way to write a record type where the record type syntax isn't otherwise allowed:

typedef Pair<S, T> = (S, T);
...
  Pair<int, int>(first, second) = somePair; // Not strictly necessary, but allowed.

We should be very clear on how it actually works when we combine those two features.

For example, just consider

typedef Convolution<S> = Map<S, S>;

Convolution<var S> map = <int, double>{};

Can we solve this? Do we want to? (Will S be num?)

Even worse:

typedef TypeOf<T> = T;  // already useful for creating type literals of any type, like `TypeOf<int?>`.

TypeOf<var runtimeType> object = o;  // Binds `runtimeType` to the actual runtime type of `o`!

That's awesome. Not only do we get the runtime-type of an object, we get it as a type variable, not just a stupid Type object. What's not to like?

Well, actually, it means that we can no longer statically analyze which types can end up in type variables. Currently, the only way a type can end up in a type variable is if there is a type argument somewhere in the program where that type can occur. If analysis tells us that a type never occurs as a type argument, then we can tree-shake everything related to that type being used as a type variable.

With the behavior above, that's no longer true. A method like:

R callWithType<R>(Object? object, R Function<T>(T value) function) {
  TypeOf<var T> typedObject = object;
  return function<T>(typedObject);
}

will completely break that analysis.

For the restriction to keep holding, we need to only be able to capture types into type variables from places where they are already type arguments to actual classes. Only capture actually reified type arguments, not structural ones like record types or arguments to type aliases.

Another example. with a record and type alias:

typedef Pair<S, T> = (S, T);
Pair<var runtimeType, _>(var object, _) = (o, "ignore me");

which gets the same effect, because the element type of a record is the runtime-type of the element.

So, we should only allow var X as a type argument capturer in a pattern iff it occurs in a "real type argument position", which means:

  • It occurs as a type argument to an interface type, or
  • it occurs as a type argument to a type alias, and the type parameter of the alias occurs exactly once in the expansion, in a real type argument position.

With that, we can blindly recursively expand type aliases, and in the end, each var T only occurs once (which is necessary for the scoping to work) and only in position of an actual type argument to an interface type. That's a nice property to have (type aliases do not exist at runtime, and hardly at compile-time, they are completely defined by what they expand to, and you can always expand eagerly.)

If you try to use var X for a type alias argument which doesn't satisfy that, you get told that "you can only capture actual type arguments".

This will also prevent

typedef F<R, A> = R Function(A);

F<var R, var A> f = someUnaryFunction;

That could also be very useful, very powerful, and very bad for our AoT compilers, for all the same reasons.

lrhn avatar Apr 28 '22 11:04 lrhn

Since we're not doing type patterns in the initial release of pattern matching, I'm going to label this "patterns-later" so that it doesn't clutter up the list of issues we're working through for the first release.

munificent avatar Aug 18 '22 23:08 munificent