Perl-Apollo / Corinna

Corinna - Bring Modern OO to the Core of Perl
Artistic License 2.0
157 stars 19 forks source link

To Twigil or Not To Twigil #29

Closed Ovid closed 2 years ago

Ovid commented 3 years ago

In Corinna, you can write this:

class Point {
    has ($x, $y) :param;

    method move ($dx, $dy) {
        $x += $dx;
        $y += $dy;
    }
}

But instead of moving the point via differences, what if you wanted to just pass the new x and y values?

method move ($x,$y) {
    ...
}

You can't do that because the local variables $x and $y hide the instance variables. In Java, you could do this:

public void move (double x, double y) {
    this.x = x;
    this.y = y;

Corinna's semantics are sufficiently different from Java's that we don't have this option. What are our options?

The twigils might look like this:

class Point {
    has ($.x, $.y) :param;

    method move ($x, $y) {
        $.x = $x;
        $.y = $y;
    }
}

While I kind of like like that and it makes it immediately clear that this isn't just a normal local variable, nonetheless, introducing even more punctuation could increase Perl's reputation for line noise. Given that I don't see Java devs complaining too often about confusing local and instance variables, I'm not sure if we really have an issue, other than the fact that when our lexical variables hide instance variables, there's not much we can do to work around this other than providing a :writer:

class Point {
    has ($x, $y) :param :writer;

    method move ($x, $y) {
        $self->x($x);
        $self->y($y);
    }
}
Ovid commented 3 years ago

Vote! Should we have twigils or not? (note: this vote is not binding)

duncand commented 3 years ago

You DEFINITELY should have separate namespaces for slots and parameters. It is normal and good that one should be able to use the same unqualified names for corresponding slots and parameters. However it is done is less important than THAT it is done.

duncand commented 3 years ago

I believe the best solution aesthetically is something where if you have multiple objects of the current class a method of that class can access each of their members with the same verbosity. So if you have say a $self and an $other where the first is an implicit method argument and the second is an explicit argument then you can, say go $self::x and $other::x or something. If you had an actual twigil $.x or whatever for $self, what would you say for $other? So I support whatever gives balance between the 2 objects.

xenu commented 3 years ago

introducing even more punctuation could increase Perl's reputation for line noise.

TBH, I don't think it's productive to worry about Perl's reputation. It's unlikely it will ever change, especially because of such a small change. Memes rarely go away.

haarg commented 3 years ago

I believe the best solution aesthetically is something where if you have multiple objects of the current class a method of that class can access each of their members with the same verbosity. So if you have say a $self and an $other where the first is an implicit method argument and the second is an explicit argument then you can, say go $self::x and $other::x or something. If you had an actual twigil $.x or whatever for $self, what would you say for $other? So I support whatever gives balance between the 2 objects.

The problem with any syntax that includes the instance variable is that they are generally already valid syntax that isn't reasonable to overload. $self::x means the global variable $x in the package self. $self:x could be part of a ternary. $self.x is concatenating $self with the return from calling the sub x. Syntax like $:x or $.x on its own does not have this problem. I can't think of any syntax that actually works for this that isn't extremely hateful.

There hasn't been any discussion about providing access to members in other objects, even of the same class. All current and potential future plans have revolved around using methods for that, possibly with a trust or protection model.

duncand commented 3 years ago

I believe the best solution aesthetically is something where if you have multiple objects of the current class a method of that class can access each of their members with the same verbosity. So if you have say a $self and an $other where the first is an implicit method argument and the second is an explicit argument then you can, say go $self::x and $other::x or something. If you had an actual twigil $.x or whatever for $self, what would you say for $other? So I support whatever gives balance between the 2 objects.

The problem with any syntax that includes the instance variable is that they are generally already valid syntax that isn't reasonable to overload. $self::x means the global variable $x in the package self. $self:x could be part of a ternary. $self.x is concatenating $self with the return from calling the sub x. Syntax like $:x or $.x on its own does not have this problem. I can't think of any syntax that actually works for this that isn't extremely hateful.

There hasn't been any discussion about providing access to members in other objects, even of the same class. All current and potential future plans have revolved around using methods for that, possibly with a trust or protection model.

To be clear, I'm not proposing any specific syntax like the :: or whatever, I only used that to illustrate the balance thing by using the same syntax for self/other.

I also want to be clear that I actually consider it a misfeature of common OO systems that there even is a conceptual difference between "self" and "other". I much prefer the conception in functional languages that you just have routines and they have parameters and you treat all the parameters the same.

Especially when you're talking about a routine that has multiple parameters of the same type and all the parameters are that type, such as comparison or addition or whatever, so you have foo($x,$y) where $x and $y are objects, you should be able to use the same syntax on both parameters to access their slots.

With classes the normal thing that everyone expects is that when a class declares a "private" slot or method, that all methods of that same class can access them for every object of that class on equal terms, and it makes no difference whether in a call $x->foo($y) whether it is the $x or $y object of that class.

Methods inside a class are NOT the same as methods of any other class.

Today is the first time I heard any notion of Corinna being designed to treat private slots differently as to whether class Foo can see them for $x and not $y. I hope that's not the case, it seems counter to any good design and seems unjustified.

duncand commented 3 years ago

I suggest a solution could be some unused variant of ->. So normal -> is used for regular accessors generated by :reader or :writer etc like is normal for Perl. And then something like I don't know ->> or -!> pick something is how a class references its own private slots on any object whether $self or $other. So eg $self->>x = 42; and return $other->>y;. This is balanced, there is no twigil, and an appropriate choice doesn't conflict with anything that exists now.

leonerd commented 3 years ago

@duncand A difference between twigils and your proposed ->>, is that twigils will still interpolate nicely into strings.

salva commented 3 years ago

A couple of ideas:

1) In the same way my and our exist, another keyword could be used to get an object instance variable into the scope. Maybe you can even reuse has for that:

