dart-lang / dart-lang/language

Importing name spaces locally

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

Description

In response to #266, this issue is a proposal to add support for locally _importing global and static name spaces as well as objects_ in a function body. It is an enhancement of the mechanism associated with `this` in instance methods, and the import mechanism of libraries.

The purpose of this issue is (1) to be a reminder about certain features that the language team has discussed, and (2) to put the `this` binding feature in anonymous methods (#260) in perspective. If we wish to be very orthogonal, we could consider the `this` binding in #260 as syntactic sugar for a local import.

The local import feature proposed here is similar to the `open` construct in SML ([see, e.g., p47 of this PDF](http://www.cs.cornell.edu/riccardo/prog-smlnj/notes-011001.pdf)), except that it does not cause an error to import the same name from more than one source, an error arises only if that name is used.

## Introduction

The mechanism proposed here is simply to enable `import` clauses to apply to library prefixes, enumerations, and objects, and to occur in function bodies. The effect of doing this is that the code can be more concise, without polluting the global name space.

In other words, one attractive property of this feature is that it lowers the cost of keeping the name space small and comprehensible at the top level (say, by importing with a prefix more frequently), because it enables each function body to open whatever name spaces it uses frequently.

For example:

```dart
import 'math.dart' as math;

enum PickerSelectionType { select, toggle, deselect, custom }

class Baz {
static int baz1() => 2;
static int baz2() => 3;

bool select(T row) {
switch (selectionType) {
case PickerSelectionType.select: return ...;
case PickerSelectionType.toggle: return ...;
case PickerSelectionType.deselect: return ...;
case PickerSelectionType.custom: return ...;
}
}
}

qux() {
var x = math.pi / Baz.baz1() / Baz.baz2();
var y = math.sin(x) * math.sin(x) + math.cos(x) * math.cos(x);
}

flob() {
String myString = "Helloworld";
print("${myString.substring(0,5)}, ${myString.substring(6,11)}");
}
```

could be expressed using local imports as follows:

```dart
import 'math.dart' as math;

enum PickerSelectionType { select, toggle, deselect, custom }

class Baz {
static int baz1() => 2;
static int baz2() => 3;

bool select(T row) {
import PickerSelectionType;
switch (selectionType) {
case select: return ...;
case toggle: return ...;
case deselect: return ...;
case custom: return ...;
}
}
}

qux() {
import Baz, math;
var x = pi / baz1() / baz2();
var y = sin(x) * sin(x) + cos(x) * cos(x);
}

flob() {
String myString = "Helloworld";
import myString;
print("${substring(0,5)}, ${substring(6,11)}");
}
```

In all cases, the scope rules would be the same as the ones that we currently use for implicit member access to the "current" instance of an enclosing class: If an identifier reference `id` is not declared in an enclosing scope then it is treated as `this.id`.

Note that this rule is applicable for any identifier reference which is the first part of an expression, e.g., `foo(42)..bar[3] = 14` means `this.foo(42)..bar[3] = 14` if `foo` is not declared in an enclosing scope. Subsequent static analysis may find that `this.foo(42)..bar[3] = 14` is a compile-time error, but it would already have been an error if considered as `foo(42)..bar[3] = 14`.

In other words, a local import will never change the meaning of any expression which would not be an error if the import were removed. This is important, because it allows developers to rely on understanding the meaning of an already-meaningful expression, without ever taking the detour into considerations about implicit usages of `this`, or any other locally imported name space.

One existing SDK issue which would be addressed by this proposal is https://github.com/dart-lang/sdk/issues/30520, where we would use the following (which is assumed to occur in a function body):

```dart
import reflector;
registerFactory(ExampleService, () => new ExampleService())
registerFactory(ExampleService2, () => new ExampleService2());
```

## Grammar

The grammar is adjusted as follows:

```
::= // New.
'import' ';'

::= // Adding new alternative at end.

|
...
|
|
```

## Static Analysis

A local import can occur in the body of a function. It is a compile-time error unless each name in the identifier list of a local import denotes a library prefix, a class, an enumeration, or an object.

Consider an identifier reference `id` which occurs in the body of a function _F_. Assume that `id` is not declared in an enclosing scope.

If _F_ or one of the enclosing functions of _F_ is an instance method, and local imports of the identifiers `id1 .. idk` exist in one of the enclosing lexical scopes before the location of said identifier reference `id`, then consider the terms `this.id`, `id1.id`, .., `idk.id`. It is a compile-time error if zero or more than one of these terms resolves statically to a member of an interface of an object, a static class member, an enumeration value, or a declaration exported by a library; otherwise `id` is treated as the single term which is not an error (that is, `this.id`, or `idj.id` for some `j`).

Otherwise (*if _F_ and each function that encloses _F_ directly or indirectly is not an instance method*) then the same treatment is given to `id`, except that only the terms `id1.id`, .., `idk.id` are considered.

## Dynamic Semantics

The dynamic semantics of this feature is fully determined by the syntactic transformation which is described in the previous section.

## Discussion

The ability to import the name space of an object is a generalization of the treatment of `this` in the bodies of instance methods and constructors, and this may be used to reduce the binding of `this` in an anonymous method (#260) to syntactic sugar for a local import, provided that we also have the ability to bind `this`. That allows us to split the binding of `this` and the import of `this` (known as implicit member access in #260) into two independent features.

The ability to import library prefixes and static class name spaces could be used to make certain function bodies less busy without polluting the global name space; but it would also be possible to allow imports to occur, say, at the beginning of a class.

The ability to have local imports anywhere in a function body could also be restricted (e.g., such that they can only occur before all statements and local declarations). This would make it easier to always spot all local imports at a glance, but could then make it harder to see the relevant import when it is actually used, and it would make the name space more busy before that point.

Note that this mechanism could support chaining. For instance, `import p, C;` could make `x` desugar to `p.C.x`: `x` could desugar to `C.x` because `x` is a static member of the class `C`, and `C` would desugar to `p.C` because the library with prefix `p` declares the class `C`. However, the current wording intentionally does not support this scenario. The technical reason is that the names in the `` of a local import must resolve to a library prefix, class, enumeration, or object without any desugaring steps, the desugaring is only applicable to expressions.

We could allow local imports to contain a `` term like `p.C`, rather than just identifiers, but this would be a non-breaking change that we could perform at any time if it turns out to be needed.

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.