dart-lang / dart-lang/language

Implicit constructors in receiver/wrapper classes

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

Description

In response to #107 and #40, and as a foundation for solutions similar to #41 and #42, this issue proposes _implicit constructors_ in a class which is either a _receiver class_ or a _wrapper class_. The modifier `receiver` respectively `wrapper` on the class has just one effect: It controls the situations where an implicit constructor can be invoked.

An implicit constructor must be a generative constructor. It only differs from other generative constructors by having the modifier `implicit`.

The difference between implicit constructors and other constructors only arises at locations where they are invoked: Constructors must in general be denoted explicitly (e.g., `C()` or `C.name()` may create a new instance of the class `C`, and the syntax explicitly allows us to look up the constructor named `C` respectively `C.name` and see that this will create a new object). Implicit constructors can also be invoked explicitly.

However, an implicit constructor invocation can also arise because a certain situation exists for an expression `e`, and that expression is then transformed into an expression `e2` which differs from `e` by invoking an implicit constructor with `e` as an argument. For instance `e.foo()` might be transformed into `C.name(e).foo()` where `C.name` is an implicit constructor.

The experience from Scala suggests that it is prudent to be cautious when introducing a mechanism that is capable of implicitly transforming objects of one type into objects of another type (see, for instance, [this comment](https://github.com/dart-lang/language/issues/257#issuecomment-474809524)). With that in mind, this proposal only allows for creation of new objects by wrapping an existing object in a new one, in the sense that the existing object is passed as a constructor argument when the new one is created (and it would be surprising if it were ever useful to ignore that argument, so it's probably going to get "wrapped" in the new object in some sense).

However, the basic idea is similar: During static analysis it may be the case that an expression `e` of type `T` is used in some context where an instance of `T` cannot be used, and `e` may then be implicitly replaced by an instance creation `C(e)` which will work in that context.

One such situation is simply when `e` occurs in a context where a type `S` is expected; this situation is handled with a `wrapper` class, as described below, and in this situation `C(e)` is a subtype of `S`. The notion of a `wrapper` class can be considered as a foundation for (and generalization of) the notion of static extension types.

Another situation is when `e` is used for a member access, like `e.m(...)`, but the static type `T` of `e` has no member named `m`. In that situation we may again replace this expression by `C(e).m(...)`, where `C` _does_ have a member named `m`. This can occur when the class `C` is a `receiver` class. The notion of receiver classes can be considered to be a foundation for (and generalization of) the notion of static extension methods.

There may be additional invocation criteria of interest, but this proposal starts off focusing on just these two: Wrapper classes and receiver classes.

The two proposals in the following can trivially be combined, this just amounts to allowing both class modifiers in the same program, and potentially on the same class. All rules about such classes will coexist without conflict.

Note that these proposals include the static class proposal (#308), because it is important for a receiver class and for a wrapper class to be able to be static, in order to ensure that extension methods can be invoked with just the cost of a top-level function invocation, and a static extension type can be used on a local variable without allocating a wrapper object for the target.

## Proposal, Shared Part

### Syntax

The grammar is adjusted as follows:

```
::=
'implicit'?
```

The only change is that it is possible to use the modifier `implicit` on a constructor.

### Static Analysis

An implicit constructor is a constructor whose declaration includes the modifier `implicit`.

It is a compile-time error for a constructor to be implicit, unless it is a generative constructor.

It is a compile-time error for a generative constructor to be implicit, except in the cases that are explicitly allowed in the proposals below.

The static analysis of an implicit constructor proceeds identically to the static analysis of the same constructor with no `implicit` modifier.

*An implicit constructor only differs from the same constructor without `implicit` by being invoked implicitly in certain locations in code. This is specified as a source code transformation, and the normal static analysis is then applied to the transformed code.*

### Dynamic Semantics

Implicit constructors have the same dynamic semantics as other constructors.

*Again, call sites may be affected by the source code transformation mentioned above, but the dynamic semantics is unchanged when considering the code after this transformation.*

## Proposal for Wrapper classes

### Syntax

The grammar is adjusted as follows:

```
::= // Modified
'abstract'? 'static'? 'wrapper'? 'class'
( ?)? ?
'{' ( )* '}'
| 'abstract'? 'static'? 'wrapper'? 'class'

```

The only change is that the modifier `wrapper` is allowed on a class declaration.

### Static Analysis

A _wrapper class_ is a class whose declaration includes the modifier `wrapper`.

The static analysis of a wrapper class is identical to the static analysis of other classes.

Expressions in the program are affected by the existence of wrapper classes:

Consider an expression `e` with static type `T` that occurs with context type `S`, and assume that `T` is not assignable to `S` (*without this proposal, that is a compile-time error*).

Assume that the set of wrapper classes in scope is `C1 .. Ck`. Let `c1 .. cm` be the implicit constructors of `C1 .. Ck` accepting one positional argument (*which means that `cj` is of the form `SomeClass` or `SomeClass.someName` for each `j` in `1 .. m`*), and assume that inference on `cj(e)` with context type `S` succeeds and yields type arguments `Ujs` (*that is a list of actual type arguments, and the type list `U1s` may have a different length than the type list `U2s`, etc*), for `j` in `1 .. n`.

*There may be fewer than `m` of these (because inference and/or type checking fails with some constructors), hence we go up to `n` rather than `m`. We assume that the constructors have been ordered such that the failing ones are the ones with the highest numbers.*

If exactly one of `cj` is such that the type of its parameter is a subtype of all of those of `c1 .. cn` then `e` is replaced by `cj(e)`. Otherwise a compile-time error occurs.

*That is, it is an error whenever multiple wrapper classes can be used (or even just multiple constructors from the same wrapper class), but none of them gives the target a "better type" than all the others.*

### Example

```dart
class A {}

wrapper class C1 implements A {
final Iterable target;
implicit C1(this.target);
}

wrapper class C2 implements A {
final List target;
implicit C2(this.target);
}

main() {
List xs = [];
A a1 = xs; // OK, desugars into `A a1 = C2(xs);`.

List ys = [];
A a2 = ys; // Error.
}
```

In the first case we infer `C1(xs)` with context type `A`, and that yields the parameter type `Iterable`; with `C2(xs)` (which is non-generic, so inference is a no-op), the parameter type is `List`. `List <: Iterable` so `C2` wins.

Note that a class `C3` with a constructor `C3(int i)` would be ignored, because it would be an error to pass `xs` as the argument to that constructor. So `C3` is out because the type check failed, and similarly an implicit wrapper constructor could be out because inference failed.

In the second case we infer `C1(ys)`, yielding parameter type `Iterable`, and `C2(ys)` again yields parameter type `List`. But none of those parameter types is most specific, so a compile-time error occurs.

Of course, it is always possible for a developer to write `C1(ys)` or `C2(ys)` explicitly, thus eliminating the error by making that disambiguation decision that the compiler should not.

## Proposal for Receiver Classes

The grammar is adjusted as follows:

```
::= // Modified
'abstract'? 'static'? 'receiver'? 'class'
( ?)? ?
'{' ( )* '}'
| 'abstract'? 'static'? 'receiver'? 'class'

```
The only change is that the modifier `receiver` is allowed on a class declaration.

### Static Analysis

A _receiver class_ is a class whose declaration includes the modifier `receiver`.

The static analysis of a receiver class is identical to the static analysis of other classes.

Expressions in the program are affected by the existence of wrapper classes:

Consider an expression `e` with static type `T` which is subject to a member lookup for a member `m`, which we will indicate as `e.m...` below. (*For instance, it could be `e.m(42)` or `e..m2()..m`), and assume that the interface of `T` does not have a member with the name `m`. (*Without this proposal, that is a compile-time error.*)

Assume that the set of receiver classes declaring a member named `m` in scope is `C1 .. Ck`. Let `c1 .. cm` be the implicit constructors of `C1 .. Ck` accepting one positional argument (*which means that `cj` is of the form `SomeClass` or `SomeClass.someName` for each `j` in `1 .. m`*), and assume that inference on `cj(e).m...` with context type `S` succeeds and yields type arguments `Ujs` (*that is a list of actual type arguments, and the type list `U1s` may have a different length than the type list `U2s`, etc*), for `j` in `1 .. n`.

*There may be fewer than `m` of these (because inference and/or type checking fails with some constructors), hence we go up to `n` rather than `m`. We assume that the constructors have been ordered such that the failing ones are the ones with the highest numbers.*

If exactly one of `cj` is such that the type of its parameter is a subtype of all of those of `c1 .. cn` then `e` is replaced by `cj(e)`. Otherwise a compile-time error occurs.

### Example

```dart
class A {}

receiver class C1 implements A {
final Iterable target;
implicit C1(this.target);
X foo() => target.first;
}

receiver class C2 implements A {
final List target;
implicit C2(this.target);
num foo() => 3.2;
}

main() {
List xs = [];
num n = xs.foo(); // OK, desugars into `num n = C2(xs).foo();`.

List ys = [];
num i = ys.foo(); // Error.
}
```

In the first case we infer `C1(xs).foo()` with context type `num`, and that gives the parameter the type `Iterable`; with `C2(xs)` (which is non-generic, so inference is a no-op), the parameter type is `List`. `List <: Iterable` so `C2` wins.

In the second case we infer `C1(ys)`, yielding parameter type `Iterable`, and `C2(ys)` again yields parameter type `List`. But none of those parameter types is most specific, so a compile-time error occurs.

Note that a receiver class `C3` that does not have a member named `foo` is ignored, and so is a receiver class `C4` that does not have an implicit constructor with one argument whose argument type is such that the type of `xs` or `ys` is assignable to it.

Of course, it is again possible for a developer to disambiguate the situation by writing the invocation of one of the implicit constructors explicitly.

## Static Variants

In the case where a wrapper class or a receiver class is static (cf. #308), the output from the code transformation associated with receiver respectively wrapper classes is such that the implicitly created object does not need to be allocated.

This makes it possible for static extension methods (#41) to be emulated by receiver classes:

```dart
extension E on T {

}

// Desugars to the following:
static receiver class E {
final T this;
E(this.this);

}
```

In this desugaring, we rely on the ability of certain declarations to have the name `this`, which makes it possible to implicitly access members of the value of `this` (just like we can access members of the current instance of the enclosing class in an instance method).

In the case where the receiver class is _not_ declared static, it is allowed for the "receiver object" to carry its own mutable state and to use references to itself in arbitrary ways (that is, all expressions are allowed rather than just the non-leaking ones, cf. #308). Similarly, it is possible for a receiver class to declare any number of constructors, implicit or not, and they can have superinterfaces and use mixins like any other class. These things make receiver classes a non-trivial generalization of static extension methods.

Similarly, it is possible for an extension type (#42) to be emulated by a wrapper class:

```dart
typedef E on T {

}

// Desugars as follows
static wrapper class E {
final T this;
E(this.this);

}
```

At call sites, a wrapped object for a static wrapper class can be accessed through desugared top-level functions taking the wrappee (and any instance variables of the wrapper) as actual arguments:

```dart
main() {
E x = e; // Some expression of type `T`.
x.someMemberOfE();
requireAnActualWrapper(x);
requireAnActualWrapper(x);
}

// Desugars as follows:
main() {
T _x = e;
lazy E x = E(_x);
desugared_E_someMemberOfE(_x, other, instance, variables);
requireAnActualWrapper(x); // The initializer of `x` is now executed.
requireAnActualWrapper(x); // Then: `x` denotes same wrapper each time.
}
```

There may be small discrepancies. For instance, the use of `super` to invoke a method on the target object—the original receiver for a receiver class and the wrapped object for the wrapper class—does not work the same when using a receiver/wrapper class, because that will simply be an invocation of a method from a superclass of that receiver/wrapper class, but `this.foo()` would always call a `foo` method on the original receiver/wrappee (rather than on the enclosing instance of the receiver/wrapper class), and a plain `foo()` would be used to get the one from the enclosing scope (and not from the original receiver/wrappee).

## Discussion

The purpose of considering receiver and wrapper classes as a foundation of extension methods and extension types is to express the desired semantics of such mechanism using the smallest possible increments:

Static classes are used in order to ensure that performance corresponds to the straightforward implementation strategies for extension methods (just desugar them to statically resolved function calls) and extension types (where the "view" provided by the extension type is applied to the target object without changing the representation of that target at all, that is, without having a wrapper: just call static methods that desugar the static wrapper class, and pass the wrappee as well as any "instance variables of the wrapper" as actual arguments to the static functions that are desugared versions of the wrapper class methods).

If we consider the ability to eliminate instances of static classes as a given, then we may think of other properties of these mechanism as if they always have those objects which are created by the code which is the output of the transformations.

So whenever a method is executed on an instance of a receiver class, it's just a completely normal method invocation and it is in principle unimportant that the original receiver is the value of a field in that receiver class instance.

Similarly, if an object is wrapped by an instance of a wrapper class and used in a context which is "leaking" (that is, it is not non-leaking, cf. #308) then we _will_ obtain an actual instance of the wrapper class, and that object will implement the superinterfaces from the declaration of the wrapper class, which means that it is a full-fledged object of the desired type. So in the case where we actually get a wrapper object, it serves as a mechanism which provides the "view" of the extension type on the given wrappee object, and this property will hold in all context, e.g., also for dynamic invocations.

Comparing with #107 (and more general implicit conversion mechanisms), this proposal only addresses the situation where we can write a constructor for the target class (`B` in #107 lingo). For instance, it does not address the situation where we want to transform a `String` into an `int`, because we cannot make `int` a wrapper class.

In relation to #108, this proposal also uses the modifier `implicit` on certain constructors. The obvious difference is that the situations where an implicit constructor can be applied is parceled out into the `receiver` case and the `wrapper` case; I believe that #108 is mainly focused on the latter.

The current proposal for wrapper classes does not support the "inverse conversion": We may implicitly wrap an `A` and get a `B`, but there is no standardized way to obtain the original `A` again from that `B`, say, if the `B` is passed around and an `A` is needed somewhere. Indeed, nobody enforces that the construction of the `B` even uses the argument of type `A` that it receives. It might be useful to have a standardized member in wrapper classes which are used to perform this kind of "unwrapping".

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.