class Foo {
  has $x;
  method ($x) {
    has $x = $x;
    # now $x refers to the instance variable
  }
}

I am not sure this bring-the-slot-into-scope behavior would be too user friendly but it removes the need for a new syntax and it is consistent when how scope is already managed.

2) Twigils but surrounded by curly brackets:

class Foo {
  has $x;
  method ($x) {
    ${.x} = $x; 
  }
}

Using that in current perl syntax results in an error, so it doesn't conflict with any currently valid syntax.

Note also, that ${^foo} is also a special syntax, so there is some precedent for going that way.

Also, this {.foo} syntax can be abused in some other ways. For instance, for accessing other object slots without breaking emcapsulation: $other->{.foo} = 7 can be translated into something functionally similar to $CURRENT_CLASS->accessor($other, "foo") = 7; That would croak unless $other belongs to the class shared with $CURRENT_CLASS where slot foo was declared.

Ovid commented 3 years ago

@salva the has $x = $x is a very interesting idea, but I think it doesn't read well. However, there's been discussion of using slot or attr instead of has, though it's not been settled. However, if we go with slot, we get this:

class Point {
    slot ($x, $y) :param;

    method move ($x, $y) {
        slot $x = $x;
        slot $y = $y;
    }
}

That would neatly sidestep the issue, at the expense of overloading the meaning of the slot keyword.

talexb commented 3 years ago

Leaving aside the twigil issue, why not have a move method that behaves in a relative manner, and a move_to method that does an absolute move?

Ovid commented 3 years ago

@talexb Those are both reasonable, but I needed to have an example which showed the inadvertent data hiding.

oodler577 commented 3 years ago

https://docs.raku.org/language/variables is useful for 2 reasons:

  1. it clearly defines what a twigil is
  2. it shows that you're heading towards perl 6/raku, which for your intents I think is a huge mistake

If you want this to be successful (and I want to see it successful myself), it needs to tend towards Perl5. This is just what the last 20 years has shown us. Otherwise, you're just risking the reimplementation (or vulnerable to the accusation) of Perl 6/Raku.

garu commented 3 years ago

Leaving aside the twigil issue, why not have a move method that behaves in a relative manner, and a move_to method that does an absolute move?

@talexb I believe it's not about solving the presented code. If it were, one could simply use a different name and avoid the lexical trap altogether:

class Point {
    has ($x, $y) :param;

    method move ($new_x, $new_y) {
        $x = $new_x;
        $y = $new_y;
    }
}

I think @Ovid is just trying to expose the caveat that "local variables named like instance variables will not work". Even more so, they are an issue that will bite people, specially newcomers, because the '"my" variable $x masks earlier declaration in same scope' warning only happens when you actually declare them in the same scope, not when we declare a variable with the same name on an inner scope:

$ perl -WE 'my $x; my $x;'
"my" variable $x masks earlier declaration in same scope at -e line 1.

