`restricted` member variables
Mike Parker
aldacron at gmail.com
Mon Jun 20 11:04:16 UTC 2022
I won't rehash the private-to-the-module debate here. I've always
looked at it purely in terms of the public vs. private interface.
From that perspective, I still don't find the argument that "it
breaks encapsulation" a convincing one on the surface. However,
[this example from Max
Samukha](https://forum.dlang.org/post/yhrioqcjmnipkrvlrpve@forum.dlang.org) goes below the surface:
```d
synchronized class C
{
private int x;
private int y = 1;
invariant() { assert(y == x + 1); }
}
void foo(C c, int x)
{
c.x = x;
c.y = x + 1;
}
```
That made me do a double take. Consider also, in place of an
`invariant`, a setter with a precondition like so:
```d
class D
{
private int _x;
void x(int newX)
in (newX >= 100 && newX < 1000)
{
x = newX;
}
}
void main()
{
D d = new D;
d._x = 10;
}
```
In each case, directly accessing the variable elsewhere in the
module bypasses all the checks.
So, the obvious solution, the same solution I have suggested
numerous times in the private-to-the-module debate, is to move
the class to a separate file. If all we are talking about is the
public vs. private interface, and not the consequences of
bypassing contracts and synchronization, then that's a suitable
solution.
Unfortunately, this is a poor solution when synchronization and
contracts are in the picture. It means these features are only
fully effective when a class is in its own file.
Moreover, the precondition on `D.x` can be bypassed within the
class itself, so moving it to a separate module doesn't make that
problem go away. Any member function can write to `_x` without
going through `x`.
The other argument I make in the private-to-the-module debate,
which again still holds if all we're talking about is `private`,
is that it doesn't matter---if you have access to the file, then
you have access to the private members anyway.
And that doesn't hold here either. `private` is intended to be
accessible inside the module. Any constraints on access are
arbitrarily set by convention (e.g., style guides). Class-level
synchronization is intended to kick in on every function call.
Invariants *are* documented to say that "relationships must hold
for any interactions with the instance from its public
interface", but then what's the point if it's so easily broken
from elsewhere in the module?
And then there's function contracts, particularly on functions
that write to member variables like in my example. This is a
cheaper form of invariant in that it's only applied to this
function and not to all functions in the class. But again, it's
so easy to bypass.
From that perspective, I see massive gaping hole here. I've done
a complete 180 on need for further restricting member variable
access. But I'm seeing it more granularly than class-level and
down to the function level.
After some spitballing, here's something I like:
```d
class E
{
restricted int _y;
restricted(_y) void y(int newY) { _y = newY; }
}
```
A restricted member variable `_y` can only be accessed in
functions explicitly marked `restricted(_y)`. No other member
function can access that variable, and therefore neither can
anything in the broader module.
The function `y` is implicitly private, meaning it follows the
rules of any private member (private-to-the-module). The `_y`
parameter to `restricted` indicates not that the function itself
is restricted, but that it is allowed to access `_y`. So then
this is also possible:
```d
restricted(_y) protected foo { // okay to access _y }
restricted(_y) public bar { // ditto }
```
I imagine this would be useful:
```d
restricted {
int _y;
void y(int newY} { _y = newY};
}
```
Any function declared in this restricted block has access to
`_y`. Multiple variables in a single block could be possible:
```d
restricted {
int _x, _y;
void foo() { // this function is implicitly restricted(_x,
_y) }
}
```
Restricted blocks are independent of each other. Functions in
this block can't access member variables in other blocks.
One benefit of this would be that by tightly restricting write
access to a single setter function like `y()`, any `in` contract
that validates the new value is effectively a class invariant,
but it's only run when this function is called instead of for
every function in the class.
Should restricted member functions be allowed? E.g.:
```d
class F {
restricted foo() { ... }
restricted(foo) bar { ... }
}
```
I don't know. If so, then functions in restricted blocks then be
implicitly `restricted` rather than `private`.
============
Anyway, I thought I'd throw this out there for comment. Maybe
someone will come up with a better idea, or take this one and run
with it. Regardless, I can't be the one to write a DIP for it
since I'm the DIP manager. So if anyone wants to, feel free.
Also, I haven't discussed this with anyone else yet, so I have no
idea what Walter or Atila would say about it.
Destroy!
More information about the Digitalmars-d
mailing list