Perl-Apollo / Corinna

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

Support trusts when you support private #19

Closed duncand closed 2 years ago

duncand commented 4 years ago

The context of this feature request is when Cor / the object orientation system supports private attributes/routines/etc in contrast with Perl's traditional way of everything being public.

I feel that it is important to have a mechanism where otherwise-private methods (and maybe also attributes, but at least methods) can be effectively public within an inner circle of related and co-developed classes.

The main use case is when a feature conceptually presents a single unified API but for technical reasons has to be implemented using multiple classes. The set of classes that together implement something in common can see each others' privates but external users of the collection can not.

An example of this is a factory pattern such as a container and inter-connected elements of the container where a container object must be used to create element objects and users may not instantiate an element object directly. Conceptually the DBI ecosystem would be an example with the inter-connectedness of connection handles and statement handles that collectively are a single API and both kinds of objects would link to each other internally.

The requested feature manifests in different forms depending on the host language.

Personally I like the .NET model the most but it relies on the concept of assemblies which is more unique to .NET. When entities are declared "internal" then anything in the same assembly can access it like it is public. The assembly is loosely the .NET analogy to a CPAN distribution "lib" folder or a Java Jar or a DLL and is a multi-level namespace.

The Java model is fundamentally deficient in contrast, where the scope includes anything declared in the exact same namespace and only that namespace, which limits the organization of a class group across a hierarchy and also allows access by tests or other parties' code that is declared in the same Java package namespace.

The Raku model seems to be the best one for Perl to emulate. It doesn't rely on assembly concepts and is strictly on an individual class-by-class basis; each class declares that it "trusts" one or more other classes, and only those named classes have the special privilege. Since the feature request is only meant to exist within co-developed classes by the same author and is NOT meant to be about third-party extension, being closed to known parties is the whole point.

The simplest implementation is just on a whole-class level, where class X just declares it trusts class Y, and then class Y can invoke all of the private methods of class X but no one else can.

Another option which adds complexity but also control is we can have an extra scope such as "trusting" that is an alternative to "private" or "public" that methods can be declared with such that only those methods in X declared "trusting" may be invoked by Y and "private" is still restricted to "X". This is what .NET/Java do, whereas I recall Raku is just all or none. The simpler whole-class option is all I require, but this additional granularity is gravy if available.

A related feature request is that whole classes can be marked private or trusting, this is for when classes are only meant to be used internally and not be part of a public API.

A related feature request is that constructors of classes can independently be marked private or trusting which controls how objects can be instantiated. So in the DBI analogy, a Statement class would be public but its constructor would be trusting so that only a Connection object may instantiate a Statement object but the public can then use it.

Please let me know if you have any questions.

tobyink commented 4 years ago

This can be done in Perl by using a coderef in a lexical variable as the setter, and then making sure both classes are defined within the scope of the lexical variable.

Zydeco example:

use v5.16;
use Data::Dumper;

package Local::App {
    use Zydeco;

    my $shared_method;

    class SecretBox {
        has $secret (
            reader => 'spill',
            writer => \$shared_method,
        );

        factory new_secret ( Str $text ) {
            my $self = $class->new;
            $self->$shared_method( $text );
            return $self;
        }
    }

    class TrustedWriter {
        has box! ( type => SecretBox );

        method alter_box ( Str $text ) {
            $self->box->$shared_method( $text );
        }
    }
}

my $box = Local::App->new_secret( "Hello world" );

say $box->spill;
print Dumper($box);

my $writer = Local::App->new_trustedwriter( box => $box );
$writer->alter_box( "Bonjour monde" );

say $box->spill;
print Dumper($box);

Here's basically the same thing, just using plain Moo, so no truly private attributes:

use v5.16;
use Data::Dumper;

{
    package Local::App;

    my $shared_method;

    package Local::App::SecretBox {
        use Moo;

        has '$secret' => (
            is       => 'bare',
            init_arg => undef,
            reader   => 'spill',
        );

        $shared_method = sub {
            my ( $self, $text ) = ( shift, @_ );
            $self->{'$secret'} = $text;
        };
    }

    sub new_secret {
        my ( undef, $text ) = ( shift, @_ );
        my $self = 'Local::App::SecretBox'->new;
        $self->$shared_method( $text );
        return $self;
    }

    package Local::App::TrustedWriter {
        use Moo;

        has box => ( is => 'ro', required => 1 );

        sub alter_box {
            my ( $self, $text ) = ( shift, @_ );
            $self->box->$shared_method( $text );
        }
    }

    sub new_trustedwriter {
        my ( $self, @args ) = ( shift, @_ );
        my $self = 'Local::App::TrustedWriter'->new( @args );
        return $self;
    }
}

my $box = Local::App->new_secret( "Hello world" );

say $box->spill;
print Dumper($box);

my $writer = Local::App->new_trustedwriter( box => $box );
$writer->alter_box( "Bonjour monde" );

say $box->spill;
print Dumper($box);
duncand commented 3 years ago

For clarity, trusts would specifically only be between 2 classes. In particular, something can not say it trusts a role. The idea is that nothing can get itself trusted by something else, all the control is on the side of the one doing the trusting.

duncand commented 3 years ago

So I had a new realization that the visibility feature I considered so important, trusts/internal/etc, can likely for what I want be faked over top of a fundamental OO system that only natively has private+public.

I am speaking of the feature where internals of class X can be effectively public to classes Y and Z while effectively private to every other class.

As such I am no longer that concerned with the plan for Corinna version 1 to exclude more than the relatively simple and straight forward private+public dual.

The main scenario I was concerned with that traditionally benefitted from a trusts/etc feature involved connected objects that were mutually created exclusively by factory methods.

In this context, there would be at least a one-way internal reference between the objects, where the factory method of the first object provides a reference to itself via a constructor argument of the object of the other class it creates, and then the newer object stores the reference to its creator in one of its own instance members, and that is optionally exposed via an accessor so a user can ask the newer object who its parent is.

In this context, code in each of the 2 classes would often want to access each others' internals for reasons but no other classes should, and typically these internals would not be exposed publicly.

A hypothetical example is that a DBI statement handle is created by a DBI connection handle factory method, and so those 2 handle objects would have have access to each others' internals but non-DBI classes would not.

So an alteration to class design that can work around a lack of language-native trusts/etc is that each class which conceptually wants to have shared internals is implemented as a corresponding inner+outer pair of classes, where an object of outer has a private member which is an object of inner, and inner's members are all public and are the members that outer conceptually has.

In this context, the factory method of class X that makes an object of class Y would pass both the outer X and the inner X objects as arguments to the constructor of Y, and Y then stores both as private members, and Y never exposes the inner X object to the public, though it might expose the outer X object to the public. And then code in Y has access to the internals of X via the inner X object and no other object has access to those internals if it wasn't made by a factory of X, except transitively.

I will still have to test this but I think it would probably work.

Ovid commented 2 years ago

Closing out tickets that are post-MVP. We will revisit this later.