$ perl -WE 'my $x; { my $x; }'
(no warnings)

Because this is already an issue with Perl, I would NOT add twigils or any other form of "magic" workaround unless it's something pervasive throughout the language (which may never happen), or at the very least under regular signatures.

perl -Mfeature=signatures -WE 'my $x = 1; sub x ($x) { $x = $x } x(2); say $x'
The signatures feature is experimental at -e line 1.
1

So, until it is fixed in Perl (fsvo "fixed"), I wouldn't try to change this behavior in Corinna at all. I'd add it to the "CAVEATS" section of the docs and move on.

Which is to say I agree with @Ovid's final comment that "(...)'m not sure if we really have an issue, other than the fact that when our lexical variables hide instance variables, there's not much we can do to work around this other than providing a :writer:" (or using another name for either variable, or making the instance variables private and providing getters, or ...)

oodler577 commented 3 years ago

@Grinnz ~ as an attempt to address your "confused" emoticon, I'd like to just simply put it this way. If Cor can't be discussed or implemented without sticking to familiar Perl 5 concepts, then this is a red flag and might indicate the need to seek a return to the ground state; that being things and stuff that will be familiar to perl 5 programmers - which Cor needs to keep as a potential set of users. Deviating from that is where I've seen all efforts fail - Perl 6 just being the most epic example - in terms of a natural progression of Perl 5 ( don't think this can be disputed ).

vrurg commented 3 years ago

@Ovid @salva By trying to avoid linenoise with twigils, you fall into another big problem: extra code for simple ops. Moreover, in the following example it's really weird to have lexical parameters being instantly overwritten with slots...

class Point {
    slot ($x, $y) :param;

    method move ($x, $y) {
        slot $x = $x;
        slot $y = $y;
    }
}

Instead you could consider doing this:

class Point {
    slot ($x, $y) :param;

    method move (slot $x, slot $y) {
    }
}

Or, depending on what is considered corinnish way of doing things:

class Point {
    slot ($x, $y) :param;

    method move ($x :slot, $y :slot) {
    }
}

The original idea about "lexical slots" could still be used though for when slots might be needed deeper inside the code:

class Point {
    slot ($x, $y) :param;

    method move ($x, $y) {
        if ($self->some_condition($x, $y)) {
            slot $x = $x;
            slot $y = $y;
            ...
        }
        else {
             die "The coords are bad, really bad!";
        }
    }
}

UPD Oh, and BTW: I'm pro-twigil anyway. The idea of attributes/slots looking same way as lexicals never looked good to me from code readability point of view. It's ok for small examples, but in big code with many slots it would require from one to remember all of them. And it's for one class alone. What about many classes in a project?

haarg commented 3 years ago

@Grinnz ~ as an attempt to address your "confused" emoticon, I'd like to just simply put it this way. If Cor can't be discussed or implemented without sticking to familiar Perl 5 concepts, then this is a red flag and might indicate the need to seek a return to the ground state; that being things and stuff that will be familiar to perl 5 programmers - which Cor needs to keep as a potential set of users. Deviating from that is where I've seen all efforts fail - Perl 6 just being the most epic example - in terms of a natural progression of Perl 5 ( don't think this can be disputed ).

None of this has anything to do with Raku/Perl 6. Just because a term has been used by Raku does not mean it owns the term.

oodler577 commented 3 years ago

@haarg you're grossly missing my point, but I digress

TBSliver commented 3 years ago

Why not something like the following to access slots:

class Point {
    slot ($x, $y) :param;

    method move ($x, $y) {
        if ($self->some_condition($x, $y)) {
            $self{ x } = $x;
            $self{ y } = $y;
            ...
        }
        else {
             die "The coords are bad, really bad!";
        }
    }
}

gives a differentiation between methods accessed using -> and variables accessed using {...}, and kind of makes sense given existing perl ideas, basically making %self be the backend magic for accessing slots. Coming from a newbie perspective as well, it'l just look like $self is how you get to the classes items in general, making it easier to teach basic concepts of hash vs hashref later (assuming someone comes into perl and dives straight for Cor based code anyway)

TBSliver commented 3 years ago

Minor aside for the example I just gave, it makes the following possible:

class Point {
  slot ($x, $y) :param;

  method move ($x, $y) {
    $self{ x } = $x;
    $self{ y } = $y;
  }

  # override the get for x for some reason
  method x () {
    return $self{ x } / 10;
  }
}

As I assume you'd use a Point class as such:

