Skip to content

Private to this #3816

Open
Open
@eernstg

Description

@eernstg

Consider a non-covariant member, as described in dart-lang/sdk#57371. That is, roughly, an instance member of a class whose return type has a non-covariant occurrence of a type parameter from the enclosing class/mixin/enum/etc.

For example, in class A<X>... it could be a method or getter whose return type is void Function(X), or a variable whose declared type is List<X> Function(X).

Such members are dangerous, in the sense that any reference to the member will give rise to a run-time type error if the statically known value for said type parameter differs from the run-time value:

class A<X> {
  void Function(X) fun; //     <-- This member is non-covariant.
  A(this.fun);
}

void main() {
  A<num> a = A<int>((i) => print(i.isEven));
  a.fun; // Throws, we don't even have to call it!
}

In this example, the statically known value of X in a is num, but the run-time value is int. Those two types differ, so a.fun throws. We can try to call it with a value which is actually ok for the run-time value of X (like a.fun(10)), but we won't even reach the point where the type correct actual argument is passed to the function, because the expression a.fun will throw before we even get hold of the function object.

I've created a proposal for a lint, dart-lang/sdk#59050, which would flag every non-covariant member.

However, a non-covariant member can be used in a type safe manner with an extra constraint: When the receiver of the access is this, the given type variable is in scope, and type checking can be performed safely:

class A<X> {
  void Function(X) fun;
  A(this.fun);
  void callFun(X x) => fun(x); // This invocation of `fun` is type safe.
}

void main() {
  A<num> a = A<int>((i) => print(i.isEven));
  a.callFun(42); // Accepted at compile time, and succeeds.
}

We can easily enforce that all accesses to a given member must occur with this as the receiver, we just need to specify in the declaration of the given member that it has this constraint.

As a strawman syntax, I'll use this. in the declaration of an instance member in order to indicate that this member is "private to this".

class A<X> {
  void Function(X) this.fun; //     <-- This member is private to `this`.
  A(this.fun);
  void callFun(X x) => fun(x); // OK, receiver of `fun` is `this`.
}

void main() {
  A<num> a = A<int>((i) => print(i.isEven));
  a.fun; // Compile-time error, receiver is not `this`.
  a.callFun(42); // OK.
}

The ability to constrain the allowed set of receivers to this can be useful in other ways, too. For example, this particular notion of privacy could be useful based on software engineering considerations (like maintainability, readability, enforcement of application domain specific constraints, etc.).

Note that being private to this and being private are orthogonal concepts (where the latter is the normal, Dart privacy which is specified via names of the form _...). That is, we can have declarations that are not private at all, that are private to this, that are private (in the normal sense), or that are private and private to this; each of those 4 combinations have their own properties.

One reason to use privacy to this which is technical as well as relevant from a software engineering perspective is that it is much safer to use a private member (in the usual sense) if it is also private to this:

abstract class A {
  void this._foo(); //     <-- This member is private to `this`.
}

class _B implements A {
  void this._foo() {}
  void bar() => _foo();
}

At the invocation of _foo, it is known that this has type _B or a subtype thereof, and it is possible to detect through analysis of the current library whether or not we can know that every subtype of _B has an implementation of _foo.

The name _foo is only accessible in the same library, and this means that we can check every invocation, to see that it occurs in a location where it is guaranteed that this has an implementation of _foo. That's not true if _foo is invoked in the body of A because some other library could create a concrete subtype of A that does not have an implementation of _foo. If that is true then this invocation is safe; if it is not known to be true then we may encounter a 'no such member' failure at run time.

The point is that if an instance member is private to this then it is a lot easier to establish some level of trust in the assumption that said member actually exists at each call site, up to a situation where it is a firm guarantee.

Based on these considerations, I think it may be worthwhile to support the notion of instance members that are private to this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions