The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Rope - Tied objects

VERSION

Version 0.27

SYNOPSIS

        package Knot;

        use Rope;

        prototyped (
                bend_update_count => 0,
                loops => 1,
                hitches => 10,
                ...

        );

        properties (
                bends => {
                        type => sub { $_[0] =~ m/^\d+$/ ? $_[0] : die "$_[0] != integer" },
                        value => 10,
                        initable => 1,
                        configurable => 1,
                        enumerable => 1,
                        required => 1,
                        trigger => sub {
                                my ($self, $value) = @_;
                                $self->{bend_update_count}++;
                                return $value;
                        }
                        delete_trigger => sub { ... }
                },
                ...
        );

        function add_loops => sub {
                my ($self, $loop) = @_;
                $self->{loops} += $loop;
        };

        1;

...

        my $k = Knot->new();

        say $k->{loops}; # 1;
        
        $k->{add_loops}(5);

        say $k->{loops}; # 6;

        $k->{add_loops} = 5; # errors

DESCRIPTION

Rope is an Object Orientation system that is built on top of perls core hash tying implementation. It extends the functionality which is available with all the modern features you would expect from an modern OO system. This includes clear class and role definitions. With Rope you also get out of the box sorted objects, where the order you define persists.

CONFIGURE PROPERTIES

initable

If set to a true value then this property can be initialised during the object creationg, when calling ->new(%params). If set to false then on initialisation the code will die with a relevant error when you try to initialise it. (Cannot initalise Object ($name) property ($key) as initable is not set to true.)

writeable

If set to a true value then this property value can be updated after initialisation with any other value. If set to false then the code will die with a relevant error. If writeable is true then configurable is not checked and redundent. (Cannot set Object ($name) property ($key) it is only readable)

configurable

If set to a true value then this property value can be updated after initialisation with a value that matches the type of the existing. If you try to set a value which is not of the same type the code will die with a relevant error. (Cannot change Object ($name) property ($key) type). If you set to false and writeable is also false you will get the same error as writeable false.

enumerable

If set to a true value then the property will be enumerable when you itterate the object for example when you call keys %{$self}. If set to false then the property will be hidden from any itteration. Note also that when itterating your object keys are already ordered based on the order they were assigned.

private

If set to a true value then the property will be private, If you try to access the property from outside of the object definition then an error will be thrown. (Cannot access Object (Custom) property ($key) as it is private)

required

If set to a true value then this property is required at initialisation, either by a value key being set or via passing into ->new. I would suggest using this in conjunction with initable when you require a value is passed. If no value is passed and required is set to true then the code will die with a relevant error. (Required property ($key) in object ($object) not set)

type

The type property/key expects a code ref to be passed, all values that are then set either during initialisation or writing will run through this ref. Rope expects that you return the final value from this ref so you can use coercion via a type.

builder

The buidler property/key expects either a code ref or a scalar that represents the name of a sub routine in your class (currently not functions/properties but may extend in the future). It expects the value for that property to be returned from either the code ref or sub routine. Within a builder you can also add new properties to your object by extending the passed defintion, when extending this way I would suggest using ++$_[0]->{keys} to set the index so that sorting is persistent further down the line.

trigger

The trigger property/key expects a code ref that expects the value for that property to be returned so it can also be used for coecion on setting of a property. Two values are passed into a trigger the first being $self which allows you to access and manipulate the existing objects other properties, the second is the value that is being set for the property being called.

delete_trigger

A delete trigger works the same as a trigger but is called on delete instead of set. The second param is the last set value for that property.

predicate

A predicate allows you to define an accessor to check whether a property is set. You have a few options, you can pass a positive integer or code reference and that will default the predicate to "has_$prop". You can also pass a string to name the accessor like "has_my_thing" or finally if you would like more customisation you can pass a hash that defines the object properties. You cannot create triggers or hooks on predicates directly.

clearer

A clearer allows you to define an accessor to clear a property (set it to undef). You have a few options, you can pass a positive integer or code reference and that will default the clearer to "clear_$prop". You can also pass a string to name the accessor like "clear_my_thing" or finally if you would like more customisation you can pass a hash that defines the object properties. You cannot create triggers or hooks on clearers. when using clearers and type checking in conjuction you need to ensure the type supports undef or you need to create your own custom clearer using a code ref or hash ref.

index

The index property/key expects an integer, if you do not set then this integer it's automatically generated and associated to the property. You will only want to set this is you always want to have a property last when itterating

KEYWORDS

property

Extends the current object definition with a single new property

        property one => (
                initable => 1,
                writeable => 0,
                enumerable => 1,
                builder => sub {
                        return 200;
                }
        );

properties

Extends the current object definition with multiple new properties

        properties (
                two => {
                        type => sub { $_[0] =~ m/^\d+$/ ? $_[0] : die "$_[0] != integer" },
                        value => 10,
                        initable => 1,
                        configurable => 1,
                        enumerable => 1,
                        required => 1
                },
                ...
        );

prototyped

Extends the current object definition with multiple new properties where initable, writable and enumerable are all set to a true value.

        prototyped (
                two => 10
                ...
        );

function

Extends the current object definition with a new property that acts as a function. A function has initable, writeable, enumerable and configurable all set to false so it cannot be changed/set once the object is instantiated.

        function three => sub {
                my ($self, $param) = @_;
                ...
        };

NOTE: traditional sub routines work and should be inherited also.

        sub three {
                my ($self, $param) = @_;
                ...
        }

extends

The extends keyword allows you to extend your current definition with another object, your object will inherit all the properties of that extended object.

        package Ping;
        use Rope;
        extends 'Pong';

with

The with keyword allows you to include roles in your current object definition, your object will inherit all the properties of that role.

        package Ping;
        use Rope;
        with 'Pong';

requires

The requires keyword allows you to define properties which are required for either a role or an object, it works in both directions.

        package Pong;
        use Rope::Role;
        requires qw/host/;
        function ping => sub { ... };
        function pong => sub { ... };

        package Ping;
        use Rope;
        requires qw/ping pong/;
        with 'Pong';
        prototyped (
                host => '...'
        );

before

The before keyword allows you to hook in before you call a sub routine or setting a value. If a defined response is returned it will be carried through to any subsequent calls including the setting of a property.

        package Locked;

        use Rope;
        use Rope::Autoload;

        property count => (
                value => 0,
                configurable => 1,
                enumerable => 1
        );

        function two => sub {
                my ($self, $count) = @_;
                $self->count = $count;
                return $self->count;
        };

        1;

        package Load;

        use Rope;
        extends 'Locked';

        before count => sub {
                my ($self, $val) = @_;
                $val = $val * 2;
                return $val;
        };

        before two => sub {
                my ($self, $count) = @_;
                ... do something that doesn't return a value ...
                return;
        };

        1;

around

The around keyword allows you to hook in around the calling of a sub routine or setting a value. If a defined response is returned it will be carried through to any subsequent calls including the setting of a property. To chain you must use the second param as a code reference.

        package Loading;

        use Rope;
        extends 'Load';

        around two => sub {
                my ($self, $cb, $val) = @_;
                $val = $val * 4;
                return $cb->($val);
        };

        1;

after

The after keyword allows you to hook in after the calling of a sub routine or setting a value. If a defined response is returned it will be carried through to any subsequent calls including the setting of a property.

        package Loaded;

        use Rope;
        extends 'Loading';

        after two => sub {
                my ($self, $val) = @_;
                $val = $val * 4;
                return $val;
        };
        
        1;

locked

The locked keyword can be used to lock your Object so that it can no longer be extended with new properties/keys.

        package Locked;

        use Rope;

        property one => ( ... );

        locked;

        1;

Once you have an object initialised you can toggle whether it is locked by calling 'locked' with either a true or false value.

        $obj->locked(1);

        $obj->locked(0);

METHODS

new

Along with class definitions you can also generate object using Rope itself, the options are the same as described above.

        my $knot = Rope->new({
                name => 'Knot',
                properties => [
                        loops => 1,
                        hitches => {
                                type => Int,
                                value => 10,
                                initable => 0,
                                configurable => 0,
                        },
                        add_loops => sub {
                                my ($self, $loop) = @_;
                                $self->{loops} += $loop;
                        }
                ]
        });

        my $with = Rope->new({
                use => [ 'Rope::Autoload' ],
                with => [ 'Knot' ],
                requires => [ qw/loops hitches add_loops/ ],
                properties => [ bends => { type => Int, initable => 1, configurable => 1 }, ... ]
        }, bends => 5);

        $knot->{loops};
        $with->loops;

destroy

For objects that are running in a long process, like under a web server, you will want to explicitly call destroy on your objects as to achieve the correct scoping a reference has to be held in memory. If running under a script the problem will not exists as destruction happens when that script ends.

        $knot->destroy();

get_initialised

This returns an existing initialised object by index set in order of initialisation, as long as it has not been destroyed by scope or calling the ->destroy explicitly, it will return undef in those cases.

        Rope->get_initialised('Knot', 0);

get_meta

This returns the existing META definition for an object.

        Rope->get_meta('Knot');

NOTE: this is now read only

clear_meta

        Rope->clear_meta('Knot');

set_meta

Extend or redefine an object meta definition.

        package Knot;

        use Rope;

        prototyped (
                one => 1,
                two => 2,
                three =>3
        );

        1;

        package Hitch

        extends 'Knot';

        1;

        my $meta = Ropt->get_meta('Hitch');

        Rope->clear_meta('Hitch');

        delete $meta->{properties};

        Rope->set_meta($meta);

        Hitch->new->one;

from_data

Initialise a Rope object from a perl hash struct.

        my $obj = Rope::Object->from_data({
                one => 1,
                two => {
                        a => 1,
                        b => 2
                },
                three => [
                        {
                                a => 1,
                                b => 2  
                        }
                ]
        }, { use => 'Rope::Monkey' });

        ...

        $obj->one;
        $obj->two->{a};
        $obj->three->[0]->{a};

from_nested_data

Initialise a Rope object from a nested perl hash struct.

        my $obj = Rope::Object->from_nested_data({
                one => 1,
                two => {
                        a => 1,
                        b => 2
                },
                three => [
                        {
                                a => 1,
                                b => 2  
                        }
                ]
        }, { use => 'Rope::Autoload' });

        ...

        $obj->one;
        $obj->two->a;
        $obj->three->[0]->a;

NOTE: this generates an object definition per array item so in many cases it is better you define your object definitions yourself and then write the relevant logic to initialise them. I will eventually look into perhaps itterating all items finding all unique keys and then initialiasing, that will just take a bit more time though.

from_array

Initialise a Rope object from a ordered perl array struct.

        my $obj = Rope::Object->from_array([
                one => 1,
                two => {
                        a => 1,
                        b => 2
                },
                three => [
                        {
                                a => 1,
                                b => 2  
                        }
                ]
        ], { use => 'Rope::Monkey' });

        ...

        $obj->one;
        $obj->two->{a};
        $obj->three->[0]->{a};

from_nested_array

Initialise a Rope object from a nested perl array struct.

        my $obj = Rope::Object->from_nested_array([
                one => 1,
                two => [
                        a => 1,
                        b => 2
                ],
                three => [
                        [
                                a => 1,
                                b => 2  
                        ]
                ],
                four => [qw/one two three/, { ROPE_scope => 'ARRAY' }]
        }, { use => 'Rope::Autoload' });

        ...

        $obj->one;
        $obj->two->a;
        $obj->three->[0]->a;

OBJECT CLASS DEFINITION

        package Builder;

        use Rope;

        property one => (
                initable => 1,
                writeable => 0,
                enumerable => 1,
                builder => sub {
                        return 200;
                }       
        );

        property two => (
                writeable => 0,
                enumerable => 0,
                builder => sub {
                        $_[0]->{properties}->{three} = {
                                value => 'works',
                                writeable => 0,
                                index => ++$_[0]->{keys}
                        };
                        return $_[0]->{properties}->{one}->{value} + 11;
                }
        );

        1;

OBJECT ROLE DEFINITION

        package Builder::Role;

        use Rope::Role;

        property one => (
                initable => 1,
                writeable => 0,
                enumerable => 1,
                builder => sub {
                        return 200;
                }       
        );

        1;

FACTORY AND CHAINS

With Rope you can also create chained and factory properties. A chained property is where you define several code blocks to be called in order that you define, they effectively hook into the Rope cores 'after' callback. A factory sub allows you to define a property which will react based upon the passed in parameters. Factories and Chains can be used in conjunction, the following is and example of a factory into a chain.

        package Church;

        use Rope;
        use Rope::Autoload;
        use Rope::Factory qw/Str/;
        use Rope::Chain;

        prototyped (
                been_cannot_find => [],
                found => []
        );

        function reset => sub {
                $_[0]->been_cannot_find = [];
                $_[0]->found = [];      
        };

        factory add => (
                [Str] => sub {
                        $_[0]->reset() if $_[1] eq 'reset';
                },
                [Str, Str] => sub {
                        $_[0]->reset() if $_[1] eq 'reset';
                        push @{$_[0]->found}, $_[2];
                },
                sub { return 'fallback' }
        );

        chain add => 'ephesus' => sub {
                push @{ $_[0]->been_cannot_find }, 'Ephesus';
                return;
        };
 
        chain add => 'smyrna' => sub {
                push @{ $_[0]->been_cannot_find }, 'Smyrna';
                return;
        };
         
        chain add => 'pergamon' => sub {
                push @{ $_[0]->been_cannot_find }, 'Pergamon';
                return;
        };

        chain add => 'thyatira' => sub {
                push @{ $_[0]->been_cannot_find }, 'Thyatira';
                return $_[0]->been_cannot_find;
        };

        ...

        my $asia = Church->new();

        $asia->add('reset');

        $asia->add('reset', 'House of Mary');

        $asia->reset();

        $asia->thyatira();

Notes: If you chain into a factory, the factory will accept not the passed params but the response from the 'last' chain. Returning undef is a hack that only works when chaining a chain, your first defined chain in this approach should return either the passed request params or some valid response. If you define multiple factories with the same name, the initial will be extended not re-written, so extending a factory or chain should work accross inheritance. If you would like to extend and existing et chain two factories together you can by passing an integer to the factory you would like to create or extend, you must understand the order they are instantiated for this to work as expected.

        factory add => 1 => ( # using the index 0 would extend the existing this extends after a new 'factory'
                [ArrayRef] => sub {
                        push @{$_[1]}, 'Sardis';
                        return $_[1];
                },
        );

AUTOLOADING

If you do not enjoy accessing properties as hash keys and would instead like to access them as package routines then you can simply include Rope::Autoload and this will use perls internal AUTOLOAD functionality to expose your properties as routines.

        package Builder;

        use Rope;
        use Rope::Autoload;

        ...

        1;

So you can write

        $builder->thing = 10;

Instead of

        $builder->{thing} = 10;

MONKEY PATCHING

If for some reason you do not like the Autoload approach then Rope also has support for monkey patching the package routines.

        package Builder;

        use Rope;
        use Rope::Monkey;

        ...

        monkey;

        1;

        $builder->thing = 10;

TYPES

Rope also includes additional helpers for defining properties with fixed types, see Rope::Type for more information. ( internally that uses Type::Standard for the actual type checking. )

        package Knot;
         
        use Rope;
        use Rope::Type qw/int/;
         
        int loops => 1;
        int hitches => 10;

        1;

CAVEATS

before, around and after hooks will work on any property regardless of configuration unless the value is a code reference, here the property must be readonly (you get this by using the function keyword). This is to prevent unexpected behaviour when trying if you have code which sets the property with a new code ref.

If you have a long running process I would suggest calling ->destroy() directly, i hook into the actual DESTROY method however this may or may not be called if Rope keeps the variable in scope.

AUTHOR

LNATION, <email at lnation.org>

BUGS

Please report any bugs or feature requests to bug-rope at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Rope. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Rope

You can also look for information at:

ACKNOWLEDGEMENTS

LICENSE AND COPYRIGHT

This software is Copyright (c) 2023 by LNATION.

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)