my $point = new Point;
$point->move(5,5);
say $point->x; # 0.5

Edit: Obviously I don't know if this will cause issues with any other parts of Cor such as overrides or accessors or setters, someone with more experience in the spec should see where those issues are though 😅

davidnicol commented 3 years ago

I don't think extended sigils are needed, because a naming custom -- such as is currently done with leading underscores in a lot of packages -- would work just fine. Why not follow that convention and always write

has ($_x, $_y) :param;

very simple very easy no problem any more.

aaronpriven commented 3 years ago

I'm not seeing that the override-outer-scopes thing is really a widespread problem.

In those rare cases where it is a problem, using has($_x) :name(x), or alternatively method ( :x($new_x), :y($new_y) ) , would avoid it.

garu commented 3 years ago

I agree with @davidnicol, specially since :param already removes leading underscores.

That said, this is yet another workaround. If the developer forgets about this and uses the same name, the "masks earlier declaration" warning will not show up and a hard to detect(?) bug will happen. But (again), since this already happens with signatures and pretty much any other inner block in perl, I think it should be added to a don't-do-this caveat section and that's it. Unless it's something worth changing everywhere.

Grinnz commented 3 years ago

I don't think extended sigils are needed, because a naming custom -- such as is currently done with leading underscores in a lot of packages -- would work just fine. Why not follow that convention and always write

has ($_x, $_y) :param;

very simple very easy no problem any more.

Since it's a core feature we're designing right now, it seems like a mistake to design it to require a special workaround when we can just, not do that.

djerius commented 3 years ago

Could the parser simply throw an error if a lexical variable has the same name as a slot? Maybe not Perlish (taking away a footgun) but it resolves the problem without new syntax.

yiyian-Lee commented 3 years ago

This is same performance of signatures and old my $x = shift style, maybe new hands can learn this automatically.

If imaging method works like:

sub move { my $x = $self->{x}; my $y = $self->{y}; ... }

Throw a error looks like naturally choice.

duncand commented 3 years ago

I don't think extended sigils are needed, because a naming custom -- such as is currently done with leading underscores in a lot of packages -- would work just fine. Why not follow that convention and always write

has ($_x, $_y) :param;

very simple very easy no problem any more.

I consider that a very dirty kludge. The only clean solution is that there are separate namespaces for slots from other things so people can use the same nice names for both without conflict.

haarg commented 3 years ago

I agree with @davidnicol, specially since :param already removes leading underscores.

Object::Pad does this, because Object::Pad is a playground to experiment with these ideas. A core implementation will definitely not have any special handling for slots beginning with underscores.

leonerd commented 3 years ago

Object::Pad does this, because Object::Pad is a playground to experiment with these ideas. A core implementation will definitely not have any special handling for slots beginning with underscores.

Infact moreover, I put the underscore handling in precisely because of a lack of ability to do twigils there. If we had twigils then that wouldn't be required, as things like

has $:name :reader;

would already create a reader method named name, and not :name.

leonerd commented 3 years ago

This doc section may also be relevant

https://metacpan.org/pod/Object::Pad#Slot-Names

xenu commented 3 years ago

I feel we're overly focusing on the ambiguity between slots and method arguments with the same names. Twigils fix that problem, which is wonderful, but that's not the main reason why I want them in Corinna.

To me, the biggest benefit of twigils is the ability to tell variables from slots at a glance. Slots aren't normal variables, they're special, so it makes sense to make them look different. It's also very perlish. After all, concise explicitness is a distinguishing feature of Perl, sigils and the separation of numeric/stringy operators are good examples of that.

duncand commented 3 years ago

I feel we're overly focusing on the ambiguity between slots and method arguments with the same names. Twigils fix that problem, which is wonderful, but that's not the main reason why I want them in Corinna.

To me, the biggest benefit of twigils is the ability to tell variables from slots at a glance. Slots aren't normal variables, they're special, so it makes sense to make them look different. It's also very perlish. After all, concise explicitness is a distinguishing feature of Perl, sigils and the separation of numeric/stringy operators are good examples of that.

Twigils can be useful for a common shorthand but the more general solution needs to work for both $self and $other objects of the current class to directly reference slots that have no :reader or :writer defined.

duncand commented 3 years ago

So I suggest that a general form could be like $self!x and $other!y and then $!x would be a short-hand for the first and the short-hand wouldn't exist for the second.

duncand commented 3 years ago

