dart-lang / dart-lang/language

Allow variables named `this`

Open
#328 8 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 #266, this issue proposes _user-declared implicit member access_, which is simply the feature which adds the ability for a variable (which could be a formal parameter, or any kind of variable) or getter to have the name `this`.

[Edit, May 3rd 2019: Clarified some errors, added an error when `this` has the type `dynamic`. May 7th 2019: Introducing a targeted `this` and more detailed lookup rules; added type-safe builder example.]

In Dart, the lexical scope is searched first, and any declaration found there is used. For instance, `m(42)` will call the library function named `m` in the body of `m2`, not the instance method `m` which is inherited from `A`:

```dart
m(x) {}

class A {
m(x) {}
m1() {}
}

class B extends A {
m2() => m(42); // Calls top-level function `m`.
m3() => m1(); // Means `this.m1()`, calls inherited method.
}
```

In the case where the lexical scope does not contain a declaration of the requested name, `this.` is prepended, and the resulting expression is used during subsequent static analysis. (So we check the lexical scopes, then `this`, but we may still end up with a compile-time error because the requested name is not found in any of those two ways).

The feature proposed here is to allow declarations of variables and getters named `this`. The existing scope rules are used to obtain the effect that `this` can be an implicit receiver: The enclosing lexical scopes will still be searched first, and member accesses with `this` as an implicit receiver will be taken into account if nothing was found in the lexical scopes, and we still get a compile-time error if none of those two approaches yield a successful lookup.

The rules proposed here specify that, in the body of an instance method, an instance member invocation is chosen whenever an instance member of the given name exists in the interface of the enclosing class (not just when such a member is declared in the lexical scope). This may seem like a breaking change (because it mentions the interface of the class, and the current rules only talk about the lexical scope), but it is in fact backward compatible because the existing rules unconditionally prescribed the addition of `this` as the last catch-all case.

The additional expressive power added by this feature is only the ability to give the name `this` to a larger set of objects, thus allowing for implicit member accesses to them.

Note that multiple declarations of `this` in different lexical scopes can shadow each other, just like other declarations, such that the innermost declaration wins:

```dart
class A {
m() {}
}

class B extends A {
m2 => m(); // Means `this.m()`, calls `A.m`.
m3(C this) => m(42); // Means `this.m(42)`, calls `C.m` on the argument.
}

class C {
m(int i) {}
}
```

This mechanism is likely to be very convenient in a number of situations.

