Skip to content

Importing name spaces locally #267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
eernstg opened this issue Mar 13, 2019 · 3 comments
Open

Importing name spaces locally #267

eernstg opened this issue Mar 13, 2019 · 3 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@eernstg
Copy link
Member

eernstg commented Mar 13, 2019

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), 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:

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:

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 dart-lang/sdk#30520, where we would use the following (which is assumed to occur in a function body):

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

Grammar

The grammar is adjusted as follows:

<localImport> ::= // New.
    'import' <identifierList> ';'

<nonLabelledStatement> ::= // Adding new alternative at end.
    <block>
  | <localVariableDeclaration>
  ...
  | <expressionStatement>
  | <localImport>

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 <identifierList> 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 <qualified> 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.

@eernstg eernstg changed the title Importing non-library name spaces locally Importing various name spaces locally Mar 14, 2019
@eernstg eernstg changed the title Importing various name spaces locally Importing name spaces locally Mar 14, 2019
@eernstg
Copy link
Member Author

eernstg commented Mar 14, 2019

My proposal does not make it an error, because the following is not an error:

class A {
  int x;
}

String x;

class B extends A {
  // A plain `x` is resolved lexically, that is, it denotes the top-level variable.
  String s = x; // No problem with access to `this`, because we don't; so the type is OK.
  String get foo => x; // Returns the value of the top-level variable, so the type is OK.
}

So we're using exactly the same approach to implicit "member" access with a locally imported name as we are with this in an instance method.

It seems un-Dartish to me to use different rules for this in an instance method and for other names which are explicitly locally imported.

The underlying rationale is that the lexical scope is more accessible to a person who is reading the code than the declarations of features in superinterfaces of the current class, so the lexical scope wins if there is a declaration.

Also, (assuming that we'd use the opposite priority) you could have a superinterface which is maintained by some third party that you do not communicate with frequently, so they could introduce an extra name in one of your superinterfaces, and you might not notice. It might break your code (say, if your class is concrete you might now need to write an implementation of one more method), but if it doesn't directly break your code then it could change the meaning of a plain identifier (like x above), and the resulting program behavior could be wrong in ways that are quite subtle (because you will now use this.x rather than x-from-the-lexical-scope, and this could change all kinds of things).

Another thing to note is that the third party change in a superinterface could break your code such that you have to edit it if the superclass gets the higher priority, and this will not happen when it is the lexical scope that gets the higher priority.

Conversely, with the current choice (lex wins), a third party library that you're importing without a prefix could start exporting one more name, and that could break your code (because that x which used to mean this.x would now refer to that new global name). However, Dart libraries imported without a prefix tend to export very few relevant names for this issue (because they mostly export types, and they are Capitalized, so they won't typically clash with the name of a member which is inherited), so that danger could arguably be considered smaller than the danger associated with the 'this wins' priority.

So this prioritization of the lexical scope was not made by accident.

I don't know how many conflicts we would actually have if we were to make all these situations an error (rather than resolving them in favor of the lexical scope). We could have a lint that checks all identifiers and reports all those that have this kind of ambiguity, but it seems likely that this is such a deep property of Dart that it would be a hugely breaking change.

@eernstg
Copy link
Member Author

eernstg commented Mar 15, 2019

Which "x" wins

That's an error: With multiple local imports offering the same name, there is no error at the imports but every use of the ambiguous name is an error. So you'll have to use a prefix like this.x to make the choice.

We could say that "imported symbol never wins", but that's the rule that we already have! —that is, if you're willing to accept the notion that the treatment of this.id in instance methods (where id is inherited) is just another local import.

@eernstg
Copy link
Member Author

eernstg commented Mar 15, 2019

I chose import because we already use import in Dart to indicate that the name space is being extended (with a prefix, or a complete name space).

I considered allowing local imports of the form import foo as f; to enable abbreviated rather than implicit accesses to a given name space, but I wasn't convinced that it would be very helpful in practice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

1 participant