The general form would need to take any arbitrary expression and combine it with a slot lookup, so <any expression here> !x for example. Since slots are always private we know at compile/parse time that the !x or whatever is a slot for the current class and so it expects that the expression to its left would evaluate to an object of the current class, though in the general case that would possibly fail at runtime if it isn't actually an object of the current class when that is evaluated.

skirmess commented 3 years ago

What about this?

my $x = 12;
{
    my $x = 'hello world';

    say "inner x = $x";
    say "outer x = $.x";
}

We don't need that why would we need this abomination for Cor? That idea and need is already confusing to me - why does scope work different in methods? Just don't name variables in your method the same as slots in the object if that method needs to access the object slot.

I'm excited about Cor because it "looks and feels" like Perl, something other systems like Moose or Mojo do not (to me). Adding this completely not obvious twigils just make the code harder to read for no real gain (IMHO) - but I'm just a hobbyist.

yiyian-Lee commented 3 years ago

Problems become 3 parts:

  1. should prevent arguments hiding attributes in method?
  2. should have some specific syntax to call attribute in method?
  3. this syntax is twigil?
duncand commented 3 years ago

I believe the best way to design this is to do something that directly mirrors the current blessed hashref approach, which is that you ALWAYS reference a slot in terms of a subscript of an object of the type.

The blessed hash way:

  $self->{x}
  $other->{y}
  (any appropriate value expression)->{z}

The Corinna way:

  $self->!x
  $other->!y
  (any appropriate value expression)->!z

Or something like that.

When I say that slots should have their own namespace, the above is what I really mean, each object such as $self or $other IS the namespace for the slots, and all other kinds of Perl variables continue to scope/behave as they did before.

We really don't need twigils, but we do need the "->" alternative to mean its a slot direct access, whereas plain -> would apply to a defined :reader etc if and only if they exist.

salva commented 3 years ago

Another syntax that can be repurposed for accessing the slots is the quote (currently an alias for ::):

class Point {
    has ($x, $y) :param;

    method foo ($x) {
      $self'x = $x;
    }

    method bar($other) {
      $x += $other'x;
      $y += $other'y;
    }
}

Though, I am not sure it is going to be completely unambiguous when used in more complex code structures. For instance:

$x->{foo}'bar;

String interpolation can be an issue too: "hello '$name'ssss"

duncand commented 3 years ago

Another syntax that can be repurposed for accessing the slots is the quote (currently an alias for ::):

I find it distasteful to use string delimiting characters for any purpose besides string delimiting, so no use of double quotes, single quotes, or backpacks for anything except delimiting strings.

duncand commented 3 years ago

Having given this further thought, I concede that at the moment its hard to think of good examples where both of the following are true at the same time for a given class Foo:

  1. A method of Foo wants to access a slot S for any other object of the Foo class besides the $self object.

  2. Slot S doesn't have any :reader or :writer or other custom public accessor method because it isn't okay for non-Foo classes to see it.

Therefore with respect to the MVP of Corinna I withdraw my objection for the direct access mechanism to slots of Foo being only available for the $self object.

For the MVP I consider it a reasonable compromise to have to declare a public :reader etc method for situations where a method needs to access slots of $other in addition to $self.

salva commented 3 years ago

@duncand,

A method of Foo wants to access a slot S for any other object of the Foo class besides the $self object.

It is pretty common to have functions that take two arguments of the same class. For instance, any class implementing mathematical objects for which some kind of algebra exits (vectors, quaternions, sets, etc.).

For instance, a class implementing sets providing an intersection method that takes $self and $other as arguments. In order to calculate the intersection efficiently you may like to access the internal representation of both objects without being restricted to accessing $other just through its public interface.

leonerd commented 3 years ago

A few other points of note, from my docs on this:

Compare in classical perl OO, with syntax highlighting in editors/etc the $self->{...} variables are easy to see because of highlighting: slot-synhi-1

A simple use of Object::Pad syntax as it currently stands makes them much harder to see because they look identical to lexicals. slot-synhi-2

The use of a unique twigil, such as $:name would make these stand out more - both in plain syntax to a human reader, and also make simple syntax highlighters able to much easier call them out as looking different. slot-synhi-3

