In the "Introduction to Venus" article, I described the standard library for Perl 5 provided by the Venus project, and I briefly touched on the fact that Venus has its own object system, and why. In this article, I'd like to explain how the object system works.
Everything, objectified
In Perl, even though the CPAN is overwhelmingly object-oriented, the core is not. In core Perl, most things are not objects. On Venus, everything is an object, or rather everything has the potential to become an object, including numbers, strings, code references, regular expressions, and more. This means you can call methods on almost anything, and it promotes a consistent and intuitive programming experience.
Because Venus-derived code needs to cooperate (play nice) with other Perl code (both functional and object-oriented), almost all Venus library methods return native Perl data types (i.e. non-objects), and facilitate method call chaining via the opt-in autoboxing mechanism. This allows engineers to pass the result of a method call to non-Venus subroutines expecting non-objects, but also optionally engage the autoboxing mechanism and treat everything as an object, chaining multiple method calls together regardless of the value returned.
use strict;
use warnings;
# no boxing
my $string = Venus::String->new('Hello')->lc;
# "hello"
print "$string\n";
# "hello"
use strict;
use warnings;
# opt-in autoboxing
my $string = Venus::String->new('Hello')->box->lc->split->join('.')->unbox;
# bless(..., "Venus::String")
print "$string\n";
# "h.e.l.l.o"
Classes and objects
Venus uses a class-based object-oriented paradigm. You create objects by defining classes and instantiating them. Classes serve as blueprints for objects, defining their attributes (instance data) and behaviors (methods). Venus supports both inheritance and composition as a means of facilitating code reuse and extendability.
package Person;
use strict;
use warnings;
# simple class
use Venus::Class;
attr 'fname';
attr 'lname';
1;
package main;
use strict;
use warnings;
# simple class usage
my $person = Person->new(fname => 'elliot', lname => 'alderson');
# $person->fname;
# "elliot"
# $person->lname;
# "alderson"
Object creation
What sets the Venus object system apart from others is that it doesn't hide the automation framework from you, and instead makes it accessible via its lifecycle hooks. When you instantiate a class, using the new
method, Venus executes a sequence of lifecycle hooks that ultimately result in an object (i.e. a reference associated with (or blessed into) a package).
package Person;
use strict;
use warnings;
# data lifecycle hook
use Venus::Class;
attr 'fname';
attr 'lname';
sub DATA {
my ($self, $data) = @_;
# this hook defines the value be blessed
return $self->SUPER::DATA($data);
}
1;
package Person;
use strict;
use warnings;
# args lifecycle hook
use Venus::Class;
attr 'fname';
attr 'lname';
sub ARGS {
my ($self, @args) = @_;
# this hook marshals constructor args
# e.g. returns a hashref from a list of args, k/v pairs, etc
return $self->SUPER::ARGS(@args);
}
1;
package Person;
use strict;
use warnings;
# buildargs lifecycle hook
use Venus::Class;
attr 'fname';
attr 'lname';
sub BUILDARGS {
my ($self, @args) = @_;
# this hook occurs before the ARGS hook
return $self->SUPER::BUILDARGS(@args);
}
1;
package Person;
use strict;
use warnings;
# build lifecycle hook
use Venus::Class;
attr 'fname';
attr 'lname';
sub BUILD {
my ($self, $data) = @_;
# this hook occurs after instantiation and is passed the blessed reference
return $self->SUPER::BUILD($data);
}
1;
Inheritance
Classes can inherit attributes and methods from parent classes, facilitating code reuse and hierarchy, via inheritance, helping to promote modular and organized code. The Venus guiding ethic is to "be a compliment, not a cudgel", which simply means that Venus doesn't try to change how Perl works, and instead offers a layer of abstraction and/or automation over top of the Perl core raw materials. This means that Venus supports multiple inheritance just as core Perl does.
package Person {
use strict;
use warnings;
use Venus::Class;
attr 'fname';
attr 'lname';
};
package User {
use strict;
use warnings;
# simple inheritence
use Venus::Class;
base 'Person';
attr 'login';
attr 'password';
};
package Entity {
use strict;
use warnings;
use Venus::Class;
attr 'id';
attr 'created';
attr 'updated';
};
package Person {
use strict;
use warnings;
use Venus::Class;
attr 'fname';
attr 'lname';
package User;
use strict;
use warnings;
# multiple inheritence
use Venus::Class;
base 'Person';
base 'Entity';
attr 'login';
attr 'password';
1;
Composition (roles and mixins)
Composition in object-oriented programming is the act of and ability to compose abstract behaviors together to create more complex classes and objects. Composition is arguably better at modularizing code than inheritance because inheritance can force you to adopt methods your derived class may not need (or want).
In Perl, it has become common to refer to units of composition as "roles". Venus supports the notion of roles and mixins, with the difference being how the units are composed into the target class. In layman's terms, "roles" are composed into a class in the order declared, only injecting methods that aren't already declared directly on the target class, whereas "mixins" operate more like Perl "exporters" injecting methods into the target class without regard to whether or not they already exist.
It goes without saying that roles in Perl do not operate using the same semantics as the "traits" concept they're patterned after. It goes without saying, but I said it anyway.
package Authorizable;
use strict;
use warnings;
# simple role
use Venus::Role;
sub login {
# does some authentication ...
return true;
}
sub EXPORT {
return ['login'];
}
1;
package Person;
use strict;
use warnings;
# simple role usage
use Venus::Class;
with 'Authorizable';
attr 'fname';
attr 'lname';
package main;
my $person = Person->new(fname => 'elliot', lname => 'alderson');
# $person->login;
# true
package Authorization;
use strict;
use warnings;
# simple mixin
use Venus::Mixin;
sub login {
# does some authentication ...
return true;
}
sub EXPORT {
return ['login'];
}
1;
package Person;
use strict;
use warnings;
# simple mixin usage
use Venus::Class;
mixin 'Authorization';
attr 'fname';
attr 'lname';
package main;
my $person = Person->new(fname => 'elliot', lname => 'alderson');
# $person->login;
# true
Data access control
Perl doesn’t offer support for data hiding. Directives like public, private, and protected, which you might find in other object-oriented languages are not available. Venus doesn't try to offer an approximation as that would be against its guiding ethic.
However, Venus does provide a superclass that was designed to hide (and prevent tampering with) its implementation details. The Venus::Sealed class (and its derivatives) hide all internal state, with no exceptions.
package Algorithm;
use strict;
use warnings;
# sealed class
use Venus::Class;
base 'Venus::Sealed';
sub __compute {
my ($self, $init, $data) = @_;
my $result;
# does something algorithmicy ...
return $result;
}
1;
package main;
use strict;
use warnings;
# sealed class usage
my $algorithm = Algorithm->new;
my $computed = $algorithm->compute;
# ...
Type checking (and unpacking)
Perl isn't a strongly typed language. Its built-in types are limited and not generally accessible by the engineer. There have been various attempts to provide Perl programmers with a type system, either as a third-party library, framework, or as argumentation.
Again, Venus' guiding ethic is to "be a compliment, not a cudgel", so it doesn't try to provide a type system or framework or argumentation of Perl, but instead provides a simple runtime data validation class, Venus::Assert which can be used by class attributes and subroutines (via Venus::Unpack) to create a consistent runtime type-checking experience.
package main;
use strict;
use warnings;
# venus assert
use Venus::Assert;
my $assert = Venus::Assert->new;
# $assert->accept('float');
# $assert->format(sub{sprintf('%.2f', $_)});
# $assert->result(123.456);
# 123.46
Venus::Assert provides a mechanism for asserting type constraints and coercions on data. It uses inheritance and derives its abilities from Venus::Check. Venus::Check is a standalone class that can be used to validate all kinds of input.
package main;
use strict;
use warnings;
# venus check number
use Venus::Check;
my $check = Venus::Check->new;
# $check->number;
# my $result = $check->result(time);
# 00000000000
package main;
use strict;
use warnings;
# venus check string
use Venus::Check;
my $check = Venus::Check->new;
# $check->string;
# my $result = $check->result("hello world");
# "hello world"
Additionally, Perl doesn't support subroutine signatures, or at least it didn't until v5.36. This means that most code is unpacking its arguments manually, an operation all Perl programmers should be familiar with. In a previous article, I talked about why I believe that types, values, objects, signatures, and the systems that tie these all together, are all inextricably bound. In other words, if you want to support a strong (or even gradual type system), the subroutine signatures have to be aware of the type system, and if you want class attributes to be typed as well, the object system has to be aware of the type system too. All these systems are inextricably bound together.
Using Venus::Unpack Venus provides a mechanism for unpacking, validating, coercing, and otherwise operating on lists of arguments. When composed into a class using the Venus::Role::Unpackable role, objects gain the ability to unpack their subroutine arguments, effectively creating runtime subroutine signatures.
package Authorizer;
use strict;
use warnings;
# unpacking args
use Venus::Class;
with 'Venus::Role::Unpackable';
sub authorize {
my ($self, @args) = @_;
# unpack (or throw exception)
my ($id, $login, $password) = $self->unpack(@args)->signature(
'number',
'string'
);
# do something with unpacked data ...
return true;
}
package main;
my $authorizer = Authorizer->new;
# $authorizer->authorize(123, "xxxxx", "xxxxxxxxx");
Object automation
One of the unique and addictive abilities showcased by the Moose object system is its support for object automation. From constructor argument marshaling, attribute validation, and coercion, to attribute defaults and triggers, Moose spoiled Perl programmers with this feature.
As with most things provided by Venus, object automation is opt-in. It is provided via the Venus::Role::Optional role. Simply aping Moose's object automation would have resulted in inheriting its flaws as well, so Venus implemented many of Moose's object automation features but as an opt-in, using convention over configuration, with a few guiding principles.
The three main Venus object automation principles:
Directives are subroutines, as opposed to attribute metadata, and can be inherited using simple inheritance.
All class attributes are by default optional and read-write, the same as a simple blessed hash member.
Configuring Venus object automation will not inject or otherwise alter the class, and will instead rely on naming conventions in furtherance of convention over configuration.
The following are examples of object automation you might find in Moose (and friends), and the equivalent code in Venus.
package Person;
use strict;
use warnings;
# read-only attributes
use Venus::Class;
with 'Venus::Role::Optional';
attr 'fname';
attr 'lname';
sub readonly_fname {
return true;
}
sub readonly_lname {
return true;
}
1;
package Person;
use strict;
use warnings;
# getter-setter methods
use Venus::Class;
with 'Venus::Role::Optional';
attr 'fname';
attr 'lname';
sub read_fname {
my ($self, $value) = @_;
return ucfirst $self->{fname};
}
sub read_lname {
my ($self, $value) = @_;
return ucfirst $self->{lname};
}
sub write_fname {
my ($self, $value) = @_;
return $self->{fname} = ucfirst $value;
}
sub write_lname {
my ($self, $value) = @_;
return $self->{lname} = ucfirst $value;
}
1;
package Person;
use strict;
use warnings;
# attribute defaults
use Venus::Class;
with 'Venus::Role::Optional';
attr 'fname';
attr 'lname';
sub default_fname {
my ($self) = @_;
return 'Elliot';
}
sub default_lname {
my ($self) = @_;
return 'Alderson';
}
1;
Standard library of abstract behaviors
Venus wants to make its tried-and-true abstract behaviors (or roles) available to its users, which is why much of the useful automation used internally is made available to users of the library as a standard library of abstract behaviors.
My personal favorites are the tryable, catchable, and throwable roles, making any object capable of object-oriented try/catch/finally operations and the ability to throw itself (or sorts). However, in an attempt to be more succinct, I'd like to demonstrate a useful behavior provided by the Venus::Role::Patchable role.
Venus::Role::Patchable provides methods for patching (or monkey-patching) routines, and optionally restoring them to their original state. This is especially useful when testing requires you to temporarily override certain subroutines that may have undesirable side effects.
package Example;
use strict;
use warnings;
# using patchable
use Venus::Class;
with 'Venus::Role::Patchable';
sub execute {
my ($self) = @_;
return $self->dbcount + 1;
}
sub dbcount {
my ($self) = @_;
require DBI;
return DBI->connect("dbi:SQLite:dbname=dbfile.db","","")->do(
'select count(id) from users'
);
}
package main;
use Test::More;
my $example = Example->new;
my $patched = $example->patch('dbcount', sub {
my ($next, @args) = @_;
return 0;
});
is $example->execute, 1, "Example#execute okay";
$patched->unpatch;
done_testing;
Interoperability
Venus is opt-in, all the way down. You can use all of it, some of it, or none of it. It's meant to be a compliment, not a cudgel. It's meant to provide common operations and the utmost level of automation, with the least investment required. As such, it plays well with others. It's highly interoperable. It's extendable, malleable, and fast. In subsequent articles, I will demonstrate, with numbers, how and why Venus is also very performant.
Top comments (0)