From 917ac1b3b4b11446457007bf63c68dea7df1e4d0 Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Mon, 8 Apr 2019 16:09:24 +0200 Subject: [PATCH 01/17] Initial draft of static extension methods design document --- .../design-document.md | 375 ++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 accepted/future-releases/static-extension-methods/design-document.md diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md new file mode 100644 index 0000000000..cb137b677f --- /dev/null +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -0,0 +1,375 @@ +# Dart Static Extension Methods Design +lrn@google.com + +This is a design document for *static extension methods* for Dart. This document describes the most basic variant of the feature, then lists a few possible variants or extensions. + +The design of this feature is kept deliberately simple. + +## What Are Static Extension Methods + +Dart classes have virtual methods. An invocation like `thing.doStuff()` will invoke the virtual `doStuff` method on the object denoted by `thing`. The only way to add methods to a class is to modify the class. If you are not the author of the class, you have to use static helper functions instead of methods, so `doMyStuff(thing)` instead of `thing.doMyStuff()`. That's acceptable for a single function, but in a larger chain of operations, it becomes overly cumbersome. Example: + +```dart +doMyOtherStuff(doMyStuff(something.doStuff()).doOtherStuff()) +``` + +That code is so much less readable than + +```dart +something.doStuff().doMyStuff().doOtherStuff().doMyOtherStuff() +``` + +So, primarily for readability reasons, static extension methods will allow you to add "extension methods", which are really just static functions, to existing types, and allow you to call those methods as if they were actual methods, using `.`. + +The extension methods are *static*, which means that we use the static type of an expression to figure out which method to call, and that also means that static extension methods are not virtual methods. + +The methods we allow you to add this way are normal methods, operators, getter and setters. + +## Declaring Static Extension Methods + +### Syntax + +A static extension of a type is declared using syntax like: + +```dart +extension MyFancyList on List { + int get doubleLength => this.length * 2; + List operator-() => this.reversed.toList(); + List> split(int at) => + >[this.sublist(0, at), this.sublist(at)]; + List mapToList(R Function(T) convert) => this.map(convert).toList(); +} +``` + +More precisely, an extension declaration is a top-level declaration with a grammar similar to: + +```ebnf + ::= + `extension' ? `on' `?'? `{' + memberDeclaration* + `}' +``` + +Such a declaration introduces its *name* (the identifier) into the surrounding scope. The name does not denote a type, but it can be used to denote the extension itself in various places. The name can be hidden or shown in `import` or `export` declarations. + +The *type* can be any valid Dart type, but not a single type variable. It can refer to the type parameters of the extension. It can be followed by `?` which means that it allows `null` values. + +The member declarations can be any static or instance member declaration except for instance variables and constructors. + +### Scope + +Dart's static extension methods are *scoped*. They only apply to code that the extension itself is accessible to. Being in accessible means that using the extension's name must denote the extension. The extension is not in scope if another declaration with the same name shadows the extension, if the extension's library is not imported, if the library is imported and the extension is hidden, or the library is only imported with a prefix. In other words, if the extension had been a class, it is only in scope if using the name would denote the class— which will allow you to call static methods on the class, which is exactly what we are going to do. + +### Extension Member Resolution + +The declaration introduces an extension. The extension's `on` type defines which types are being extended. + +For any member access, `x.foo`, `x.bar()`, `x.baz = 42`, `x(42)` or `x + y`, the language first checks whether the static type of `x` has a member with the same base name as the operation. That is, if it has a corresponding instance member, respectively, a `foo` method or getter or a `foo=` setter. a `bar` member or `bar=` setter, a `baz` member or `baz=` setter, a `call` method, or a `+` method. If so, then the operation is unaffected by extensions. *This check does not care whether the invocation is otherwise correct, based on number or type of the arguments, it only checks if there is a member at all.* + +(The types `dynamic` and `Never` are considered as having all members, the type `void` is always a compile-time error when used in a receiver position, so none of these can ever be affected by static extension methods). + +If there is no such member, the operation is currently a compile-time error. In that case, all extensions in scope are checked for whether they apply. An extension applies to a member access if the static type of the receiver is a subtype of the `on` type of the extension *and* the extension has an instance member with the same base name as the operation. + +For generic extensions, standard type inference is used to infer the type arguments. As an example, take the following extension: + +```dart +extension MyList> on List { + void quickSort() { ... } +} +``` + +and a member access like: + +```dart +Uint8List bytes = ...; +bytes.quickSort(); +``` + +Here we perform type inference equivalent to what we would do for: + +```dart +class MyList> { + MyList(List argument); + void quickSort() { ... } +} +... +Uint8List bytes = ...; +... MyList(bytes).quickSort() ... // Infer type argument of MyList here. +``` + +This is inference based on static types only. The inferred type argument becomes the value of `T` for the function invocation that follows. + +If the inference fails, or if the synthetic constructor invocation would not be valid for any other reason, then the extension does not apply. If the extension does not have an instance member with the base name `quickSort`, it does not apply. + +If exactly one extension applies to an otherwise failed member invocation, then that invocation becomes an invocation of the corresponding instance member of the extension, with `this` bound to the receiver object and type parameter bound to the inferred types. + +If the member is itself generic and has no type parameters supplied, normal static type inference applies again. + +It is *as if* the invocation `bytes.quickSort` was converted to `MyList(bytes).quickSort()`. The difference is that there is never an actual `MyList` object, and the `this` object inside `quickSort` is just the `bytes` list itself (although the extension methods apply to that code too). + +### Extension Conflict Resolution + +If more than one extension applies to a specific member invocation, then if exactly one of them is more specific than all the others, that one is chosen. Otherwise it is a compile-time error. + +An extension is more specific than another extension if its `on` type is a subtype of the `on` type of the other extension. If the extension is generic, first do instantiation to bounds, then use the `on` type of that when comparing specificity of extensions. + +If an extension's `on` type has a trailing `?`, say `T?`, then it is considered a supertype of the corresponding type `T` without the trailing `?`, as a subtype of any supertype of `T` that also has a trailing `?`, and otherwise unrelated to all other types. So, `on int?` is less specific than `on int`, and `on int?` is more specific than `on num?`. There is no relation between `on num` and `on int?`. + +That is, the specificity of an extension is independent of the type it is used at. + +Example: + +```dart +extension SmartIterable on Iterable { + void doTheSmartThing(void Function(T) smart) { + for (var e in this) smart(e); + } +} +extension SmartList on List { + void doTheSmartThing(void Function(T) smart) { + for (int i = 0; i < length; i++) smart(this[i]); + } +} +... + List x = ....; + x.doTheSmartThing(print); +``` + +Here both the extensions apply, but the `SmartList` extension is more specific than the `SmartIterable` extension because `List` <: `Iterable`. + +Example: + +```dart +extension BoxCom> on Box> { foo() {} } +extension BoxList on Box> { foo() {} } +extension BoxSpec on Box> { foo() {} } +... + List x = ...; + x.foo(); +``` + +Here all three extensions apply. The most specific one is `BoxSpec` which is specialized for lists of numbers. If `BoxSpec` had not been in scope, then neither of `BoxCom` or `BoxList` would be more specific than the other. Their `on` types, when instantiated to bounds, are `Box>>` and `Box>` which are unrelated by subtyping. + +This is also why we use the instantiated-to-bounds type for comparison. If we used the actual type, bound to the type variable by the current use-case, we would consider `BoxList` to be more specific than `BoxSpec`, but in practice, the `BoxList` extension would not be able to *use* the integer-ness of the type in its code, so `BoxSpec`, which can specialize its code for lists of numbers, is the most precise match. + +### Overriding Access + +If two or more extensions apply to the same member access, or if a member of the receiver type prevents using an extension method, or if the extension is shadowed, then it is possible to force an extension member invocation: + +```dart +MyList(object).quickSort(); +``` + +The syntax looks like a constructor invocation, but it does not create a new object. + +If `object.quickSort()` would invoke an extension method of `MyList`, then `MyList(object).quickSort()` will invoke the exact same method in the same way. + +This mode of invocation also allows invocation of extensions that are *not* in scope, perhaps as `prefix.MyExtension(object).extensionMember()` if imported with a prefix. + +The syntax is not *convenient*—you have to put the "constructor" invocation up front, which removes the one advantage that extension methods have over normal static methods. It is not intended as the common use-case, but as an escape hatch out of conflicts. + +### Static Members and Member Resolution + +Static member declarations in the extension declaration can be accessed the same way as static members of a class or mixin declaration: By prefixing with the extension's name. + +Example: + +```dart +extension MySmart on Object { + smart() => smartHelper(this); // valid + static smartHelper(Object o) { ... } +} +... + MySmart.smartHelper(someObject); // valid +``` + +### Semantics of Invocations + +If an extension is found to be the one applying to a member invocation, then at run-time, the invocation will perform a method invocation of the corresponding instance member of the extension, with `this` bound to the receiver value and type parameters bound to the types found by static inference. + +If the receiver is `null`, then that invocation is an immediate run-time error unless the `on` type of the extension has a trailing `?`. + +With NNBD types, we will not allow a non-nullable extension `on` type to apply to a member invocation with a nullable receiver type. + +### Semantics of Extension Members + +When executing an extension instance member, we stated earlier that the member is invoked with the original receiver as `this` object. We still have to describe how that works, and what the lexical scope is for those members. + +Inside an extension declaration, we give preference to that extension over any other extension. At any otherwise invalid invocation, if the current extension applies, then it is considered more specific than all other extensions that may apply. This applies to static methods too. + +Example: + +```dart +extension MyUnaryNumber on List { + bool get isEven => this.length.isEven; + bool get isOdd => !this.isEven; + static bool isListEven(List list) => list.isEven; +} +``` + +Here the `list.isEven` is guaranteed to hit the `isEven` of the same extension (unless someone puts an `isEven` member on `List`), an extension has more specificity than any other extension inside itself. + +About *`this`*: Inside an instance member of an extension, the static type of the `this` operator is the `on` type. However, for member invocations, we give preference to extension methods of the current extension, *even over members of the `on` type*. + +Here, the `this.isEven` is a member invocation on `this`, so it *first* tries to resolve `isEven` against the current extension. If it finds a member with the same base name on the extension, that member is invoked with the same `this` value. If not, then it is considered a normal member invocation on an object with static type `List,` and if `List` does not have the necessary member, then all applicable extensions are checked (which will definitely not match the current extension since we already checked that). + +The behavior is *only* for the `this` operator. Doing something like `var self = this; self.isEven;` will not give preference over members of `List` (if `List` had an `isEven`, then `self.isEven` would invoke that, where `this.isEven` will invoke the extension method). It is as if `this` has a type representing the extension, even if no such type actually exists. + +This behavior makes the extension act similarly to a class, which will hopefully make it easier for users to understand the resolution. + +Like for a class or mixin member declaration, the names of the extension members, both static and instance, are in the *lexical* scope of the extension member. That is why `MySmart` above can invoke the static `smartHelper` without prefixing it by the extension name. In the same way, *instance* members are in the lexical scope. + +Example: + +```dart +extension MyUnaryNumber on List { + bool get isEven => this.length.isEven; + bool get isOdd => !isEven; // not `!this.isEven` +} +``` + +Here, the `isEven` name exists in the surrounding static scope, so just as for `class` members, it is considered equivalent to `this.isEven`. Because of how we treat `this`, it means that `isEven` and `this.isEven` means the same thing when the extension declares an `isEven` member, just as for a class or mixin declaration. + +### Member Conflict Resolution + +An extension can declare a member with the same (base-)name as the type it is declared on. This does not cause a compile-time conflict, even if the member does not have a compatible signature. + +Example: + +```dart +extension MyList on List { + void add(T value, {int count = 1}) { ... } + void add2(T value1, T value2) { ... } +} +``` + +You cannot *access* this member in a normal invocation, so it could be argued that you shouldn't be allowed to add it. We allow it because we do not want to make it a compile-time error for a type to add a method just because an extension is already adding a method with the same name. it will likely be a problem if any code *uses* the method, but only that code needs to change (perhaps using an override). + +That also means that an extension can shadow members of the `on` type. To implement `add2` above, we don't want to call `this.add`, instead we want to use the `add` of `List` directly. To do that, we allow `super` invocations to target the underlying `on` type directly, with *no* extensions applying. + +```dart +void add2(T value1, T value2) { + super..add(value1)..add(value2); +} +``` + +## Summary + +- Extensions are declared using the syntax: + + ```ebnf + ::= `extension' ? `on' `?'? `{' + * + `}' + ``` + + where `extension` becomes a built-in identifier, `` must not be a type variable, and `` does not allow instance fields or constructors. It does allow static members. + +- The extension declaration introduces a name (``) into the surrounding scope. + - The name can be shown or hidden in imports/export. It can be shadowed by other declarations as any other top-level declaration. + - The name can be used as suffix for invoking static members (used as a namespace, same as class/mixin declarations). + +- A member invocation (getter/setter/method/operator) which targets a member that is not on the static type of the receiver (no member with same base-name is available) is subject to extension application. It would otherwise be a compile-time error. + +- An extension applies to such a member invocation if + + - the extension name is visible in the lexical scope, + - the extension declares an instance member with the same base name, and + - the `on` type (after type inference) of the extension is a super-type of the static type of the receiver. + +- Type inference for `extension Foo on Bar { baz(params) => ...}` for an invocation `receiver.baz(args)` is performed as if the extension was a class: + + ```dart + class Foo { + Bar _receiver; + Foo(Bar this._receiver); + void baz(params) => ...; + } + ``` + + that was invoked as `Foo(receiver).baz(args)`. The binding of `T` and `S` found here is the same binding used by the extension. If the constructor invocation would be a compile-time error, the extension does not apply. + +- One extension is more specific than another if the `on` type of the former is a subtype of the `on` type of the latter. For generic extensions, use the `on` type after instantiating to bounds. An `on` type `T?`, with a trailing `?`, is considered more specific than the same type without a `?`, and less specific than a supertype of `T` which also has a `?`. That is: + + - *S* <: *T*? iff *S* does not have a trailing `?` and *S* <: *T*. + - *S*? <: *T*? iff *S* <: *T* + +- If there is no single most-specific extension which applies to a member invocation, then it is a compile-time error. (This includes the case with no applicable extensions, which is just the current behavior). + +- Otherwise, the single most-specific extension's member is invoked with the extension's type parameters bound to the types found by inference, and with `this ` bound to the receiver. + +- An extension method can be invoked explicitly using the syntax `ExtensionName(object).method(args)`. Type arguments can be applied to the extension explicitly as well, `MyList(listOfString).quickSort()`. Such an invocation overrides all extension resolution. It is a compile-time error if `ExtensionName` would not apply to the `object.method(args)` invocation if it was in scope. + +- The override can also be used for extensions imported with a prefix (which are not otherwise in scope): `prefix.ExtensionName(object).method(args)`. + +- An invocation of an extension method throws if the receiver is `null` unless the `on` type has a trailing `?`. + +- Otherwise an invocation of an extension method runs the instance method with `this` bound to the receiver and with type variables bound to the types found by type inference (or written explicitly for an override invocation). + +- Inside an extension member, the current extension is considered more specific than any other extension, so if it applies to an invocation, then it doesn't matter which other extensions also apply. +- Inside an *instance* extension member,: + - invocations on `this` check the current extension's members *before* the `on` type members. + - in every other way the static type of `this ` is the `on` type. + - A `super` invocations is an invocation on `this` allowing only members of the `on` type. No extension methods apply, from the current extension or any other. + +## Variants + +The design above can be extended in the following ways. + +### Multiple `on` Types + +The `on ` clause only allows a single type. The similar clause on `mixin` declarations allow multiple types, as long as they can all agree on a single combined interface. + +We could allow multiple types in the `extension` `on` clause as well. It would have the following consequences: + +- An extension only applies if the receiver type is a subtype of all `on` types. +- An extension is more specific than another if for every `on` type in the latter, there is an `on` type in the former which is a subtype of that type. +- The trailing `?` makes the most sense if it is applied only once (it's the extension which accepts and understands `null` as a receiver), but for forwards compatibility, we will need to put it on every `on` type individually. +- There is no clear type to assign to `this` inside an instance extension method. For a mixin that's not a problem because it introduces a type by itself, and the combined super-interface is only used for `super` invocations. For extension, a statement like `var self = this;` needs to be assigned a useful type. + +The last item is the reason this feature is not something we will definitely do. We can start out without the feature and maybe add it later if it is necessary, but it's safer to start without it. + +### Extending Static Members + +The feature above only extends instance members. There is no way to add a new static member on an existing type, something that should logically be a *simpler* operation. + +We could allow + +```dart +extension MyInt on num { + int get double => this * s; + static int get random => 4; +} +``` + +to introduce both a `double` instance getter on `num` instances and a `random` getter on `num` itself, usable as `var n = num.random;` + +However, while this is possible, not all `on` types are class or mixin types. It is not clear what it would mean to put static methods on `on` types of extension like: + +- `extension X on Iterable` +- `extension X> on Iterable` +- `extension X on int Function(int)` +- `extension X on FutureOr` +- `extension X on int?` + +For the first two, we could put the static members on the `Iterable` class, but since the extension does not apply to *all* iterables, it is not clear that this is correct. + +For `int Function(int)` and `FutureOr`, it's unclear how to call such a static method at all. There is no type literal with type `int Function(int)`. We could put the static method on `Function`, but that's not particularly discoverable, and why not require that they are put on `Functon` explicitly. For `FutureOr`, we could allow static members on `FutureOr`, but again it seems spurious. For `int?`, we could put the method on `int`, but why not just require that it's on `int`. + +The issue here is that the type patterns used by `on` are much more powerful than what is necessary to put static members on class types. + +It would probably be more readable to introduce a proper static extension declaration: + +```dart +static extension Foo on int { // or: extension Foo on static int + int fromList(List l) => l.length; +} + +... + print(int.fromList([1, 2])); // 2 +``` + +where the `on` type must be something that can already have static methods. + +If we allow extension static declarations like these, we could also allow extension constructors. + From d41ecb5cedfb477c4367020580aa4bf46524e623 Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Wed, 24 Apr 2019 13:13:21 +0200 Subject: [PATCH 02/17] Address comments --- .../design-document.md | 130 ++++++++++++++---- 1 file changed, 102 insertions(+), 28 deletions(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index cb137b677f..cc005cf9b6 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -1,9 +1,12 @@ # Dart Static Extension Methods Design -lrn@google.com + +lrn@google.com
Version: 1.0
Status: Design Proposal This is a design document for *static extension methods* for Dart. This document describes the most basic variant of the feature, then lists a few possible variants or extensions. -The design of this feature is kept deliberately simple. +See [Problem Description](https://github.com/dart-lang/language/issues/40) and [Feature Request](https://github.com/dart-lang/language/issues/41) for background. + +The design of this feature is kept deliberately simple, while still attempting to make extension methods act similarly to instance methods in most cases. ## What Are Static Extension Methods @@ -13,13 +16,15 @@ Dart classes have virtual methods. An invocation like `thing.doStuff()` will inv doMyOtherStuff(doMyStuff(something.doStuff()).doOtherStuff()) ``` -That code is so much less readable than +That code is much less readable than: ```dart something.doStuff().doMyStuff().doOtherStuff().doMyOtherStuff() ``` -So, primarily for readability reasons, static extension methods will allow you to add "extension methods", which are really just static functions, to existing types, and allow you to call those methods as if they were actual methods, using `.`. +The code is also much less discoverable. An IDE can suggest `doMyStuff()` after `something.doStuff().`, but will be unlikely to suggest putting `doMyOherStuff(…)` around the expression. + +For these discoverability and readability reasons, static extension methods will allow you to add "extension methods", which are really just static functions, to existing types, and allow you to discover and call those methods as if they were actual methods, using `.`-notation. It will not add any new abilities to the language which are not already available, they just require a more cumbersome and less discoverable syntax to reach. The extension methods are *static*, which means that we use the static type of an expression to figure out which method to call, and that also means that static extension methods are not virtual methods. @@ -52,13 +57,13 @@ More precisely, an extension declaration is a top-level declaration with a gramm Such a declaration introduces its *name* (the identifier) into the surrounding scope. The name does not denote a type, but it can be used to denote the extension itself in various places. The name can be hidden or shown in `import` or `export` declarations. -The *type* can be any valid Dart type, but not a single type variable. It can refer to the type parameters of the extension. It can be followed by `?` which means that it allows `null` values. +The *type* can be any valid Dart type, including a single type variable. It can refer to the type parameters of the extension. It can be followed by `?` which means that it allows `null` values. When Dart gets non-nullable types by default (NNBD), this `?` syntax is removed and subsumed by nullable types like `int?` being allowed in the `` position. The member declarations can be any static or instance member declaration except for instance variables and constructors. ### Scope -Dart's static extension methods are *scoped*. They only apply to code that the extension itself is accessible to. Being in accessible means that using the extension's name must denote the extension. The extension is not in scope if another declaration with the same name shadows the extension, if the extension's library is not imported, if the library is imported and the extension is hidden, or the library is only imported with a prefix. In other words, if the extension had been a class, it is only in scope if using the name would denote the class— which will allow you to call static methods on the class, which is exactly what we are going to do. +Dart's static extension methods are *scoped*. They only apply to code that the extension itself is accessible to. Being accessible means that using the extension's name must denote the extension. The extension is not in scope if another declaration with the same name shadows the extension, if the extension's library is not imported, if the library is imported and the extension is hidden, or the library is only imported with a prefix. In other words, if the extension had been a class, it is only in scope if using the name would denote the class— which will allow you to call static methods on the class, which is exactly what we are going to do. ### Extension Member Resolution @@ -111,11 +116,16 @@ It is *as if* the invocation `bytes.quickSort` was converted to `MyList(byt If more than one extension applies to a specific member invocation, then if exactly one of them is more specific than all the others, that one is chosen. Otherwise it is a compile-time error. -An extension is more specific than another extension if its `on` type is a subtype of the `on` type of the other extension. If the extension is generic, first do instantiation to bounds, then use the `on` type of that when comparing specificity of extensions. +An extension with `on` type clause *T*1 is more specific than another extension with `on` type clause *T*2 iff the instantiated type (the type after applying type inference from the receiver) of *T*1 is a subtype of the instantiated type of *T*2 and either: -If an extension's `on` type has a trailing `?`, say `T?`, then it is considered a supertype of the corresponding type `T` without the trailing `?`, as a subtype of any supertype of `T` that also has a trailing `?`, and otherwise unrelated to all other types. So, `on int?` is less specific than `on int`, and `on int?` is more specific than `on num?`. There is no relation between `on num` and `on int?`. +- not vice versa, or +- the instantiate-to-bounds type of *T*1 is a subtype of the instantiate-to-bounds type of *T*2 and not vice versa. -That is, the specificity of an extension is independent of the type it is used at. +This definition is designed to ensure that the extension chosen is the one that has the most precise type information available. + +If an extension's `on` type has a trailing `?`, say `T?`, then we treat it just as we would for the corresponding nullable type with NNBD types, which are described in a separate design document. In short, it means treating the type `T?`, for subtyping purposes, as a union type of `T` with `Null` (a least supertype of `T` and `Null`). + +That is, the specificity of an extension wrt. an application depends of the type it is used at, and how specific the extension is itself (what its implementation can assume about the type). Example: @@ -140,26 +150,44 @@ Here both the extensions apply, but the `SmartList` extension is more specific t Example: ```dart -extension BoxCom> on Box> { foo() {} } -extension BoxList on Box> { foo() {} } -extension BoxSpec on Box> { foo() {} } +extension BoxCom> on Box> { T best() {...} } +extension BoxList on Box> { T best() {...} } +extension BoxSpec on Box> { num best() {...} } ... List x = ...; - x.foo(); + var v = x.best(); + List y = ...; + var w = y.best(); ``` -Here all three extensions apply. The most specific one is `BoxSpec` which is specialized for lists of numbers. If `BoxSpec` had not been in scope, then neither of `BoxCom` or `BoxList` would be more specific than the other. Their `on` types, when instantiated to bounds, are `Box>>` and `Box>` which are unrelated by subtyping. +Here all three extensions apply to both invocations. + +For `x.best()`, the most specific one is `BoxList`. Because `Box>` is a proper subtype of both ` Box>` and `Box>`, we expect `BoxList` to be the best implementation. The return type causes `v` to have type `int`. If we had chosen `BoxSpec` instead, the return type could only be `num`, which is why we choose the most specific instantiated type as the winner. -This is also why we use the instantiated-to-bounds type for comparison. If we used the actual type, bound to the type variable by the current use-case, we would consider `BoxList` to be more specific than `BoxSpec`, but in practice, the `BoxList` extension would not be able to *use* the integer-ness of the type in its code, so `BoxSpec`, which can specialize its code for lists of numbers, is the most precise match. +For `y.best()`, the most specific extension is `BoxSpec`. The instantiated `on` types that are compared are `Box>` for `BoxCom` and `Box>` for the two other. Using the instantiate-to-bounds types as tie-breaker, we find that `Box>` is less precise than `Box>`, so the code of `BoxSpec` has more precise information available for its method implementation. The type of `w` becomes `Box>`. + +In practice, unintended extension method name conflicts are likely to be rare. Intended conflicts happen where the same author is providing more specialized versions of an extension for subtypes, and in that case, picking the extension which has the most precise types available to it is considered the best choice. ### Overriding Access -If two or more extensions apply to the same member access, or if a member of the receiver type prevents using an extension method, or if the extension is shadowed, then it is possible to force an extension member invocation: +If two or more extensions apply to the same member access, or if a member of the receiver type takes precedence over an extension method, or if the extension is imported with a prefix, then it is possible to force an extension member invocation: ```dart MyList(object).quickSort(); ``` +or if you don't want the type argument to the extension to be inferred: + +```dart +MyList(object).quickSort(); +``` + +or if you imported the extension with a prefix to avoid name collision: + +```dart +prefix.MyList(object).quickSort(); +``` + The syntax looks like a constructor invocation, but it does not create a new object. If `object.quickSort()` would invoke an extension method of `MyList`, then `MyList(object).quickSort()` will invoke the exact same method in the same way. @@ -189,7 +217,7 @@ If an extension is found to be the one applying to a member invocation, then at If the receiver is `null`, then that invocation is an immediate run-time error unless the `on` type of the extension has a trailing `?`. -With NNBD types, we will not allow a non-nullable extension `on` type to apply to a member invocation with a nullable receiver type. +With NNBD types, it is a run-time error if the receiver is `null` and the instantiated `on` type of the selected extension does not allow `null`. For sound non-nullable types, this can be excluded statically. If a receiver expression is nullable, then the matching `on` type must be nullable too. If a receiver expression is soundly non-nullable, then the value cannot be `null` at run-time. During migration, we will have unsound nullable types like `int*` which should be matched by `extension Wot on int {…}`, and in that case we will need a run-time check to avoid passing `null` to that extension. ### Semantics of Extension Members @@ -253,12 +281,32 @@ void add2(T value1, T value2) { } ``` +### Tearoffs + +A static extension method can be torn off like any other instance method. + +```dart +extension Foo on Bar { + int baz(T x) => x.toString().length; +} +... + Bar b = ...; + int Function(int) func = b.baz; +``` + +This assignment does a tear-off of the `baz` method. In this case it even does generic specialization, so it creates a function value of type `int Function(int)` which, when called with argument `x`, works just as `Foo(b).baz(x)`, whether or not`Foo` is in scope at the point where the function is called. The torn off function closes over both the extension type and the receiver, and over any type arguments that it is implicitly instantiated with. + +An explicitly overridden extension method, like `Foo(b).baz` also works as a tear-off. + +There is still no way to tear off getters, setters or operators. If we ever introduce such a feature, it should work for extension methods too. + ## Summary - Extensions are declared using the syntax: ```ebnf - ::= `extension' ? `on' `?'? `{' + ::= `extension' ? `on' `?'? + `{' * `}' ``` @@ -266,6 +314,7 @@ void add2(T value1, T value2) { where `extension` becomes a built-in identifier, `` must not be a type variable, and `` does not allow instance fields or constructors. It does allow static members. - The extension declaration introduces a name (``) into the surrounding scope. + - The name can be shown or hidden in imports/export. It can be shadowed by other declarations as any other top-level declaration. - The name can be used as suffix for invoking static members (used as a namespace, same as class/mixin declarations). @@ -289,10 +338,7 @@ void add2(T value1, T value2) { that was invoked as `Foo(receiver).baz(args)`. The binding of `T` and `S` found here is the same binding used by the extension. If the constructor invocation would be a compile-time error, the extension does not apply. -- One extension is more specific than another if the `on` type of the former is a subtype of the `on` type of the latter. For generic extensions, use the `on` type after instantiating to bounds. An `on` type `T?`, with a trailing `?`, is considered more specific than the same type without a `?`, and less specific than a supertype of `T` which also has a `?`. That is: - - - *S* <: *T*? iff *S* does not have a trailing `?` and *S* <: *T*. - - *S*? <: *T*? iff *S* <: *T* +- One extension is more specific than another if the instantiated `on` type of the former is a proper subtype of the instantiated `on` type of the latter, or if the two instantiated types are equivalent and the instantiate-to-bounds `on` type of the former is a proper subtype of the one on the latter. An `on` type `T?`, with a trailing `?` works like a NNBD nullable type. - If there is no single most-specific extension which applies to a member invocation, then it is a compile-time error. (This includes the case with no applicable extensions, which is just the current behavior). @@ -302,12 +348,14 @@ void add2(T value1, T value2) { - The override can also be used for extensions imported with a prefix (which are not otherwise in scope): `prefix.ExtensionName(object).method(args)`. -- An invocation of an extension method throws if the receiver is `null` unless the `on` type has a trailing `?`. +- An invocation of an extension method throws if the receiver is `null` unless the `on` type has a trailing `?`. With NNBD types, the invocation throws if the receiver is `null` and the instantiated `on` type of the selected extension does not accept `null`. (In most cases, this case can be excluded statically, but not for unsafely nullable types like `int*`). - Otherwise an invocation of an extension method runs the instance method with `this` bound to the receiver and with type variables bound to the types found by type inference (or written explicitly for an override invocation). - Inside an extension member, the current extension is considered more specific than any other extension, so if it applies to an invocation, then it doesn't matter which other extensions also apply. + - Inside an *instance* extension member,: + - invocations on `this` check the current extension's members *before* the `on` type members. - in every other way the static type of `this ` is the `on` type. - A `super` invocations is an invocation on `this` allowing only members of the `on` type. No extension methods apply, from the current extension or any other. @@ -323,8 +371,8 @@ The `on ` clause only allows a single type. The similar clause on `mixin` We could allow multiple types in the `extension` `on` clause as well. It would have the following consequences: - An extension only applies if the receiver type is a subtype of all `on` types. -- An extension is more specific than another if for every `on` type in the latter, there is an `on` type in the former which is a subtype of that type. -- The trailing `?` makes the most sense if it is applied only once (it's the extension which accepts and understands `null` as a receiver), but for forwards compatibility, we will need to put it on every `on` type individually. +- An extension is more specific than another if for every `on` type in the latter, there is an `on` type in the former which is a proper subtype of that type, or the two are equivalent, and the former is a proper subtype of the latter when instantiated to bounds. +- The trailing `?` makes the most sense if it is applied only once (it's the extension which accepts and understands `null` as a receiver), but for forwards compatibility, we will need to put it on every `on` type individually. All `on` types must be nullable in order to accept a nullable receiver. - There is no clear type to assign to `this` inside an instance extension method. For a mixin that's not a problem because it introduces a type by itself, and the combined super-interface is only used for `super` invocations. For extension, a statement like `var self = this;` needs to be assigned a useful type. The last item is the reason this feature is not something we will definitely do. We can start out without the feature and maybe add it later if it is necessary, but it's safer to start without it. @@ -354,7 +402,7 @@ However, while this is possible, not all `on` types are class or mixin types. It For the first two, we could put the static members on the `Iterable` class, but since the extension does not apply to *all* iterables, it is not clear that this is correct. -For `int Function(int)` and `FutureOr`, it's unclear how to call such a static method at all. There is no type literal with type `int Function(int)`. We could put the static method on `Function`, but that's not particularly discoverable, and why not require that they are put on `Functon` explicitly. For `FutureOr`, we could allow static members on `FutureOr`, but again it seems spurious. For `int?`, we could put the method on `int`, but why not just require that it's on `int`. +For `int Function(int)` and `FutureOr`, it's unclear how to call such a static method at all. We can denote`int Function(int)` with a type alias, but putting static members on type aliases is a new concept. We could put the static method on `Function`, but that's not particularly discoverable, and why not require that they are put on `Functon` explicitly. For `FutureOr`, we could allow static members on `FutureOr` (which is a denotable type), but again it seems spurious. For `int?`, we could put the method on `int`, but why not just require that it's on `int`. The issue here is that the type patterns used by `on` are much more powerful than what is necessary to put static members on class types. @@ -369,7 +417,33 @@ static extension Foo on int { // or: extension Foo on static int print(int.fromList([1, 2])); // 2 ``` -where the `on` type must be something that can already have static methods. +where the `on` type must be something that can already have static methods. + +The disadvantage is that if you want to introduce related functionality that is both static and instance methods on a class, then you need to write two extensions with different names. + +If we allow extension static declarations like these, we can also allow extension constructors. + +### Omitting Names For Local Extensions + +If an extension declaration is only used locally in a library, there is no need to worry about naming conflicts or overrides. In that case, then name identifier can be omitted. + +Example: -If we allow extension static declarations like these, we could also allow extension constructors. +```dart +extension on List { + void quadruple() { ... } +} +``` + +This is equivalent to giving the extension a fresh private name. + +The grammar then becomes: + +```ebnf + ::= + `extension' ? ? `on' `?'? `{' + memberDeclaration* + `}' +``` +This is a simple feature, but with very low impact. It only allows you to omit a singl eprivate name for an extension that is only used in a single library. Unless there is a documented demand for this feature, it doesn't seem worth the effort. From 65b0d3bd687dac663be10fb82ecedc38d4fbe9cf Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Mon, 29 Apr 2019 07:29:39 +0200 Subject: [PATCH 03/17] Fix typo --- .../future-releases/static-extension-methods/design-document.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index cc005cf9b6..456d6765cb 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -446,4 +446,4 @@ The grammar then becomes: `}' ``` -This is a simple feature, but with very low impact. It only allows you to omit a singl eprivate name for an extension that is only used in a single library. Unless there is a documented demand for this feature, it doesn't seem worth the effort. +This is a simple feature, but with very low impact. It only allows you to omit a single private name for an extension that is only used in a single library. Unless there is a documented demand for this feature, it doesn't seem worth the effort. From 9e210563d596f442a6b78f1cb53d3dc869bd73af Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Mon, 20 May 2019 15:48:31 +0200 Subject: [PATCH 04/17] Adding a few more variants. --- .../design-document.md | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index 456d6765cb..84b650bbea 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -446,4 +446,48 @@ The grammar then becomes: `}' ``` -This is a simple feature, but with very low impact. It only allows you to omit a single private name for an extension that is only used in a single library. Unless there is a documented demand for this feature, it doesn't seem worth the effort. +This is a simple feature, but with very low impact. It only allows you to omit a singl eprivate name for an extension that is only used in a single library. Unless there is a documented demand for this feature, it doesn't seem worth the effort. + +### Explicitly Specify Related Declarations + +The above resolution strategy uses an implicit "more specific" relation between applicable extensions to select one of them (when possible). This may cause surprises when two unrelated extensions happen to both apply. + +Alternatively, we can allow extensions to declare that they are *related*, and only allow conflicts between related extensions, which are presumably aware of each other. + +The syntax would be: + +```dart +extension Foo on Iterable { twizzle() {...} } +extension Bar extends Foo on List { twizzle() { ... } } +``` + +If both of these extensions apply, then pick the one that extends the other. If there are more related extensions which apply, then pick one which transitively extends all the others. + +If there isn't exactly one among applicable extensions which extends all the rest, the conflict is a compile-time error. + +This approach allows related extensions to declare functionality on a number of types, without accidentally allowing a conflict with an unrelated extension. + +### Explicitly Specify Related Declarations 2 + +Alternative syntax: Allow extensions with the same *name* to be related: + +```dart +extension Foo on Iterable { twizzle() { ... } } +extension Foo on List { twizzle() { ... } } +``` + +Whenever an extension is declared with the same name as another extension in scope, it is considered as being *related*, which is an equivalence relation. The declaration extends the extension rather than conflicting with it. + +Only extension resolution conflicts between related extensions are allowed and resolved, any other conflict between applicable extensions is a compile-time error. We then still need to use an ordering relation on the `on` type to figure out which one is more specific in a particular case, and it can fail if there isn't one most specific applicable extension. + +An explicit override like `Foo(something).twizzle()` would still have to pick the most specific applicable extension. There is no way to hide one part of an extension "cluster", and no way to override with a specific extension declaration since they all have the same name. If the extensions do not have the same number of type parameters, an explicit instantiated override like `Foo(something)` won't apply to all of them, which may be confusing. + +Maybe even allow combined declarations when the extensions do have the same type parameters: + +```dart +extension Foo + on Iterable { twizzle() { ... } } + on List { twizzle() { ... } } +``` + +This shows that it really is a single thing being declared, even if we allow multiple declarations with the same name. (We can also choose not to allow multiple declarations, and require all related extensions to be declared in a single declaration with multiple `on` clauses like above). From 082f3f843d3b7b1e5fe0fb85abcc1adac7402366 Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Mon, 20 May 2019 15:50:55 +0200 Subject: [PATCH 05/17] Refix Typo --- .../future-releases/static-extension-methods/design-document.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index 84b650bbea..22aad78f09 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -446,7 +446,7 @@ The grammar then becomes: `}' ``` -This is a simple feature, but with very low impact. It only allows you to omit a singl eprivate name for an extension that is only used in a single library. Unless there is a documented demand for this feature, it doesn't seem worth the effort. +This is a simple feature, but with very low impact. It only allows you to omit a single private name for an extension that is only used in a single library. Unless there is a documented demand for this feature, it doesn't seem worth the effort. ### Explicitly Specify Related Declarations From 6f74fd43d35861ea50cbaccfa78b77d8c1da42ba Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Fri, 24 May 2019 15:11:41 +0200 Subject: [PATCH 06/17] Simplified the semantics inside extension methods (no special casing, acts like a normal method where `this` has the `on` type as static type) --- .../design-document.md | 161 ++++++++---------- 1 file changed, 75 insertions(+), 86 deletions(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index 22aad78f09..e6f9ee2eba 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -22,7 +22,7 @@ That code is much less readable than: something.doStuff().doMyStuff().doOtherStuff().doMyOtherStuff() ``` -The code is also much less discoverable. An IDE can suggest `doMyStuff()` after `something.doStuff().`, but will be unlikely to suggest putting `doMyOherStuff(…)` around the expression. +The code is also much less discoverable. An IDE can suggest `doMyStuff()` after `something.doStuff().`, but will be unlikely to suggest putting `doMyOtherStuff(…)` around the expression. For these discoverability and readability reasons, static extension methods will allow you to add "extension methods", which are really just static functions, to existing types, and allow you to discover and call those methods as if they were actual methods, using `.`-notation. It will not add any new abilities to the language which are not already available, they just require a more cumbersome and less discoverable syntax to reach. @@ -59,7 +59,32 @@ Such a declaration introduces its *name* (the identifier) into the surrounding s The *type* can be any valid Dart type, including a single type variable. It can refer to the type parameters of the extension. It can be followed by `?` which means that it allows `null` values. When Dart gets non-nullable types by default (NNBD), this `?` syntax is removed and subsumed by nullable types like `int?` being allowed in the `` position. -The member declarations can be any static or instance member declaration except for instance variables and constructors. +The member declarations can be any non-abstract static or instance member declaration except for instance variables and constructors. Abstract members are not allowed since the extension declaration does not introduce an interface, and constructors are not allowed because the extension declaration doesn't introduce any type that can be constructed. Instance variables are not allowed because there won't be any memory allocation per instance that the extension applies to. We could implement instance variables using an `Expando`, but it would necessarily be nullable, so it would still not be an actual instance variable. + +### Omitting Names For Local Extensions + +If an extension declaration is only used locally in a library, there is no need to worry about naming conflicts or overrides. In that case, then name identifier can be omitted. + +Example: + +```dart +extension on List { + void quadruple() { ... } +} +``` + +This is equivalent to giving the extension a fresh private name. + +The grammar then becomes: + +```ebnf + ::= + `extension' ? ? `on' `?'? `{' + memberDeclaration* + `}' +``` + +This is a simple feature, but with very low impact. It only allows you to omit a single private name for an extension that is only used in a single library. ### Scope @@ -69,9 +94,9 @@ Dart's static extension methods are *scoped*. They only apply to code that the e The declaration introduces an extension. The extension's `on` type defines which types are being extended. -For any member access, `x.foo`, `x.bar()`, `x.baz = 42`, `x(42)` or `x + y`, the language first checks whether the static type of `x` has a member with the same base name as the operation. That is, if it has a corresponding instance member, respectively, a `foo` method or getter or a `foo=` setter. a `bar` member or `bar=` setter, a `baz` member or `baz=` setter, a `call` method, or a `+` method. If so, then the operation is unaffected by extensions. *This check does not care whether the invocation is otherwise correct, based on number or type of the arguments, it only checks if there is a member at all.* +For any member access, `x.foo`, `x.bar()`, `x.baz = 42`, `x(42)`, `x[0] = 1` or `x + y`, including null-aware and cascade accesses which effectively desugar to one of those direct accesses, and including implicit member accesses on `this`, the language first checks whether the static type of `x` has a member with the same base name as the operation. That is, if it has a corresponding instance member, respectively, a `foo` method or getter or a `foo=` setter. a `bar` member or `bar=` setter, a `baz` member or `baz=` setter, a `call` method, a `[]=` operator or a `+` operator. If so, then the operation is unaffected by extensions. *This check does not care whether the invocation is otherwise correct, based on number or type of the arguments, it only checks whether there is a member at all.* -(The types `dynamic` and `Never` are considered as having all members, the type `void` is always a compile-time error when used in a receiver position, so none of these can ever be affected by static extension methods). +(The types `dynamic` and `Never` are considered as having all members, the type `void` is always a compile-time error when used in a receiver position, so none of these can ever be affected by static extension methods. Methods declared on `Object` are available on all types and therefore cannot be affected by extensions). If there is no such member, the operation is currently a compile-time error. In that case, all extensions in scope are checked for whether they apply. An extension applies to a member access if the static type of the receiver is a subtype of the `on` type of the extension *and* the extension has an instance member with the same base name as the operation. @@ -86,8 +111,8 @@ extension MyList> on List { and a member access like: ```dart -Uint8List bytes = ...; -bytes.quickSort(); +List times = ...; +meatimesickSort(); ``` Here we perform type inference equivalent to what we would do for: @@ -98,19 +123,19 @@ class MyList> { void quickSort() { ... } } ... -Uint8List bytes = ...; -... MyList(bytes).quickSort() ... // Infer type argument of MyList here. +List times = ...; +... MyList(times).quickSort() ... // Infer type argument of MyList here. ``` -This is inference based on static types only. The inferred type argument becomes the value of `T` for the function invocation that follows. +This is inference based on static types only. The inferred type argument becomes the value of `T` for the function invocation that follows. Notice that the context type of the invocation does not affect whether the extension applies, and neither the context type nor the method invocation affects the type inference, but if the extension method itself is generic, the context type may affect the member invocation. -If the inference fails, or if the synthetic constructor invocation would not be valid for any other reason, then the extension does not apply. If the extension does not have an instance member with the base name `quickSort`, it does not apply. +If the inference fails, or if the synthetic constructor invocation (`MyList(times)` in the above example) would not be statically valid for any other reason, then the extension does not apply. If an extension does not have an instance member with the base name `quickSort`, it does not apply. -If exactly one extension applies to an otherwise failed member invocation, then that invocation becomes an invocation of the corresponding instance member of the extension, with `this` bound to the receiver object and type parameter bound to the inferred types. +If exactly one extension applies to an otherwise failed member invocation, then that invocation becomes an invocation of the corresponding instance member of the extension, with `this` bound to the receiver object and extension type parameters bound to the inferred types. If the member is itself generic and has no type parameters supplied, normal static type inference applies again. -It is *as if* the invocation `bytes.quickSort` was converted to `MyList(bytes).quickSort()`. The difference is that there is never an actual `MyList` object, and the `this` object inside `quickSort` is just the `bytes` list itself (although the extension methods apply to that code too). +It is *as if* the invocation `times.quickSort` was converted to `MyList(times).quickSort()`. The difference is that there is never an actual `MyList` object, and the `this` object inside `quickSort` is just the `times` list itself (although the extension methods apply to that code too). ### Extension Conflict Resolution @@ -150,7 +175,7 @@ Here both the extensions apply, but the `SmartList` extension is more specific t Example: ```dart -extension BoxCom> on Box> { T best() {...} } +extension BoxCom on Box> { T best() {...} } extension BoxList on Box> { T best() {...} } extension BoxSpec on Box> { num best() {...} } ... @@ -162,9 +187,9 @@ extension BoxSpec on Box> { num best() {...} } Here all three extensions apply to both invocations. -For `x.best()`, the most specific one is `BoxList`. Because `Box>` is a proper subtype of both ` Box>` and `Box>`, we expect `BoxList` to be the best implementation. The return type causes `v` to have type `int`. If we had chosen `BoxSpec` instead, the return type could only be `num`, which is why we choose the most specific instantiated type as the winner. +For `x.best()`, the most specific one is `BoxList`. Because `Box>` is a proper subtype of both ` Box>` and `Box>`, we expect `BoxList` to be the best implementation. The return type causes `v` to have type `int`. If we had chosen `BoxSpec` instead, the return type could only be `num`, which is one of the reasons why we choose the most specific instantiated type as the winner. -For `y.best()`, the most specific extension is `BoxSpec`. The instantiated `on` types that are compared are `Box>` for `BoxCom` and `Box>` for the two other. Using the instantiate-to-bounds types as tie-breaker, we find that `Box>` is less precise than `Box>`, so the code of `BoxSpec` has more precise information available for its method implementation. The type of `w` becomes `Box>`. +For `y.best()`, the most specific extension is `BoxSpec`. The instantiated `on` types that are compared are `Box>` for `BoxCom` and `Box>` for the two other. Using the instantiate-to-bounds types as tie-breaker, we find that `Box>` is less precise than `Box>`, so the code of `BoxSpec` has more precise information available for its method implementation. The type of `w` becomes `num`. In practice, unintended extension method name conflicts are likely to be rare. Intended conflicts happen where the same author is providing more specialized versions of an extension for subtypes, and in that case, picking the extension which has the most precise types available to it is considered the best choice. @@ -192,8 +217,6 @@ The syntax looks like a constructor invocation, but it does not create a new obj If `object.quickSort()` would invoke an extension method of `MyList`, then `MyList(object).quickSort()` will invoke the exact same method in the same way. -This mode of invocation also allows invocation of extensions that are *not* in scope, perhaps as `prefix.MyExtension(object).extensionMember()` if imported with a prefix. - The syntax is not *convenient*—you have to put the "constructor" invocation up front, which removes the one advantage that extension methods have over normal static methods. It is not intended as the common use-case, but as an escape hatch out of conflicts. ### Static Members and Member Resolution @@ -211,56 +234,55 @@ extension MySmart on Object { MySmart.smartHelper(someObject); // valid ``` +Like for a class or mixin declaration, static members simply treat the surrounding declaration as a namespace. + ### Semantics of Invocations If an extension is found to be the one applying to a member invocation, then at run-time, the invocation will perform a method invocation of the corresponding instance member of the extension, with `this` bound to the receiver value and type parameters bound to the types found by static inference. If the receiver is `null`, then that invocation is an immediate run-time error unless the `on` type of the extension has a trailing `?`. -With NNBD types, it is a run-time error if the receiver is `null` and the instantiated `on` type of the selected extension does not allow `null`. For sound non-nullable types, this can be excluded statically. If a receiver expression is nullable, then the matching `on` type must be nullable too. If a receiver expression is soundly non-nullable, then the value cannot be `null` at run-time. During migration, we will have unsound nullable types like `int*` which should be matched by `extension Wot on int {…}`, and in that case we will need a run-time check to avoid passing `null` to that extension. - -### Semantics of Extension Members - -When executing an extension instance member, we stated earlier that the member is invoked with the original receiver as `this` object. We still have to describe how that works, and what the lexical scope is for those members. +With NNBD types, a non-nullable `on` type would not match a nullable receiver type, so it is impossible to invoke an extension method that does not expect `null` on a `null` value (except with legacy unsafely nullable types, then it's still a run-time error if the `on` type is not nullable) -Inside an extension declaration, we give preference to that extension over any other extension. At any otherwise invalid invocation, if the current extension applies, then it is considered more specific than all other extensions that may apply. This applies to static methods too. +In a fully migrated NNBD mode program, an extension with a non-nullable `on` type does not apply to a receiver with a nullable type, and a nullable `on` type means that the `this` value may be `null` inside the extension methods. -Example: +During NNBD migration, where non-nullable type may contain `null`, it stays a run-time error if an extension method is called on `null` and a migrated extension's `on` type does not allow null, or an unmigrated extension does not have a trailing `?` on the `on` type. -```dart -extension MyUnaryNumber on List { - bool get isEven => this.length.isEven; - bool get isOdd => !this.isEven; - static bool isListEven(List list) => list.isEven; -} -``` +During NNBD migration, we will have unsound nullable types like `int*` which should be matched by `extension Wot on int {…}` (in migrated code), and in that case we will need a run-time check to avoid passing `null` to that extension. Unmigrated extensions will still need the trailing `?` to allow getting called with `null` as receiver, but they will apply to nullable types everywhere. -Here the `list.isEven` is guaranteed to hit the `isEven` of the same extension (unless someone puts an `isEven` member on `List`), an extension has more specificity than any other extension inside itself. +### Semantics of Extension Members -About *`this`*: Inside an instance member of an extension, the static type of the `this` operator is the `on` type. However, for member invocations, we give preference to extension methods of the current extension, *even over members of the `on` type*. +When executing an extension instance member, we stated earlier that the member is invoked with the original receiver as `this` object. We still have to describe how that works, and what the lexical scope is for those members. -Here, the `this.isEven` is a member invocation on `this`, so it *first* tries to resolve `isEven` against the current extension. If it finds a member with the same base name on the extension, that member is invoked with the same `this` value. If not, then it is considered a normal member invocation on an object with static type `List,` and if `List` does not have the necessary member, then all applicable extensions are checked (which will definitely not match the current extension since we already checked that). +Inside an extension method body, `this` is bound to the original receiver, and the static type of `this` is the `on` type (which may contain type variables as usual). -The behavior is *only* for the `this` operator. Doing something like `var self = this; self.isEven;` will not give preference over members of `List` (if `List` had an `isEven`, then `self.isEven` would invoke that, where `this.isEven` will invoke the extension method). It is as if `this` has a type representing the extension, even if no such type actually exists. +Invocations on `this` use the same extension method resolution as any other code. Most likely the current extension will be the only one in scope which applies. -This behavior makes the extension act similarly to a class, which will hopefully make it easier for users to understand the resolution. +Like for a class or mixin member declaration, the names of the extension members, both static and instance, are in the *lexical* scope of the extension member body. That is why `MySmart` above can invoke the static `smartHelper` without prefixing it by the extension name. In the same way, *instance* members are in the lexical scope. -Like for a class or mixin member declaration, the names of the extension members, both static and instance, are in the *lexical* scope of the extension member. That is why `MySmart` above can invoke the static `smartHelper` without prefixing it by the extension name. In the same way, *instance* members are in the lexical scope. +If an unqualified identifier may lexically resolves to an extension method, the invocation becomes an explicit invocation of that extension method on `this` (which we already know has a compatible type for the extension). Example: ```dart extension MyUnaryNumber on List { - bool get isEven => this.length.isEven; - bool get isOdd => !isEven; // not `!this.isEven` + bool get isEven => length.isEven; + bool get isOdd => !isEven; + static bool isListEven(List list) => list.isEven; } ``` -Here, the `isEven` name exists in the surrounding static scope, so just as for `class` members, it is considered equivalent to `this.isEven`. Because of how we treat `this`, it means that `isEven` and `this.isEven` means the same thing when the extension declares an `isEven` member, just as for a class or mixin declaration. +Here the `list.isEven` will find that `isEven` of `MyUnaryNumber` applies, and unless there are any other extensions in scope, it will call that. + +The unqualified `length` of `isEven` is not defined in the current lexical scope, so is equivalent to `this.length`, which is valid since `List` has a `length` getter. + +The unqualified `isEven` of `isOdd` resolves lexically to the `isEvent` getter above it, so it is equivalent to `MyUnaryNumber(this).isEven`, even if there are other extensions in scope which define an `isEven` on `List`. + +Even though you can access `this`, you cannot use `super` inside an extension method. ### Member Conflict Resolution -An extension can declare a member with the same (base-)name as the type it is declared on. This does not cause a compile-time conflict, even if the member does not have a compatible signature. +An extension can declare a member with the same (base-)name as a member of the type it is declared on. This does not cause a compile-time conflict, even if the member does not have a compatible signature. Example: @@ -273,13 +295,7 @@ extension MyList on List { You cannot *access* this member in a normal invocation, so it could be argued that you shouldn't be allowed to add it. We allow it because we do not want to make it a compile-time error for a type to add a method just because an extension is already adding a method with the same name. it will likely be a problem if any code *uses* the method, but only that code needs to change (perhaps using an override). -That also means that an extension can shadow members of the `on` type. To implement `add2` above, we don't want to call `this.add`, instead we want to use the `add` of `List` directly. To do that, we allow `super` invocations to target the underlying `on` type directly, with *no* extensions applying. - -```dart -void add2(T value1, T value2) { - super..add(value1)..add(value2); -} -``` +An unqualified identifier can refer to any member declaration of the extension, so inside an extension member body, `this.add` and `add` are not necessarily the same thing. This may be confusing. In practice, extensions will rarely introduce members with the same name as their `on` type's members. ### Tearoffs @@ -316,7 +332,7 @@ There is still no way to tear off getters, setters or operators. If we ever intr - The extension declaration introduces a name (``) into the surrounding scope. - The name can be shown or hidden in imports/export. It can be shadowed by other declarations as any other top-level declaration. - - The name can be used as suffix for invoking static members (used as a namespace, same as class/mixin declarations). + - The name can be used as prefix for invoking static members (used as a namespace, same as class/mixin declarations). - A member invocation (getter/setter/method/operator) which targets a member that is not on the static type of the receiver (no member with same base-name is available) is subject to extension application. It would otherwise be a compile-time error. @@ -344,21 +360,15 @@ There is still no way to tear off getters, setters or operators. If we ever intr - Otherwise, the single most-specific extension's member is invoked with the extension's type parameters bound to the types found by inference, and with `this ` bound to the receiver. -- An extension method can be invoked explicitly using the syntax `ExtensionName(object).method(args)`. Type arguments can be applied to the extension explicitly as well, `MyList(listOfString).quickSort()`. Such an invocation overrides all extension resolution. It is a compile-time error if `ExtensionName` would not apply to the `object.method(args)` invocation if it was in scope. +- An extension method can be invoked explicitly using the syntax `ExtensionName(object).method(args)`. Type arguments can be applied to the extension explicitly as well, `MyList(listOfString).quickSort()`. Such an invocation overrides all extension resolution. It is a compile-time error if `ExtensionName` would not apply to the `object.method(args)` invocation if it was in scope. - The override can also be used for extensions imported with a prefix (which are not otherwise in scope): `prefix.ExtensionName(object).method(args)`. - An invocation of an extension method throws if the receiver is `null` unless the `on` type has a trailing `?`. With NNBD types, the invocation throws if the receiver is `null` and the instantiated `on` type of the selected extension does not accept `null`. (In most cases, this case can be excluded statically, but not for unsafely nullable types like `int*`). -- Otherwise an invocation of an extension method runs the instance method with `this` bound to the receiver and with type variables bound to the types found by type inference (or written explicitly for an override invocation). - -- Inside an extension member, the current extension is considered more specific than any other extension, so if it applies to an invocation, then it doesn't matter which other extensions also apply. +- Otherwise an invocation of an extension method runs the instance method with `this` bound to the receiver and with type variables bound to the types found by type inference (or written explicitly for an override invocation). The static type of `this` is the `on` type of the extension. -- Inside an *instance* extension member,: - - - invocations on `this` check the current extension's members *before* the `on` type members. - - in every other way the static type of `this ` is the `on` type. - - A `super` invocations is an invocation on `this` allowing only members of the `on` type. No extension methods apply, from the current extension or any other. +- Inside an instance extension member, extension members accessed by unqualified name are treated as extension override accesses on `this`. Otherwise invocations on `this` are treated as any other invocations on the same static type. ## Variants @@ -370,7 +380,7 @@ The `on ` clause only allows a single type. The similar clause on `mixin` We could allow multiple types in the `extension` `on` clause as well. It would have the following consequences: -- An extension only applies if the receiver type is a subtype of all `on` types. +- An extension only applies if the receiver type is a subtype of *all* `on` types. - An extension is more specific than another if for every `on` type in the latter, there is an `on` type in the former which is a proper subtype of that type, or the two are equivalent, and the former is a proper subtype of the latter when instantiated to bounds. - The trailing `?` makes the most sense if it is applied only once (it's the extension which accepts and understands `null` as a receiver), but for forwards compatibility, we will need to put it on every `on` type individually. All `on` types must be nullable in order to accept a nullable receiver. - There is no clear type to assign to `this` inside an instance extension method. For a mixin that's not a problem because it introduces a type by itself, and the combined super-interface is only used for `super` invocations. For extension, a statement like `var self = this;` needs to be assigned a useful type. @@ -423,31 +433,6 @@ The disadvantage is that if you want to introduce related functionality that is If we allow extension static declarations like these, we can also allow extension constructors. -### Omitting Names For Local Extensions - -If an extension declaration is only used locally in a library, there is no need to worry about naming conflicts or overrides. In that case, then name identifier can be omitted. - -Example: - -```dart -extension on List { - void quadruple() { ... } -} -``` - -This is equivalent to giving the extension a fresh private name. - -The grammar then becomes: - -```ebnf - ::= - `extension' ? ? `on' `?'? `{' - memberDeclaration* - `}' -``` - -This is a simple feature, but with very low impact. It only allows you to omit a single private name for an extension that is only used in a single library. Unless there is a documented demand for this feature, it doesn't seem worth the effort. - ### Explicitly Specify Related Declarations The above resolution strategy uses an implicit "more specific" relation between applicable extensions to select one of them (when possible). This may cause surprises when two unrelated extensions happen to both apply. @@ -457,8 +442,8 @@ Alternatively, we can allow extensions to declare that they are *related*, and o The syntax would be: ```dart -extension Foo on Iterable { twizzle() {...} } -extension Bar extends Foo on List { twizzle() { ... } } +extension Foo on Iterable { twizzle() {...} fromp() { ... }} +extension Bar extends Foo on List { twizzle() { ... } } ``` If both of these extensions apply, then pick the one that extends the other. If there are more related extensions which apply, then pick one which transitively extends all the others. @@ -467,6 +452,10 @@ If there isn't exactly one among applicable extensions which extends all the res This approach allows related extensions to declare functionality on a number of types, without accidentally allowing a conflict with an unrelated extension. +The extending extension must be defined on a subtype of its super-extension. In the above, the `on` type of `Bar` is `List`, which is a subtype of the `on` type of `Foo`, which is `Iterable`. Also, the declared extension methods in the extending extension must be valid overrides of the same-named super-extensions extension methods, as if it was a subclass relationship. + +Using `Bar(myList).fromp()` would work just like `Foo(myList).fromp()`, as if `Bar` inherits the extension methods of `Foo` that it doesn't declare itself. (Maybe we can even allow `super.twizzle()` calls, which would work like `Foo(this).twizzle()`). + ### Explicitly Specify Related Declarations 2 Alternative syntax: Allow extensions with the same *name* to be related: From 65cf7002beead784b2edd9388bc30bd0e06f0e4c Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Tue, 28 May 2019 13:41:35 +0200 Subject: [PATCH 07/17] Address more comments. Add variant where we use type aliases for renaming to overcome scoping issues. --- .../design-document.md | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index e6f9ee2eba..1e282c2a67 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -50,7 +50,7 @@ More precisely, an extension declaration is a top-level declaration with a gramm ```ebnf ::= - `extension' ? `on' `?'? `{' + `extension' ? ? `on' `?'? `{' memberDeclaration* `}' ``` @@ -63,7 +63,7 @@ The member declarations can be any non-abstract static or instance member declar ### Omitting Names For Local Extensions -If an extension declaration is only used locally in a library, there is no need to worry about naming conflicts or overrides. In that case, then name identifier can be omitted. +If an extension declaration is only used locally in a library, there is no need to worry about naming conflicts or overrides. In that case, then name identifier can be omitted (hence the `?` in the grammar above). Example: @@ -75,14 +75,7 @@ extension on List { This is equivalent to giving the extension a fresh private name. -The grammar then becomes: - -```ebnf - ::= - `extension' ? ? `on' `?'? `{' - memberDeclaration* - `}' -``` +We may need to make `on` a built-in identifier, and not allow those as names of extensions, then there should not be any parsing issue. Even without that, the grammar should be unambiguous because `extension on on on { … }` and `extension on on { … }` are distinguishable, and the final type cannot be empty. It may be *harder* to parse, though. This is a simple feature, but with very low impact. It only allows you to omit a single private name for an extension that is only used in a single library. @@ -112,7 +105,7 @@ and a member access like: ```dart List times = ...; -meatimesickSort(); +times.quickSort(); ``` Here we perform type inference equivalent to what we would do for: @@ -258,9 +251,9 @@ Inside an extension method body, `this` is bound to the original receiver, and t Invocations on `this` use the same extension method resolution as any other code. Most likely the current extension will be the only one in scope which applies. -Like for a class or mixin member declaration, the names of the extension members, both static and instance, are in the *lexical* scope of the extension member body. That is why `MySmart` above can invoke the static `smartHelper` without prefixing it by the extension name. In the same way, *instance* members are in the lexical scope. +Like for a class or mixin member declaration, the names of the extension members, both static and instance, are in the *lexical* scope of the extension member body. That is why `MySmart` above can invoke the static `smartHelper` without prefixing it by the extension name. In the same way, *instance* member declarations (the extension members) are in the lexical scope. -If an unqualified identifier may lexically resolves to an extension method, the invocation becomes an explicit invocation of that extension method on `this` (which we already know has a compatible type for the extension). +If an unqualified identifier may lexically resolve to an extension method, the invocation becomes an explicit invocation of that extension method on `this` (which we already know has a compatible type for the extension). Example: @@ -272,7 +265,7 @@ extension MyUnaryNumber on List { } ``` -Here the `list.isEven` will find that `isEven` of `MyUnaryNumber` applies, and unless there are any other extensions in scope, it will call that. +Here the `list.isEven` will find that `isEven` of `MyUnaryNumber` applies, and unless there are any other extensions in scope, it will call that. (Or unless someone adds an `isEven` member to `List`, but that's a breaking change, and then, if still necessary, this code can change the call to `MyUnaryNumber(list).isEven`.) The unqualified `length` of `isEven` is not defined in the current lexical scope, so is equivalent to `this.length`, which is valid since `List` has a `length` getter. @@ -293,9 +286,9 @@ extension MyList on List { } ``` -You cannot *access* this member in a normal invocation, so it could be argued that you shouldn't be allowed to add it. We allow it because we do not want to make it a compile-time error for a type to add a method just because an extension is already adding a method with the same name. it will likely be a problem if any code *uses* the method, but only that code needs to change (perhaps using an override). +You cannot *access* this member in a normal invocation, so it could be argued that you shouldn't be allowed to add it. We allow it because we do not want to make it a compile-time error to add an instance member to an existing class just because an extension is already adding a method with the same name. It will likely be a problem if any code *uses* the method, but only that code needs to change (perhaps using an override to keep using the extension). -An unqualified identifier can refer to any member declaration of the extension, so inside an extension member body, `this.add` and `add` are not necessarily the same thing. This may be confusing. In practice, extensions will rarely introduce members with the same name as their `on` type's members. +An unqualified identifier can refer to any extension member declaration of the extension, so inside an extension member body, `this.add` and `add` are not necessarily the same thing (if the `on` type has an `add` member, then `this.add` refers to that, while `add` refers to the extension method in the lexical scope). This may be confusing. In practice, extensions will rarely introduce members with the same name as their `on` type's members. ### Tearoffs @@ -321,13 +314,13 @@ There is still no way to tear off getters, setters or operators. If we ever intr - Extensions are declared using the syntax: ```ebnf - ::= `extension' ? `on' `?'? + ::= `extension' ? ? `on' `?'? `{' * `}' ``` - where `extension` becomes a built-in identifier, `` must not be a type variable, and `` does not allow instance fields or constructors. It does allow static members. + where `extension` becomes a built-in identifier and `` does not allow instance variables, constructors or abstract members. It does allow static members. - The extension declaration introduces a name (``) into the surrounding scope. @@ -360,7 +353,7 @@ There is still no way to tear off getters, setters or operators. If we ever intr - Otherwise, the single most-specific extension's member is invoked with the extension's type parameters bound to the types found by inference, and with `this ` bound to the receiver. -- An extension method can be invoked explicitly using the syntax `ExtensionName(object).method(args)`. Type arguments can be applied to the extension explicitly as well, `MyList(listOfString).quickSort()`. Such an invocation overrides all extension resolution. It is a compile-time error if `ExtensionName` would not apply to the `object.method(args)` invocation if it was in scope. +- An extension method can be invoked explicitly using the syntax `ExtensionName(object).method(args)`. Type arguments can be applied to the extension explicitly as well, `MyList(listOfString).quickSort()`. Such an invocation overrides all extension resolution. It is a compile-time error if `ExtensionName` would not apply to the `object.method(args)` invocation if it was in scope. - The override can also be used for extensions imported with a prefix (which are not otherwise in scope): `prefix.ExtensionName(object).method(args)`. @@ -480,3 +473,29 @@ extension Foo ``` This shows that it really is a single thing being declared, even if we allow multiple declarations with the same name. (We can also choose not to allow multiple declarations, and require all related extensions to be declared in a single declaration with multiple `on` clauses like above). + +### Aliasing + +If we have two different extensions with the same name, they can't both be in scope, even if they don't apply to the same types. At least one of them must be delegated to a prefixed import scope, and if so, it doesn't *work* as an extension method any more. + +To overcome this issue, we can use a *generalized typedef* to give a new name to an existing entity in a given scope. Example: + +```dart +typedef MyCleverList = prefix.MyList; +``` + +If `prefix.MyList` is an extension, this would put that extension back in the current scope under a different name (use a private name to avoid exporting the extension again). + +If we do this, we should be *consistent* with other type aliases, which means that the type parameter of the RHS must be explicit. Just writing + +```drt +typedef MyCleverList = prefix.MyList; // bad! +``` + +would make `MyCleverList` an alias for `prefix.MyList`, which would still apply to `List`, but the type variable of `MyList` will always be `dynamic`. Similarly, we can put more bounds on the type variable: + +```dart +typedef MyWidgetList = prefix.MyList; +``` + +Here the extension will only apply if it matches `Widget` *and* would otherwise match `MyList` (but `T` needs to be a valid type argument to `MyList`, which means that it must satisfy all bounds of `MyList` as well, otherwise the typedef is rejected). From aa303441925472599f39fa0c59520ba60ab0be97 Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Mon, 3 Jun 2019 07:26:26 +0200 Subject: [PATCH 08/17] Fix example to not use unnecessary `Box` type --- .../static-extension-methods/design-document.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index 1e282c2a67..d2f24b3b1d 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -168,9 +168,9 @@ Here both the extensions apply, but the `SmartList` extension is more specific t Example: ```dart -extension BoxCom on Box> { T best() {...} } -extension BoxList on Box> { T best() {...} } -extension BoxSpec on Box> { num best() {...} } +extension BestCom on Iterable { T best() {...} } +extension BestList on List { T best() {...} } +extension BestSpec on List { num best() {...} } ... List x = ...; var v = x.best(); @@ -180,12 +180,15 @@ extension BoxSpec on Box> { num best() {...} } Here all three extensions apply to both invocations. -For `x.best()`, the most specific one is `BoxList`. Because `Box>` is a proper subtype of both ` Box>` and `Box>`, we expect `BoxList` to be the best implementation. The return type causes `v` to have type `int`. If we had chosen `BoxSpec` instead, the return type could only be `num`, which is one of the reasons why we choose the most specific instantiated type as the winner. +For `x.best()`, the most specific one is `BestList`. Because `List` is a proper subtype of both ` iterable` and ``, we expect `BestList` to be the best implementation. The return type causes `v` to have type `int`. If we had chosen `BestSpec` instead, the return type could only be `num`, which is one of the reasons why we choose the most specific instantiated type as the winner. -For `y.best()`, the most specific extension is `BoxSpec`. The instantiated `on` types that are compared are `Box>` for `BoxCom` and `Box>` for the two other. Using the instantiate-to-bounds types as tie-breaker, we find that `Box>` is less precise than `Box>`, so the code of `BoxSpec` has more precise information available for its method implementation. The type of `w` becomes `num`. +For `y.best()`, the most specific extension is `BestSpec`. The instantiated `on` types that are compared are `Iterable` for `Best +Com` and `List` for the two other. Using the instantiate-to-bounds types as tie-breaker, we find that `List` is less precise than `List`, so the code of `BestSpec` has more precise information available for its method implementation. The type of `w` becomes `num`. In practice, unintended extension method name conflicts are likely to be rare. Intended conflicts happen where the same author is providing more specialized versions of an extension for subtypes, and in that case, picking the extension which has the most precise types available to it is considered the best choice. + + ### Overriding Access If two or more extensions apply to the same member access, or if a member of the receiver type takes precedence over an extension method, or if the extension is imported with a prefix, then it is possible to force an extension member invocation: From 3665b36f13663b0c4324e7a9d67df617dce824df Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Thu, 6 Jun 2019 15:33:46 +0200 Subject: [PATCH 09/17] Update section on `extends` variant. --- .../design-document.md | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index d2f24b3b1d..1011c3e053 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -132,7 +132,7 @@ It is *as if* the invocation `times.quickSort` was converted to `MyList1 is more specific than another extension with `on` type clause *T*2 iff the instantiated type (the type after applying type inference from the receiver) of *T*1 is a subtype of the instantiated type of *T*2 and either: @@ -187,8 +187,6 @@ Com` and `List` for the two other. Using the instantiate-to-bounds types as In practice, unintended extension method name conflicts are likely to be rare. Intended conflicts happen where the same author is providing more specialized versions of an extension for subtypes, and in that case, picking the extension which has the most precise types available to it is considered the best choice. - - ### Overriding Access If two or more extensions apply to the same member access, or if a member of the receiver type takes precedence over an extension method, or if the extension is imported with a prefix, then it is possible to force an extension member invocation: @@ -431,9 +429,15 @@ If we allow extension static declarations like these, we can also allow extensio ### Explicitly Specify Related Declarations -The above resolution strategy uses an implicit "more specific" relation between applicable extensions to select one of them (when possible). This may cause surprises when two unrelated extensions happen to both apply. +The conflict resolution described above is heuristic. It chooses a winner among potential extensions based entirely on the `on` type. + +If the applicable extensions are deliberately written to co-exist, with some extensions using more efficient algorithms on more precisely know receivers, then that conflict resolution algorithm gives the extension authors a clean and, hopefully, understandable way to control which extension is chosen in which situation. A more precise algorithm will be chosen over a less precise one (say, an operation on `List` over one on`Iterable`). + +However, if the applicable extensions are unrelated, written by authors which are oblivious to the other extensions, then the choice risks being arbitrary. A small change, say from `extension Foo on num` to `extension Foo on T`, which will not change where the extension applies, will change the specificity ordering. That makes the conflict resolution heuristic *fragile*. -Alternatively, we can allow extensions to declare that they are *related*, and only allow conflicts between related extensions, which are presumably aware of each other. +There is no way for an author to express the intent that one extension is a specialization of another, and that another one isn't. The only author intent available is in which extensions are imported in the library which triggers the extension member. However, anyone can add an extension to an existing library, or change the specificity of an extension (generalizing an extension with `on` type of `int` to work on all `num`s), which should be non-breaking changes, but which affects conflict resolution. + +Alternatively, we can allow extensions to declare that they are *related*, and only allow conflicts between related extensions, which are presumably aware of each other. Then a conflict between any two unrelated extensions is a compile-time error and will require an explicit override, which will also be stable against future changes. The syntax would be: @@ -444,13 +448,19 @@ extension Bar extends Foo on List { twizzle() { ... } } If both of these extensions apply, then pick the one that extends the other. If there are more related extensions which apply, then pick one which transitively extends all the others. +It should probably be possible to declare that an extension `extends` more than one other extension. That allows an extension hierarchy which matches a diamond type pattern. If you can only extend a single extension, only tree-shaped type hierarchies can be matched by the extensions. + If there isn't exactly one among applicable extensions which extends all the rest, the conflict is a compile-time error. -This approach allows related extensions to declare functionality on a number of types, without accidentally allowing a conflict with an unrelated extension. +This approach allows related extensions to declare functionality on a number of types, without accidentally allowing a conflict with an unrelated extension. There is no heuristic in the conflict resolution, the choice is made entirely based on existing information added explicitly to make that choice. At least one author has made the explicit choice of declaring that if all the currently applicable extensions are available, their extension should be used. + +This should be sufficient to avoid arbitrary extensions from applying. -The extending extension must be defined on a subtype of its super-extension. In the above, the `on` type of `Bar` is `List`, which is a subtype of the `on` type of `Foo`, which is `Iterable`. Also, the declared extension methods in the extending extension must be valid overrides of the same-named super-extensions extension methods, as if it was a subclass relationship. +On top of that, we *may* want to require that the extending extension is defined `on` a subtype of its super-extension. In the above, the `on` type of `Bar` is `List`, which is a subtype of the `on` type of `Foo`, which is `Iterable`. This is not strictly required, and an extending extension could also be allowed to be declared on a supertype of the extended extension (an unrelated type would mean that the `extends` is likely unnecessary). The only reason to make the restriction is that this feature is intended for *specialization*, not wholesale replacement. Even if we do this, the subtype does not have to be a proper subtype, so it's still possible to completely shadow the extended extension. -Using `Bar(myList).fromp()` would work just like `Foo(myList).fromp()`, as if `Bar` inherits the extension methods of `Foo` that it doesn't declare itself. (Maybe we can even allow `super.twizzle()` calls, which would work like `Foo(this).twizzle()`). +Also, we may, and perhaps should, require that the declared extension methods in the extending extension must be valid overrides of the same-named super-extension's extension methods, as if it was a subclass relationship. This will ensure that a user who only knows about the super-extension will still get a consistent signature when they instead hit the extending extension. + +If we do require the extending extension's `on` type to be a subtype of the extended extension's `on` type, then we can also let the extending extension *inherit* any member that it doesn't specialize. This will not work if there is more than one super-extension declaring the same member. It's unclear whether this has any advantage. ### Explicitly Specify Related Declarations 2 @@ -477,6 +487,8 @@ extension Foo This shows that it really is a single thing being declared, even if we allow multiple declarations with the same name. (We can also choose not to allow multiple declarations, and require all related extensions to be declared in a single declaration with multiple `on` clauses like above). +This approach works *badly* with static members. + ### Aliasing If we have two different extensions with the same name, they can't both be in scope, even if they don't apply to the same types. At least one of them must be delegated to a prefixed import scope, and if so, it doesn't *work* as an extension method any more. @@ -502,3 +514,10 @@ typedef MyWidgetList = prefix.MyList; ``` Here the extension will only apply if it matches `Widget` *and* would otherwise match `MyList` (but `T` needs to be a valid type argument to `MyList`, which means that it must satisfy all bounds of `MyList` as well, otherwise the typedef is rejected). + +The use of `typedef` for something which is not a type may be too confusing. Another option is: + +```dart +extension MyWidgetList = prefix.MyList; +``` + From 8a604703e98240c6c689f728190c4be80f5e156d Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Wed, 12 Jun 2019 07:38:23 +0200 Subject: [PATCH 10/17] Updated according to discussion. Extension names may conflict without affecting the implicit use of their declared members. Only access to the *name* is an error (and that can be double-imported with a prefix if necessary). Pre-NNBBD, an `on` type of `Object` or `Null` will allow a `null` value at run-time, even without a trailing `?`. Removed the "explicit extension" variants. We are not going with those. --- .../design-document.md | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index 1011c3e053..9b59aafc2e 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -81,7 +81,34 @@ This is a simple feature, but with very low impact. It only allows you to omit a ### Scope -Dart's static extension methods are *scoped*. They only apply to code that the extension itself is accessible to. Being accessible means that using the extension's name must denote the extension. The extension is not in scope if another declaration with the same name shadows the extension, if the extension's library is not imported, if the library is imported and the extension is hidden, or the library is only imported with a prefix. In other words, if the extension had been a class, it is only in scope if using the name would denote the class— which will allow you to call static methods on the class, which is exactly what we are going to do. +Dart's static extension methods are *scoped*. They only apply to code where the extension itself is *in scope*. Being in scope means that the extension is declared or imported into a scope which is a parent scope of the current lexical scope. The extension is not in scope if the extension declaration's library is not imported, if the library is imported and the extension is hidden, or the library is only imported with a prefix. + +An extension *is* in scope if the name is *shadowed* by another declaration (a class or local variable with the same name shadowing a top-level or imported declaration, a top-level declaration shadowing an imported extension, or a non-platform import shadowing a platform import). + +An extension *is* in scope if is imported, and the extension name conflicts with one or more other imported declarations. + +The usual rules applies to referencing the extension by name, which can be useful in some situations; the extension's *name* is only accessible if it is not shadowed and not conflicting with another imported declaration. + +If an extension conflicts with, or is shadowed by, another declaration, and you need to access it by name anyway, it can be imported with a prefix and the name referenced through that prefix. + +Example: + +```dart +import "all.dart"; // exposes extensions `Foo`, `Bar` and `Baz`. +import "bar.dart"; // exposes another extension named `Bar`. +import "bar.dart" as b; // Also import with prefix. +class Foo {} +main() { + Foo(); // refers to class declartion. + Baz("ok").baz(); // Explicit reference to `Baz` extension. + Bar("ok").bar(); // *Compile-time error*, `Bar` name has conflict. + b.Bar("ok").bar(); // Valid explicit reference to `Bar` from bar.dart. +} +``` + +*Rationale*: We want users to have control over which extensions are available. They control this through the imports and declarations used to include declarations into the import scope or declaration scope of the library. The typical ways to control the import scope is using `show` /`hide` in the imports or importing into a prefix scope. These features work exactly the same for extensions. On the other hand, we do not want extension writers to have to worry too much about name clashes for their extension names since most extension members are not accessed through that name anyway. In particular we do not want them to name-mangle their extensions in order to avoid hypothetical conflicts. So, all imported extensions are considered in scope, and choosing between the individual extensions is handled as described in the next section. You only run into problems with the extension name if you try to use the name itself. That way you can import two extensions with the same name and use the members without issue (as long as they don't conflict in an unresolvable way), even if you can only refer to *at most* one of them by name. + +You still cannot *export* two extensions with the same name. ### Extension Member Resolution @@ -211,7 +238,7 @@ The syntax looks like a constructor invocation, but it does not create a new obj If `object.quickSort()` would invoke an extension method of `MyList`, then `MyList(object).quickSort()` will invoke the exact same method in the same way. -The syntax is not *convenient*—you have to put the "constructor" invocation up front, which removes the one advantage that extension methods have over normal static methods. It is not intended as the common use-case, but as an escape hatch out of conflicts. +The syntax is not *convenient*—you have to put the "constructor" invocation up front, which removes the one advantage that extension methods have over normal static methods. It is not intended as the common use-case, but as an escape hatch out of unresolvable conflicts. ### Static Members and Member Resolution @@ -234,13 +261,13 @@ Like for a class or mixin declaration, static members simply treat the surroundi If an extension is found to be the one applying to a member invocation, then at run-time, the invocation will perform a method invocation of the corresponding instance member of the extension, with `this` bound to the receiver value and type parameters bound to the types found by static inference. -If the receiver is `null`, then that invocation is an immediate run-time error unless the `on` type of the extension has a trailing `?`. +Prior to NNBD, if the `on` type does not have a trailing `?`, it is a run-time error if the receiver object *r* is `null` and the resolved `on` type *T* of the extension would not satisfy `r is T`. That is, unless the type is `Null`, `Object`, or `dynamic`, it will not accept `null`. This ensure that the `this` value of the extension member cannot be `null` unless the receiver type is `Null` or a top type . If the `on` type does have a trailing `?`, then the `this` value can be `null`. -With NNBD types, a non-nullable `on` type would not match a nullable receiver type, so it is impossible to invoke an extension method that does not expect `null` on a `null` value (except with legacy unsafely nullable types, then it's still a run-time error if the `on` type is not nullable) +With NNBD types, a non-nullable `on` type would not match a nullable receiver type, so it is impossible to invoke an extension method that does not expect `null` on a `null` value (except with legacy unsafely nullable types, then it's still a run-time error if the `on` type is not nullable). In a fully migrated NNBD mode program, an extension with a non-nullable `on` type does not apply to a receiver with a nullable type, and a nullable `on` type means that the `this` value may be `null` inside the extension methods. -During NNBD migration, where non-nullable type may contain `null`, it stays a run-time error if an extension method is called on `null` and a migrated extension's `on` type does not allow null, or an unmigrated extension does not have a trailing `?` on the `on` type. +During NNBD migration, where non-nullable type may contain `null`, it stays a run-time error if an extension method is called on `null` and a migrated extension's `on` type does not allow null, or an unmigrated extension does not have a trailing `?` on the `on` type and isn't `Null` or a top type. During NNBD migration, we will have unsound nullable types like `int*` which should be matched by `extension Wot on int {…}` (in migrated code), and in that case we will need a run-time check to avoid passing `null` to that extension. Unmigrated extensions will still need the trailing `?` to allow getting called with `null` as receiver, but they will apply to nullable types everywhere. @@ -248,13 +275,13 @@ During NNBD migration, we will have unsound nullable types like `int*` which sho When executing an extension instance member, we stated earlier that the member is invoked with the original receiver as `this` object. We still have to describe how that works, and what the lexical scope is for those members. -Inside an extension method body, `this` is bound to the original receiver, and the static type of `this` is the `on` type (which may contain type variables as usual). +Inside an extension method body, `this` does not refer to an instance of a surrounding type. Instead it is bound to the original receiver, and the static type of `this` is the declared `on` type of the surrounding extension (which may contain unbound type variables). -Invocations on `this` use the same extension method resolution as any other code. Most likely the current extension will be the only one in scope which applies. +Invocations on `this` use the same extension method resolution as any other code. Most likely the current extension will be the only one in scope which applies. It definitely applies to its own declared `on` type. Like for a class or mixin member declaration, the names of the extension members, both static and instance, are in the *lexical* scope of the extension member body. That is why `MySmart` above can invoke the static `smartHelper` without prefixing it by the extension name. In the same way, *instance* member declarations (the extension members) are in the lexical scope. -If an unqualified identifier may lexically resolve to an extension method, the invocation becomes an explicit invocation of that extension method on `this` (which we already know has a compatible type for the extension). +If an unqualified identifier lexically resolves to an extension method of the surrounding extension, then that identifier is not equivalent to `this.id`, rather the invocation is equivalent to an explicit invocation of that extension method on `this` (which we already know has a compatible type for the extension): `Ext(this).id`, where `Ext` is the surrounding extension and `T1` through `Tn` are its type parameters, if any. The invocation works whether or not the names of the extension or parameters are actually accessible, it is not a syntactic rewrite. Example: @@ -289,7 +316,7 @@ extension MyList on List { You cannot *access* this member in a normal invocation, so it could be argued that you shouldn't be allowed to add it. We allow it because we do not want to make it a compile-time error to add an instance member to an existing class just because an extension is already adding a method with the same name. It will likely be a problem if any code *uses* the method, but only that code needs to change (perhaps using an override to keep using the extension). -An unqualified identifier can refer to any extension member declaration of the extension, so inside an extension member body, `this.add` and `add` are not necessarily the same thing (if the `on` type has an `add` member, then `this.add` refers to that, while `add` refers to the extension method in the lexical scope). This may be confusing. In practice, extensions will rarely introduce members with the same name as their `on` type's members. +An unqualified identifier in the extension can refer to any extension member declaration, so inside an extension member body, `this.add` and `add` are not necessarily the same thing (if the `on` type has an `add` member, then `this.add` refers to that, while `add` refers to the extension method in the lexical scope). This may be confusing. In practice, extensions will rarely introduce members with the same name as their `on` type's members. ### Tearoffs @@ -306,7 +333,7 @@ extension Foo on Bar { This assignment does a tear-off of the `baz` method. In this case it even does generic specialization, so it creates a function value of type `int Function(int)` which, when called with argument `x`, works just as `Foo(b).baz(x)`, whether or not`Foo` is in scope at the point where the function is called. The torn off function closes over both the extension type and the receiver, and over any type arguments that it is implicitly instantiated with. -An explicitly overridden extension method, like `Foo(b).baz` also works as a tear-off. +An explicitly overridden extension method access, like `Foo(b).baz`, also works as a tear-off. There is still no way to tear off getters, setters or operators. If we ever introduce such a feature, it should work for extension methods too. From d77040e3dd0e87076c0f0f595945cfacfba09daa Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Wed, 12 Jun 2019 12:55:18 +0200 Subject: [PATCH 11/17] Make it explicit when an extension is part of a library export scope. --- .../design-document.md | 68 ++----------------- 1 file changed, 4 insertions(+), 64 deletions(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index 9b59aafc2e..5b55b37374 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -61,9 +61,11 @@ The *type* can be any valid Dart type, including a single type variable. It can The member declarations can be any non-abstract static or instance member declaration except for instance variables and constructors. Abstract members are not allowed since the extension declaration does not introduce an interface, and constructors are not allowed because the extension declaration doesn't introduce any type that can be constructed. Instance variables are not allowed because there won't be any memory allocation per instance that the extension applies to. We could implement instance variables using an `Expando`, but it would necessarily be nullable, so it would still not be an actual instance variable. -### Omitting Names For Local Extensions +An extension declaration with a non-private name is included in the library's export scope, and a privately named extension is not. It is a compile-time error to export two declarations, including extensions, with the same name, whether they come from declarations in the library itself or from export declarations (with the usual exception when all but one declaration come from platform libraries). Extension *members* with private names are simply inaccessible in other libraries. -If an extension declaration is only used locally in a library, there is no need to worry about naming conflicts or overrides. In that case, then name identifier can be omitted (hence the `?` in the grammar above). +### Omitting Names For Private Extensions + +If an extension declaration is only used locally in a library, there might be no need to worry about naming conflicts or overrides. In that case, then name identifier can be omitted (hence the `?` in the grammar above). Example: @@ -454,68 +456,6 @@ The disadvantage is that if you want to introduce related functionality that is If we allow extension static declarations like these, we can also allow extension constructors. -### Explicitly Specify Related Declarations - -The conflict resolution described above is heuristic. It chooses a winner among potential extensions based entirely on the `on` type. - -If the applicable extensions are deliberately written to co-exist, with some extensions using more efficient algorithms on more precisely know receivers, then that conflict resolution algorithm gives the extension authors a clean and, hopefully, understandable way to control which extension is chosen in which situation. A more precise algorithm will be chosen over a less precise one (say, an operation on `List` over one on`Iterable`). - -However, if the applicable extensions are unrelated, written by authors which are oblivious to the other extensions, then the choice risks being arbitrary. A small change, say from `extension Foo on num` to `extension Foo on T`, which will not change where the extension applies, will change the specificity ordering. That makes the conflict resolution heuristic *fragile*. - -There is no way for an author to express the intent that one extension is a specialization of another, and that another one isn't. The only author intent available is in which extensions are imported in the library which triggers the extension member. However, anyone can add an extension to an existing library, or change the specificity of an extension (generalizing an extension with `on` type of `int` to work on all `num`s), which should be non-breaking changes, but which affects conflict resolution. - -Alternatively, we can allow extensions to declare that they are *related*, and only allow conflicts between related extensions, which are presumably aware of each other. Then a conflict between any two unrelated extensions is a compile-time error and will require an explicit override, which will also be stable against future changes. - -The syntax would be: - -```dart -extension Foo on Iterable { twizzle() {...} fromp() { ... }} -extension Bar extends Foo on List { twizzle() { ... } } -``` - -If both of these extensions apply, then pick the one that extends the other. If there are more related extensions which apply, then pick one which transitively extends all the others. - -It should probably be possible to declare that an extension `extends` more than one other extension. That allows an extension hierarchy which matches a diamond type pattern. If you can only extend a single extension, only tree-shaped type hierarchies can be matched by the extensions. - -If there isn't exactly one among applicable extensions which extends all the rest, the conflict is a compile-time error. - -This approach allows related extensions to declare functionality on a number of types, without accidentally allowing a conflict with an unrelated extension. There is no heuristic in the conflict resolution, the choice is made entirely based on existing information added explicitly to make that choice. At least one author has made the explicit choice of declaring that if all the currently applicable extensions are available, their extension should be used. - -This should be sufficient to avoid arbitrary extensions from applying. - -On top of that, we *may* want to require that the extending extension is defined `on` a subtype of its super-extension. In the above, the `on` type of `Bar` is `List`, which is a subtype of the `on` type of `Foo`, which is `Iterable`. This is not strictly required, and an extending extension could also be allowed to be declared on a supertype of the extended extension (an unrelated type would mean that the `extends` is likely unnecessary). The only reason to make the restriction is that this feature is intended for *specialization*, not wholesale replacement. Even if we do this, the subtype does not have to be a proper subtype, so it's still possible to completely shadow the extended extension. - -Also, we may, and perhaps should, require that the declared extension methods in the extending extension must be valid overrides of the same-named super-extension's extension methods, as if it was a subclass relationship. This will ensure that a user who only knows about the super-extension will still get a consistent signature when they instead hit the extending extension. - -If we do require the extending extension's `on` type to be a subtype of the extended extension's `on` type, then we can also let the extending extension *inherit* any member that it doesn't specialize. This will not work if there is more than one super-extension declaring the same member. It's unclear whether this has any advantage. - -### Explicitly Specify Related Declarations 2 - -Alternative syntax: Allow extensions with the same *name* to be related: - -```dart -extension Foo on Iterable { twizzle() { ... } } -extension Foo on List { twizzle() { ... } } -``` - -Whenever an extension is declared with the same name as another extension in scope, it is considered as being *related*, which is an equivalence relation. The declaration extends the extension rather than conflicting with it. - -Only extension resolution conflicts between related extensions are allowed and resolved, any other conflict between applicable extensions is a compile-time error. We then still need to use an ordering relation on the `on` type to figure out which one is more specific in a particular case, and it can fail if there isn't one most specific applicable extension. - -An explicit override like `Foo(something).twizzle()` would still have to pick the most specific applicable extension. There is no way to hide one part of an extension "cluster", and no way to override with a specific extension declaration since they all have the same name. If the extensions do not have the same number of type parameters, an explicit instantiated override like `Foo(something)` won't apply to all of them, which may be confusing. - -Maybe even allow combined declarations when the extensions do have the same type parameters: - -```dart -extension Foo - on Iterable { twizzle() { ... } } - on List { twizzle() { ... } } -``` - -This shows that it really is a single thing being declared, even if we allow multiple declarations with the same name. (We can also choose not to allow multiple declarations, and require all related extensions to be declared in a single declaration with multiple `on` clauses like above). - -This approach works *badly* with static members. - ### Aliasing If we have two different extensions with the same name, they can't both be in scope, even if they don't apply to the same types. At least one of them must be delegated to a prefixed import scope, and if so, it doesn't *work* as an extension method any more. From 5970bd3ce4459c2500943cfff68dc3fc21d13e4c Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Wed, 12 Jun 2019 14:01:05 +0200 Subject: [PATCH 12/17] Add example of type parameter inference. --- .../design-document.md | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index 5b55b37374..c6d97d5c8f 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -149,6 +149,15 @@ List times = ...; ... MyList(times).quickSort() ... // Infer type argument of MyList here. ``` +or for: + +```dart +void Function() MyList$quickSort>(List $this) => + () { ... } +... + MyList$quickSort(times)(); +``` + This is inference based on static types only. The inferred type argument becomes the value of `T` for the function invocation that follows. Notice that the context type of the invocation does not affect whether the extension applies, and neither the context type nor the method invocation affects the type inference, but if the extension method itself is generic, the context type may affect the member invocation. If the inference fails, or if the synthetic constructor invocation (`MyList(times)` in the above example) would not be statically valid for any other reason, then the extension does not apply. If an extension does not have an instance member with the base name `quickSort`, it does not apply. @@ -159,6 +168,71 @@ If the member is itself generic and has no type parameters supplied, normal stat It is *as if* the invocation `times.quickSort` was converted to `MyList(times).quickSort()`. The difference is that there is never an actual `MyList` object, and the `this` object inside `quickSort` is just the `times` list itself (although the extension methods apply to that code too). +### Generic Parameter Inference + +If both the extension and the method is generic, then inference must infer the extension type parameters first, to figure out whether the extension applies, and only then start inferring method type parameters. As mentioned above, the inference is similar to other cases of chained inference. + +Example: + +```dart +extension SuperList on List { + R foldRight(R base, R combine(T element, R accumulator)) { + for (int i = this.length - 1; i >= 0; i--) { + base = combine(this[i], base); + } + return base; + } +} +... + List strings = ...; + int count(String string, int length) => length + string.length; + ... + var length = strings.foldRight(0, count); +``` + +Here the inference occurs just as if the extension had been declared like: + +```dart +class SuperList { + final List $this; + SuperList(this.$this); + R foldRight(R base, R Function(T, R) combine) { ... } +} +``` + +and it was invoked as `SuperList(strings).foldRight(0, count)`. + +Or, alternatively, like the extension method had been declared like: + +```dart +R Function(R, R Function(T, R)) SuperList$foldRight(List $this) => + (R base, R Function(T, R) combine) { ... }; +``` + +and it was invoked as `SuperList$foldRight(strings)(0, count)`. + +In either case, the invocation of the `foldRight` method does not contribute to the inference of `T` at all, but after `T` has been inferred. + +The extension type parameter can also occur as a parameter type for the method. + +Example: + +```dart +extension TypedEquals { + bool equals(T value) => this == value; +} +``` + +Using such an extension as: + +```dart +Object o = ...; +String s = ...; +print(s.equals(o)); // Compile-time type error. +``` + +will fail. While we could make it work by inferring `T` as `Object`, we don't. We infer `T` *only* based on the receiver type, and therefore `T` is `String`, and `o` is not a valid argument (at least not when we remove implicit downcasts). + ### Extension Conflict Resolution If more than one extension applies to a specific member invocation, then we resort to a heuristic to choose one of the extensions to apply. If exactly one of them is "more specific" than all the others, that one is chosen. Otherwise it is a compile-time error. From cdf390295d6e889fd62b1232416d998c3d1a26df Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Wed, 12 Jun 2019 18:08:10 +0200 Subject: [PATCH 13/17] Update resolution to prefer non-platform library declarations over platform library declarations. Also state the a trailing `?` does not affects specificity pre-NNBD. Since all types are effectively nullable, the `?` merely describes run-time behavior. --- .../static-extension-methods/design-document.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index c6d97d5c8f..ead7697c63 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -237,14 +237,19 @@ will fail. While we could make it work by inferring `T` as `Object`, we don't. W If more than one extension applies to a specific member invocation, then we resort to a heuristic to choose one of the extensions to apply. If exactly one of them is "more specific" than all the others, that one is chosen. Otherwise it is a compile-time error. -An extension with `on` type clause *T*1 is more specific than another extension with `on` type clause *T*2 iff the instantiated type (the type after applying type inference from the receiver) of *T*1 is a subtype of the instantiated type of *T*2 and either: +An extension with `on` type clause *T*1 is more specific than another extension with `on` type clause *T*2 iff -- not vice versa, or -- the instantiate-to-bounds type of *T*1 is a subtype of the instantiate-to-bounds type of *T*2 and not vice versa. +1. The latter extension is declared in a platform library, and the former extension is not, or +2. they are both declared in platform libraries or both declared in non-platform libraries, and +3. the instantiated type (the type after applying type inference from the receiver) of *T*1 is a subtype of the instantiated type of *T*2 and either +4. not vice versa, or +5. the instantiate-to-bounds type of *T*1 is a subtype of the instantiate-to-bounds type of *T*2 and not vice versa. -This definition is designed to ensure that the extension chosen is the one that has the most precise type information available. +This definition is designed to ensure that the extension chosen is the one that has the most precise type information available, while ensuring that a platform library provided extension never conflicts with a user provided extension. We avoid this because it allows adding extensions to platform libraries without breaking existing code when the platform is upgraded. -If an extension's `on` type has a trailing `?`, say `T?`, then we treat it just as we would for the corresponding nullable type with NNBD types, which are described in a separate design document. In short, it means treating the type `T?`, for subtyping purposes, as a union type of `T` with `Null` (a least supertype of `T` and `Null`). +If an extension's `on` type has a trailing `?`, say `on String?`, then we do not use that for specificity pre-NNBD. Since all receivers are potentially nullable pre-NNBD, it does not provide any useful signal. + +Post-NNBD, any trailing `?` is part of the `on` type, and a nullable type like `int?` is a proper supertype of, and therefore less specific than, `int`. That is, the specificity of an extension wrt. an application depends of the type it is used at, and how specific the extension is itself (what its implementation can assume about the type). From c25227c7e1fb72b0076cf5d39c4a65dab2f77a47 Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Thu, 13 Jun 2019 16:24:05 +0200 Subject: [PATCH 14/17] Mention `call` methods. Elaborate on instantiated tear-offs. --- .../design-document.md | 59 ++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index ead7697c63..59824ce4d7 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -59,7 +59,7 @@ Such a declaration introduces its *name* (the identifier) into the surrounding s The *type* can be any valid Dart type, including a single type variable. It can refer to the type parameters of the extension. It can be followed by `?` which means that it allows `null` values. When Dart gets non-nullable types by default (NNBD), this `?` syntax is removed and subsumed by nullable types like `int?` being allowed in the `` position. -The member declarations can be any non-abstract static or instance member declaration except for instance variables and constructors. Abstract members are not allowed since the extension declaration does not introduce an interface, and constructors are not allowed because the extension declaration doesn't introduce any type that can be constructed. Instance variables are not allowed because there won't be any memory allocation per instance that the extension applies to. We could implement instance variables using an `Expando`, but it would necessarily be nullable, so it would still not be an actual instance variable. +The member declarations can be any non-abstract static or instance member declaration except for instance variables and constructors. Instance member declaration parameters must not be marked `covariant`. Abstract members are not allowed since the extension declaration does not introduce an interface, and constructors are not allowed because the extension declaration doesn't introduce any type that can be constructed. Instance variables are not allowed because there won't be any memory allocation per instance that the extension applies to. We could implement instance variables using an `Expando`, but it would necessarily be nullable, so it would still not be an actual instance variable. An extension declaration with a non-private name is included in the library's export scope, and a privately named extension is not. It is a compile-time error to export two declarations, including extensions, with the same name, whether they come from declarations in the library itself or from export declarations (with the usual exception when all but one declaration come from platform libraries). Extension *members* with private names are simply inaccessible in other libraries. @@ -118,7 +118,7 @@ The declaration introduces an extension. The extension's `on` type defines which For any member access, `x.foo`, `x.bar()`, `x.baz = 42`, `x(42)`, `x[0] = 1` or `x + y`, including null-aware and cascade accesses which effectively desugar to one of those direct accesses, and including implicit member accesses on `this`, the language first checks whether the static type of `x` has a member with the same base name as the operation. That is, if it has a corresponding instance member, respectively, a `foo` method or getter or a `foo=` setter. a `bar` member or `bar=` setter, a `baz` member or `baz=` setter, a `call` method, a `[]=` operator or a `+` operator. If so, then the operation is unaffected by extensions. *This check does not care whether the invocation is otherwise correct, based on number or type of the arguments, it only checks whether there is a member at all.* -(The types `dynamic` and `Never` are considered as having all members, the type `void` is always a compile-time error when used in a receiver position, so none of these can ever be affected by static extension methods. Methods declared on `Object` are available on all types and therefore cannot be affected by extensions). +(The types `dynamic` and `Never` are considered as having all members, the type `void` is always a compile-time error when used in a receiver position, so none of these can ever be affected by static extension methods. The type `Function` and all function types are considered as having a `call` member on top of any members inherited from `Object`. Methods declared on `Object` are available on all types and can therefore never be affected by extensions). If there is no such member, the operation is currently a compile-time error. In that case, all extensions in scope are checked for whether they apply. An extension applies to a member access if the static type of the receiver is a subtype of the `on` type of the extension *and* the extension has an instance member with the same base name as the operation. @@ -380,6 +380,8 @@ The unqualified `length` of `isEven` is not defined in the current lexical scope The unqualified `isEven` of `isOdd` resolves lexically to the `isEvent` getter above it, so it is equivalent to `MyUnaryNumber(this).isEven`, even if there are other extensions in scope which define an `isEven` on `List`. +An unqualified identifier `id` which is not declared in the lexical scope at all, is considered equivalent to `this.id` as usual. It is subject to extension if `id` is not declared by the static type of `this`. + Even though you can access `this`, you cannot use `super` inside an extension method. ### Member Conflict Resolution @@ -401,7 +403,7 @@ An unqualified identifier in the extension can refer to any extension member dec ### Tearoffs -A static extension method can be torn off like any other instance method. +A static extension method can be torn off like an instance method. ```dart extension Foo on Bar { @@ -412,12 +414,55 @@ extension Foo on Bar { int Function(int) func = b.baz; ``` -This assignment does a tear-off of the `baz` method. In this case it even does generic specialization, so it creates a function value of type `int Function(int)` which, when called with argument `x`, works just as `Foo(b).baz(x)`, whether or not`Foo` is in scope at the point where the function is called. The torn off function closes over both the extension type and the receiver, and over any type arguments that it is implicitly instantiated with. +This assignment does a tear-off of the `baz` method. In this case it even does generic specialization, so it creates a function value of type `int Function(int)` which, when called with argument `x`, works just as `Foo(b).baz(x)`, whether or not`Foo` is in scope at the point where the function is called. The torn off function closes over both the extension method, the receiver, and any type arguments to the extension, and if the tear-off is an instantiating tear-off of a generic method, also over the type arguments that it is implicitly instantiated with. The tear-off effectively creates a curried function from the extension: + +```dart +int Function(int) func = (int x) => Foo(b).baz(x); +``` + +(It is yet undecided whether two tear-offs of the same extension function with the same receiver will be equal in some cases or never). An explicitly overridden extension method access, like `Foo(b).baz`, also works as a tear-off. There is still no way to tear off getters, setters or operators. If we ever introduce such a feature, it should work for extension methods too. +### The `call` Member + +An instance method named `call` is implicitly callable on the object, and implicitly torn off when assigning the instance to a function type. + +The text above suggests that an extension method named `call` can also be called implicitly. This is still being discussed. + +If it is decided to allow this feature, then the following should work: + +```dart +extension Tricky on int { + Iterable call(int to) => + Iterable.generate(to - this + 1, (i) => i + this); +} +... + for (var i in 1(10)) { + print(i); // prints 1, 2, 3, 4, 5, 6, 7, 8, 9, 10. + } +``` + +This looks somewhat surprising, but not much more surprising that an extension `operator[]` would: `for (var i in 1[10])...`. + +A second question is whether this would also work with implicit `call` method tear-off: + +```dart +Iterable Function(int) from2 = 2; +``` + +This code would find, during type inference, that `2` is not a function. It would then find that `int` does not have a `call` method. Without the extension, inference would fail there. If we allow implicit tear-off of an extension `call` method, then the next step would be to check if `int` has a `call` extension member, which it does with the above declaration. Then, if the extension member is a method, it would treat the code like the equivalent: + +```dart +Iterable Function(int) from2 = 2.call; +``` + +This implicit conversion may come at a readability cost. A type like `int` is well known as being non-callable, and because of the implicit `.call` access, there is no visible syntax at the tear-off point which can inform the reader what is going on. + +As such, allowing `call` as a static extension method to make objects callable, might be confusing. The solution can be to disallow it (fail if an extension method is named `call`), make it not work (`2(2)` would not consider the `call` extension method as applying), or allow it with a caution to users about using the functionality judiciously. + ## Summary - Extensions are declared using the syntax: @@ -440,7 +485,7 @@ There is still no way to tear off getters, setters or operators. If we ever intr - An extension applies to such a member invocation if - - the extension name is visible in the lexical scope, + - the extension is declared or imported in the lexical scope, - the extension declares an instance member with the same base name, and - the `on` type (after type inference) of the extension is a super-type of the static type of the receiver. @@ -456,7 +501,7 @@ There is still no way to tear off getters, setters or operators. If we ever intr that was invoked as `Foo(receiver).baz(args)`. The binding of `T` and `S` found here is the same binding used by the extension. If the constructor invocation would be a compile-time error, the extension does not apply. -- One extension is more specific than another if the instantiated `on` type of the former is a proper subtype of the instantiated `on` type of the latter, or if the two instantiated types are equivalent and the instantiate-to-bounds `on` type of the former is a proper subtype of the one on the latter. An `on` type `T?`, with a trailing `?` works like a NNBD nullable type. +- One extension is more specific than another if the former is a non-platform extension and the latter is a platform extension, or if the instantiated `on` type of the former is a proper subtype of the instantiated `on` type of the latter, or if the two instantiated types are equivalent and the instantiate-to-bounds `on` type of the former is a proper subtype of the one on the latter. - If there is no single most-specific extension which applies to a member invocation, then it is a compile-time error. (This includes the case with no applicable extensions, which is just the current behavior). @@ -466,7 +511,7 @@ There is still no way to tear off getters, setters or operators. If we ever intr - The override can also be used for extensions imported with a prefix (which are not otherwise in scope): `prefix.ExtensionName(object).method(args)`. -- An invocation of an extension method throws if the receiver is `null` unless the `on` type has a trailing `?`. With NNBD types, the invocation throws if the receiver is `null` and the instantiated `on` type of the selected extension does not accept `null`. (In most cases, this case can be excluded statically, but not for unsafely nullable types like `int*`). +- An invocation of an extension method throws if the receiver is `null` unless the `on` type has a trailing `?` or is `Null` or a top type. With NNBD types, the invocation throws if the receiver is `null` and the instantiated `on` type of the selected extension does not accept `null`. (In most cases, this case can be excluded statically, but not for unsafely nullable types like `int*`). - Otherwise an invocation of an extension method runs the instance method with `this` bound to the receiver and with type variables bound to the types found by type inference (or written explicitly for an override invocation). The static type of `this` is the `on` type of the extension. From 782ca3a90f65c8bb8c85b60f98d1b067d4f3d29a Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Mon, 17 Jun 2019 14:16:36 +0200 Subject: [PATCH 15/17] add explicit **OPEN ISSUE:** markers. --- .../static-extension-methods/design-document.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index 59824ce4d7..9c54ee5afd 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -420,7 +420,7 @@ This assignment does a tear-off of the `baz` method. In this case it even does g int Function(int) func = (int x) => Foo(b).baz(x); ``` -(It is yet undecided whether two tear-offs of the same extension function with the same receiver will be equal in some cases or never). +**OPEN ISSUE:** It is yet undecided whether two tear-offs of the same extension function with the same receiver will be equal in some cases or never. An explicitly overridden extension method access, like `Foo(b).baz`, also works as a tear-off. @@ -430,7 +430,7 @@ There is still no way to tear off getters, setters or operators. If we ever intr An instance method named `call` is implicitly callable on the object, and implicitly torn off when assigning the instance to a function type. -The text above suggests that an extension method named `call` can also be called implicitly. This is still being discussed. +**OPEN ISSUE:** The text above suggests that an extension method named `call` can also be called implicitly. This is still being discussed. If it is decided to allow this feature, then the following should work: From d7b11fc0e2ce70eaffb7b8a3a45da9ab7f5b4a5e Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Mon, 17 Jun 2019 16:14:20 +0200 Subject: [PATCH 16/17] Elaborate on the behavior of `.call` invocations. --- .../static-extension-methods/design-document.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index 9c54ee5afd..760b6dd2fd 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -447,13 +447,15 @@ extension Tricky on int { This looks somewhat surprising, but not much more surprising that an extension `operator[]` would: `for (var i in 1[10])...`. +Any expression of the form `e1(args)` or `e1(args)` where the static type of `e1` is not a function type, an interface type declaring a `call` method, or `dynamic,` will currently be a compile-time error. We would keep it a compile-time error if the interface type defines a `call` *getter*. Otherwise we would check for extensions declaring a `call` member and applying to the static type of `e1`. I any such exists, and the declared member is a method, then that method is invoked. Otherwise it is still a compile-time error. + A second question is whether this would also work with implicit `call` method tear-off: ```dart Iterable Function(int) from2 = 2; ``` -This code would find, during type inference, that `2` is not a function. It would then find that `int` does not have a `call` method. Without the extension, inference would fail there. If we allow implicit tear-off of an extension `call` method, then the next step would be to check if `int` has a `call` extension member, which it does with the above declaration. Then, if the extension member is a method, it would treat the code like the equivalent: +This code would find, during type inference, that `2` is not a function. It would then find that `int` does not have a `call` member. Without the extension, inference would fail there. If we allow implicit tear-off of an extension `call` method, then the next step would be to check if `int` has a `call` extension member, which it does with the above declaration. Then, if the extension member is a method, it would treat the code like the equivalent: ```dart Iterable Function(int) from2 = 2.call; @@ -596,12 +598,14 @@ If we do this, we should be *consistent* with other type aliases, which means th ```drt typedef MyCleverList = prefix.MyList; // bad! + ``` would make `MyCleverList` an alias for `prefix.MyList`, which would still apply to `List`, but the type variable of `MyList` will always be `dynamic`. Similarly, we can put more bounds on the type variable: ```dart typedef MyWidgetList = prefix.MyList; + ``` Here the extension will only apply if it matches `Widget` *and* would otherwise match `MyList` (but `T` needs to be a valid type argument to `MyList`, which means that it must satisfy all bounds of `MyList` as well, otherwise the typedef is rejected). @@ -610,5 +614,6 @@ The use of `typedef` for something which is not a type may be too confusing. Ano ```dart extension MyWidgetList = prefix.MyList; + ``` From 3156988b6d2006bb9d622f23083541124c194f41 Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Tue, 18 Jun 2019 09:57:51 +0200 Subject: [PATCH 17/17] State decision for `call` extension method behavior. --- .../design-document.md | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/accepted/future-releases/static-extension-methods/design-document.md b/accepted/future-releases/static-extension-methods/design-document.md index 760b6dd2fd..7856f488ba 100644 --- a/accepted/future-releases/static-extension-methods/design-document.md +++ b/accepted/future-releases/static-extension-methods/design-document.md @@ -430,9 +430,7 @@ There is still no way to tear off getters, setters or operators. If we ever intr An instance method named `call` is implicitly callable on the object, and implicitly torn off when assigning the instance to a function type. -**OPEN ISSUE:** The text above suggests that an extension method named `call` can also be called implicitly. This is still being discussed. - -If it is decided to allow this feature, then the following should work: +As the initial examples suggest, an extension method named `call` can also be called implicitly. The following should work: ```dart extension Tricky on int { @@ -445,9 +443,9 @@ extension Tricky on int { } ``` -This looks somewhat surprising, but not much more surprising that an extension `operator[]` would: `for (var i in 1[10])...`. +This looks somewhat surprising, but not much more surprising that an extension `operator[]` would: `for (var i in 1[10])...`. We will expect users to use this power responsibly. -Any expression of the form `e1(args)` or `e1(args)` where the static type of `e1` is not a function type, an interface type declaring a `call` method, or `dynamic,` will currently be a compile-time error. We would keep it a compile-time error if the interface type defines a `call` *getter*. Otherwise we would check for extensions declaring a `call` member and applying to the static type of `e1`. I any such exists, and the declared member is a method, then that method is invoked. Otherwise it is still a compile-time error. +In detail: Any expression of the form `e1(args)` or `e1(args)` where `e1` does not denote a method, and where the static type of `e1` is not a function type, an interface type declaring a `call` method, or `dynamic,` will currently be a compile-time error. If the static type of `e1` is an interface type declaring a `call` *getter*, then this stays a compile-time error. Otherwise we check for extensions applying to the static type of `e1` and declaring a `call` member. I one such most specific extension exists, and it declares a `call` extension method, then the expression is equivalent to `e1.call(args)` or `e1.call(args)`. Otherwise it is still a compile-time error. A second question is whether this would also work with implicit `call` method tear-off: @@ -455,15 +453,13 @@ A second question is whether this would also work with implicit `call` method te Iterable Function(int) from2 = 2; ``` -This code would find, during type inference, that `2` is not a function. It would then find that `int` does not have a `call` member. Without the extension, inference would fail there. If we allow implicit tear-off of an extension `call` method, then the next step would be to check if `int` has a `call` extension member, which it does with the above declaration. Then, if the extension member is a method, it would treat the code like the equivalent: +This code will find, during type inference, that `2` is not a function. It will then find that the interface type `int` does not have a `call` method, and inference will fail to make the program valid. -```dart -Iterable Function(int) from2 = 2.call; -``` +We could allow an applicable `call` extension method to be coerced instead, as an implicit tear-off. We will not do so. -This implicit conversion may come at a readability cost. A type like `int` is well known as being non-callable, and because of the implicit `.call` access, there is no visible syntax at the tear-off point which can inform the reader what is going on. +That is: We do *not* allow implicit tear-off of an extension `call` method in a function typed context. -As such, allowing `call` as a static extension method to make objects callable, might be confusing. The solution can be to disallow it (fail if an extension method is named `call`), make it not work (`2(2)` would not consider the `call` extension method as applying), or allow it with a caution to users about using the functionality judiciously. +This implicit conversion would come at a readability cost. A type like `int` is well known as being non-callable, and an implicit `.call` tear-off would have no visible syntax at the tear-off point to inform the reader what is going on. For implicit `call` invocations, the *arguments* are visible to a reader, but for implicit coercion to a function, there is no visible syntax at all. ## Summary @@ -598,14 +594,12 @@ If we do this, we should be *consistent* with other type aliases, which means th ```drt typedef MyCleverList = prefix.MyList; // bad! - ``` would make `MyCleverList` an alias for `prefix.MyList`, which would still apply to `List`, but the type variable of `MyList` will always be `dynamic`. Similarly, we can put more bounds on the type variable: ```dart typedef MyWidgetList = prefix.MyList; - ``` Here the extension will only apply if it matches `Widget` *and* would otherwise match `MyList` (but `T` needs to be a valid type argument to `MyList`, which means that it must satisfy all bounds of `MyList` as well, otherwise the typedef is rejected). @@ -614,6 +608,5 @@ The use of `typedef` for something which is not a type may be too confusing. Ano ```dart extension MyWidgetList = prefix.MyList; - ```