(see also the screenshots in my doc: https://docs.google.com/document/d/1d62U2Z3w8zUnzg2QVxuGN6gtCoKATuiW1SCCSVRVIgc/edit#heading=h.a41lkp4lj35)

leonerd commented 3 years ago

Additionally, "unbound methods" with syntactically unambiguous slot accesses inside could solve the question of how a MOP API can add method bodies which refer to such slots. An :unbound attribute could permit the parser to compile a prototype method body unbound to a particular class that can use any slot name, which only becomes checked (and presumably resolved into representation indices) at the time it is bound into a class: Hypothetically allowing such code as:

package NotAClass;

sub import {
  my $class = Object::Pad::MOP->create_class( "Ball" );

  $class->create_slot( '$:colour' );

  $class->create_method( paint => method :unbound {
    say "Will now paint $self in colour $:colour";
  } );
}

as without this ability it becomes difficult to write method bodies.

salva commented 3 years ago

@leonerd,

I think it is reasonable to not have syntax sugar for cases where you are already using the MOP.

IMO, requiring a more convoluted way for accessing slots could be acceptable. For instance:

$class->create_method( paint => sub ($self) {
    my \$colour = $class->get_ref_to_instance_slot($self, "colour");
    say "Will now paint $self in colour $colour";
  } );
Grinnz commented 3 years ago

That means the method you created would be significantly slower, which would in most cases mean nobody would use the MOP for this.

salva commented 3 years ago

That means the method you created would be significantly slower, which would in most cases mean nobody would use the MOP for this.

If you are serious about the MOP, whatever syntax sugar you use would result at the end into a call like the one above. Every time an slot is referenced from a method you have to perform a lookup (or do it when entering the method and cache it as it is done in the code above).

At most, you could use a faster lookup method for the common case (i.e. default storage), but then, you could also use that shortcut in the code above.

Grinnz commented 3 years ago

People aren't "serious about the MOP", they are serious about solving their task in a way that works. The task of this design is to make the way that works natural to use and huffman coded.

duncand commented 3 years ago

@duncand,

A method of Foo wants to access a slot S for any other object of the Foo class besides the $self object.

It is pretty common to have functions that take two arguments of the same class. For instance, any class implementing mathematical objects for which some kind of algebra exits (vectors, quaternions, sets, etc.).

For instance, a class implementing sets providing an intersection method that takes $self and $other as arguments. In order to calculate the intersection efficiently you may like to access the internal representation of both objects without being restricted to accessing $other just through its public interface.

@salva Yes, I am keenly aware of such common cases to use $self plus $other, the math-like things are what I had in mind.

But my first sentence both of the following are true at the same time is the key here, and you didn't counter that.

I figure that in such objects it would be normal that you'd want any class to be able to see the same slots, say to read out the elements of the vectors etc, and so sufficient public accessors would be provided to read them such as with :reader.

Of course I would still rather have what I argued for originally, direct access to all objects, but I realized for an MVP the public accessors should be sufficient, so I won't push it so much.

salva commented 3 years ago

@duncand,

But my first sentence both of the following are true at the same time is the key here, and you didn't counter that.

Yes, I was thinking about that too.

You have an internal representation for your objects that you would like to access when implementing some algorithm combining two objects of the same class.

For instance:

duncand commented 3 years ago

@duncand,

But my first sentence both of the following are true at the same time is the key here, and you didn't counter that.

Yes, I was thinking about that too.

You have an internal representation for your objects that you would like to access when implementing some algorithm combining two objects of the same class.

For instance:

  • A class representing a tree or a queue and a method to merge two of them. In order to do that efficiently you usually need access to the internal representation.
  • A matrix or tensor class where data is stored packed as NVs plus the dimensions and some internal house keeping values (i.e. offsets and stripe sizes). You don't want to make that internal data public, but you need it in order to implement an add operator efficiently.

I have 2 main points to answer this:

  1. To properly solve the general version of this we need something like Raku's "trusts" because in the general case there are MULTIPLE classes involved in the internal representation of a data structure and any one of those classes, or additional classes, should be able to access their members directly. An example I've mentioned in the past is a graph data structure including separate Node and Side classes plus a class representing an entire Graph. Simply being able to see $self and $other slots isn't enough, its only a half-measure.

  2. A workaround for the MVP I would settle for which I might have to do anyway for that multi-class graph example even if private works normally, but can also work if you don't get $other, is to declare "public" accessor methods which are named such that it clearly documents they aren't part of the normal public API and are just part of the internal implementation, for example the method could be named "internal__foo" or such. Assuming the internal properties in question are reference types these can return direct references to them, and then the caller can work with them as if they had direct access to the slots.