In particular, it is commonly expected that implicit access to an object which is not the "current instance" of an enclosing class is available in extension methods (such as #41 and #177); that can be achieved simply by making the syntactic receiver of the extension method invocation be a parameter named `this` in the function which is the desugared version of the extension method. This means that we can use this general mechanism, and we don't need to invent a whole set of special rules to explain why it is possible to use `this` to access the syntactic receiver of an extension method invocation.

In general, the use of a variable or getter named `this` allows for concise access to the instance members of that object.

On the other hand, excessive usage of declarations named `this` will make it harder to read the source code. In the specification below we will leave it open how to constrain this mechanism, but it would of course be very easy to single out specific usages and make them a compile-time error (e.g., it could be a compile-time error for a top-level variable to have the name `this`).

If we prefer to take a more cautious path, we could make the choice to _only allow_ declarations named `this` in a very small number of specific situations. In all other situations it would just be a compile-time error for a declaration to have the name `this`.

It would then presumably be quite easy (in terms of the specification as well as the implementation) to allow additional kinds of declarations to have the name `this`, whenever we have determined that this generalization is valuable in practice.

## Syntax

The grammar is adjusted as follows in order to support user-declared implicit member access:

```
::= // NEW
| 'this'

::= // MODIFIED
'covariant'?

::= // MODIFIED

| 'covariant'?

::= // MODIFIED
('=' )?

::=
('=' )? (',' )*

::=
|
| 'super'
|
|
|
| '(' ')'
|
|

::= '.' 'this'

::= ? 'get'
```

*Note that it is a syntax error to use the name `this` for a type variable, but other variables can have the name `this`.*

## Static Analysis

It is a compile-time error for a variable or getter with the name `this` to have the type `dynamic`.

*This is true both in the case where the variable has a type annotation, and in the case where the type of the variable is inferred.*

It is a compile-time error for a `` of the form `C.this` to occur, unless it occurs in an instance method of a class named `C`. A `` is considered to be an identifier whose static type is the enclosing class, passing any declared type parameters as actual type arguments. (*For instance, `C.this` has type `C` if `C` is declared as `class C {...}`.*)

The static type of a `` _e_ is the type annotation of the declaration named `this` if any such declaration is in scope; otherwise, the static type of _e_ is the enclosing class, when _e_ occurs in an instance method; otherwise, _e_ is a compile-time error.

In the rules for [unqualified invocations](https://github.com/dart-lang/language/blob/87bd63679a8233776f559d7ab4ed45a49456f5bb/specification/dartLangSpec.tex#L8731), a new 5th bullet point is added ([here](https://github.com/dart-lang/language/blob/87bd63679a8233776f559d7ab4ed45a49456f5bb/specification/dartLangSpec.tex#L8782)), specifying that

```
Otherwise, if $i$ occurs in an instance method body and
the interface of the enclosing class $C$ contains a member named \id,
then $i$ is equivalent to
\code{$C$.\THIS.\id<$A_1, \ldots,\ A_r$>($a_1, \ldots,\ a_n,\ x_{n+1}$: $a_{n+1}, \ldots,\ x_{n+k}$: $a_{n+k}$)}.
```

and the last bullet is adjusted to say

```
Otherwise, if $i$ occurs in an instance method body, or a declaration named \THIS{} is in scope,
$i$ is equivalent to the ordinary method invocation
\code{\THIS{}.\id<$A_1, \ldots,\ A_r$>($a_1, \ldots,\ a_n,\ x_{n+1}$: $a_{n+1}, \ldots,\ x_{n+k}$: $a_{n+k}$)}.
```

Similar adjustments are made for identifier references [here](https://github.com/dart-lang/language/blob/87bd63679a8233776f559d7ab4ed45a49456f5bb/specification/dartLangSpec.tex#L11638).

*For instance, if an expression `e` of the form `m(42)` occurs in the body of an instance method of a class `C` then, if there is no declaration of `m` in the enclosing lexical scopes, `e` is desugared to `this.m(42)`, and subsequent processing is based on the desugared expression. In particular, the static analysis of `e` will check that there is a member named `m` in the static interface of the type of `this`, and that `42` is an appropriate actual argument list for an invocation of `m`. Similarly, the dynamic semantics of `e` follows from the result of desugaring.*

## Dynamic Semantics

When an ordinary method invocation takes place and it invokes an instance method `m` in a class `C` with receiver `o`, the targeted this-expression `C.this` is bound to `o`; if no declaration named `this` is in scope, `this` is bound to `o` as well.

*If a declaration named `this` is in scope then no special bindings for `this` are provided, and the corresponding declared entity is accessed using the ordinary rules for access to a declared entity.*

## Discussion

Kotlin supports the notion of a [function literal with receiver](https://kotlinlang.org/docs/reference/lambdas.html?_ga=2.85660441.1815970936.1556091506-1559271053.1556091506#function-literals-with-receiver) and [function type with receiver](https://kotlinlang.org/docs/reference/lambdas.html?_ga=2.85660441.1815970936.1556091506-1559271053.1556091506#function-types). An example is the following:

```kotlin
val sum = fun Int.(other: Int): Int = this + other
```

This specifies that the receiver type is `Int`, and it allows the call to pass an instance of type `Int` by providing it as the syntactic receiver (such as `4` in `4.sum(2)`), and to access that instance in the body of the function using `this`.

The Kotlin mechanism differs from the proposal of this issue by being more constrained: It bundles together two different mechanisms, and they can't be used separately. The first one is that a certain function argument is passed as a receiver (that is, that the call is of the form `r.f(...)`, but `r` will be passed to `f` as the first actual argument). The second mechanism is that the "receiver argument" is accessed in the body of the function using the name `this`, and it also allows for implicit accesses (such that `m()` in the body can denote the instance method invocation `this.m()`).

The feature proposed in this issue, user-declared implicit member access, is only concerned with the scoping and the implicit member access, that is, the second mechanism mentioned above.

Other mechanisms (like #41, #42, #177, #309) are used to enable the first mechanism, and they in turn rely on this feature. This allows us to specify extension methods, extension types and so on in terms of a common underlying mechanism which allows `this` to take on the appropriate meaning in each case, as opposed to an approach where we would specify a new set of ad-hoc rules for how to understand `this` in the context of each of those constructs.

A major use case for Kotlin's function types and literals with receiver is [type-safe builders](https://kotlinlang.org/docs/reference/type-safe-builders.html). Here is an example from [here](https://kotlinlang.org/docs/reference/lambdas.html?_ga=2.85660441.1815970936.1556091506-1559271053.1556091506#function-literals-with-receiver):

```kotlin
// Kotlin type-safe builder example.

class HTML {
fun body() { ... }
}

fun html(init: HTML.() -> Unit): HTML {
val html = HTML() // create the receiver object
html.init() // pass the receiver object to the lambda
return html
}

// Example expression using the type-safe builder.

html { // lambda with receiver begins here
body() // calling a method on the receiver object
}
```

The point is that the construction of an instance of `HTML` is done in the function `html`, and the `{...}` construct at the end is an argument to `html` which is a function literal, and the body of that function literal gets to use methods on that `HTML` implicitly, because it is a function literal with receiver.

If we choose to give the implicit parameter the name `this` then we could do something similar in Dart:

```dart
// Dart counterpart.

class HTML {
void body() { ... }
}

HTML html(void Function(HTML) init) {
final html = HTML();
init(html);
return html;
}

// Example expression using the `html` builder.

html({
body(); // Calling a method on the receiver object
});
```

It's a bit more noisy because of the parentheses and the semicolons, but we do get the basic structure of a Kotlin style type-safe build.

Note that Dart gives more information locally:

In Kotlin, you'd need to look up the type of the parameter of `html` (possibly in some other file) in order to understand that we may rely on an implicit receiver—for instance, `body()` calls a method on the argument. In Dart, we immediately know that `body()` can be a method call on the argument, because (if we choose the name `this` for the parameter) that's how abbreviated function literals work.

Contributor guide

Open the contributing guide

Research direction

Start by reading the linked specification/dartLangSpec.tex sections on unqualified invocations and identifier references, then review the proposed Syntax, Static Analysis, and Dynamic Semantics rules. There are no tests or implementation entry points named in the issue. Done would require resolving the open constraints and semantics for declarations named `this` and updating the Dart language specification accordingly.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.