Skip to content
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

Private to this #3816

Open
eernstg opened this issue May 17, 2024 · 6 comments
Open

Private to this #3816

eernstg opened this issue May 17, 2024 · 6 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@eernstg
Copy link
Member

eernstg commented May 17, 2024

Consider a non-covariant member, as described in #296. 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/linter#4111, 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.

@eernstg eernstg added the feature Proposed language feature that solves one or more problems label May 17, 2024
@eernstg
Copy link
Member Author

eernstg commented May 17, 2024

Note that #2918 proposes that every private instance member should be made private to this, in the terminology introduced by this issue. I believe that this is too impractical and too breaking, but the arguments made there are of course also relevant as arguments in favor of allowing some private instance members to be private to this.

(Note that non-private instance members can also be private to this, since this proposal defines the two kinds of privacy as orthogonal and independent).

@escamoteur
Copy link

Maybe I misunderstand what you mean by private to this but while discussing the introduction of a protected members in #3825
It was pointed out that adding a private keyword like protected that would make a member private to this` would make a lot of sense. Especially private final properties that get initialized from the constructor could then do

class A{
   A(this.privateMember);

  private final privateMember:
}

without the need to use initializers to assign constructor parameters to _privateMember because this._privateMember is discouraged.

I would even argue that library privacy that we get with _ is not what many people want if they want a private member so in the majority of cases a this private member would be the more secure solution.

@eernstg
Copy link
Member Author

eernstg commented May 23, 2024

Right, a protected member as defined in #3825 will indeed be private to this, because it's a compile-time error to invoke or tear off such a member when the receiver is any other expression than this (explicitly or implicitly).

I'm not convinced that those two concepts need to be tied together that firmly. For instance, it would make sense to me that operator == could rely on some protected properties of the given objects, which means that we'd want to call those protected members on some other object (the formal parameter), as well as on this.

That wouldn't be a problem, though, if the two features are provided separately: We could then choose to make any particular protected member private to this as well, or not, depending on the concrete situation.

About this example:

class A {
  A(this.privateMember);
  private final privateMember:
}

We can do this today:

class A {
  final _member:
  A(this._member);
}

Some people really don't like the idea that _member could occur in the documentation. I think it makes perfect sense: In one quick glance you know that (1) you're initializing an instance variable whose conceptual role is "member", and (2) you won't be able to read or write that particular instance variable later on because it is private.

However, with named parameters we do have a difficulty: It is a compile-time error for a named parameter to have a name that starts with _.

We have a proposal that has substantial support from the language team (I think we just didn't have enough time to get it done, and I think something like that will happen): #2509 (comment).

If that proposal is accepted then we can do this:

class A {
  final _member:
  A({this._member}); // Invoked as `A(member: someValue)`.
}

With these small tools in mind, we shouldn't need to move elephants like privacy around willy-nilly just so we can have a nicer parameter declaration syntax.

library privacy that we get with _ is not what many people want

True. We could consider things like package wide scopes (that is, "private to the set of libraries that are located in the lib of this package"), but no good solution has emerged even though it has been discussed for years.

@escamoteur
Copy link

escamoteur commented May 23, 2024

I think your example of the == operator that might want to access a protected property of other is probably the only case where it could be a problem if protected where private to this.
Which kind of privacy should protected then have? same as _ members?

IMHO we only need library privacy because we have no protected and friend concept. To me library privacy always didn't really express who can access a certain member without looking through the whole library. It's kind of private but easily broken by placing any client into the same library which isn't in the control of the class that declares the member.

Having a private and protected ,modifier that behave like people know from other languages would make this more clear and secure. While we could still use the _members for people needing library privacy.

And having a this private would not only make nicer parameters (which would be a side benefit) but allow to express that something really should be this private.

@eernstg
Copy link
Member Author

eernstg commented May 24, 2024

Which kind of privacy should protected then have? same as _ members?

I think library privacy (_...) should be used to ensure encapsulation. We've had this kind of privacy for many years, and we'll just keep it as it is. (We might allow for enhancements such that a set of libraries can share all their private members, but that's basically the same thing as making each of those libraries a part of a single library that represents the library group.)

Being private to this is a completely different thing. The main technical motivation from my point of view is that this will enable type safe usage which is otherwise not safe:

class A<X> {
  final X Function(X) this.transform;
  X value;
  A(this.transform, X initialValue) : value = initialValue;

  void foo() {
    ...
    value = transform(value); // Type safe!
    ...
  }
}

void main() {
  A<num> a = A<int>(42);
  a.transform(a.value); // Would throw, but is now a compile-time error.
}

The point is that we can safely use expressions involving the type X in the body of the class A that declares the type parameter X, but we cannot use it safely from the outside (as in main).

I think the ability to restrict a given member to be private to this can be relevant in a lot of software engineering situations. This is all about semantic encapsulation ("only this object can mess with that member" ;-), it doesn't help at all to avoid recompilations.

I haven't had an urgent need for a member to be protected. Presumably, it could actually just be private to this, but there may also be cases where several objects collaborate on a given task (operator == is an example of a collaboration involving two objects). This brings up the notion of friend classes as well, because those collaborating objects may well have different types. I don't have much of an opinion about protected because I don't really need it.

@lrhn
Copy link
Member

lrhn commented May 24, 2024

A protected behavior is the only kind of access control I've actually missed in Dart, library privacy is fine for everything else.

Protected comes up when you write libraries with base or skeleton classes that others are expected to build on, to create public facing APIs for some third party.
That's when you want there to be members that the API builder can use to communicate privately with the base class, without having to expose those members in their own API.

There are ways, usually involving library private members and public extension members forwarding to the private members, where the second author then doesn't expose those extensions.

But it's cumbersome, and if the third party author happens to import the extensions for another reason, they'll still be there.

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

3 participants