Perl's object system is famously flexible. bless a reference, add some accessors, and you're in business. Over the years, modules like Moose, Moo, and Mouse have built increasingly sophisticated layers on top of that foundation — giving us type constraints, roles, lazy attributes, and declarative syntax.
But they all share the same bedrock: a blessed hash reference.
Object::Proto takes a less used approach. It stores objects as blessed arrays, maps property names to slot indices at compile time, and compiles accessors down to custom ops — the same mechanism Perl uses internally for built-in operations. The result is an object system that is type-safe, feature-rich, and significantly faster than the previously mentioned alternatives.
The Basics
use Object::Proto;
object 'Animal',
'name:Str:required',
'sound:Str:default(silence)',
'age:Int';
my $cat = new Animal name => 'Whiskers', age => 3;
print $cat->name; # Whiskers
print $cat->sound; # silence
$cat->age(4); # setter
The object keyword defines a class at compile time. Property specifications use a colon-separated format: name:Type:modifier. No hash lookups at runtime — $cat->name compiles to a direct array slot access.
Both positional and named constructors are supported:
my $cat = new Animal 'Whiskers', 'meow', 3; # positional (fastest)
my $cat = new Animal name => 'Whiskers', age => 3; # named pairs
What You Get
Built-in Types (Zero Overhead)
Any Defined Str Int Num Bool ArrayRef HashRef CodeRef Object
These are checked inline at the C level — no function call, no callback. You can also register custom types in Perl or in XS.
Slot Modifiers
object 'Person',
'name:Str:required',
'email:Str:required:readonly',
'age:Int:default(0)',
'nickname:Str:clearer:predicate',
'settings:HashRef:lazy:builder(_build_settings)',
'parent:Object:weak',
'internal_id:Int:arg(_id)', # constructor uses _id, accessor uses internal_id
'score:Num:trigger(on_score_change)',
'rank:Str:reader(get_rank):writer(set_rank)';
Everything you'd expect from a modern object system: required, readonly, default, lazy, builder, clearer, predicate, reader, writer, trigger, weak references, and arg (init_arg) — all compiled to C/XS.
Inheritance
object 'Animal', 'name:Str:required', 'sound:Str';
object 'Dog',
extends => 'Animal',
'breed:Str';
my $dog = new Dog name => 'Rex', sound => 'Woof', breed => 'Lab';
print $dog->isa('Animal'); # 1
Multiple inheritance and multi-level chains work as youl would expect:
object 'Triathlete',
extends => ['Swimmer', 'Runner'],
'event:Str';
Important note: because Object::Proto objects are arrays and Moo/Moose/Mouse objects are hashes, you cannot inherit from Moo, Moose, or Mouse classes (or vice versa). The internal representations are fundamentally incompatible. If you need to interoperate, use composition (roles or delegation via slots) rather than inheritance.
Roles
Roles work the way you'd expect — define a bundle of slots and method requirements, then compose them into any class. A role can carry its own attributes and demand that consuming classes implement specific methods:
use Object::Proto;
role('Printable', 'format:Str:default(text)');
requires('Printable', 'to_string');
object 'Document',
'title:Str:required',
'body:Str';
with('Document', 'Printable');
package Document;
sub to_string { $_[0]->title . ': ' . $_[0]->body }
The format slot from Printable is merged into Document at define time. If Document didn't provide a to_string method, the with call would croak. You can check role consumption at runtime with Object::Proto::does($obj, 'Printable').
Method Modifiers
You can wrap existing methods without subclassing. before runs code ahead of the original, after runs code after it, and around gives you full control — you receive the original method as $orig and decide whether (and how) to call it:
before 'bark' => sub {
my ($self) = @_;
warn "About to bark...";
};
after 'bark' => sub {
my ($self) = @_;
warn "Done barking.";
};
around 'bark' => sub {
my ($orig, $self, @args) = @_;
return uc $self->$orig(@args);
};
Multiple modifiers can be stacked on the same method. They're stored as linked lists in C, so the dispatch overhead is minimal.
Prototype Chains
This is where Object::Proto gets its name. Like JavaScript's prototype model, any object can delegate to another object. When you access a property that is undef in the current object, the lookup walks up the prototype chain until it finds a defined value:
my $defaults = new Animal name => 'Default', sound => 'silence';
my $cat = new Animal name => 'Whiskers';
$cat->set_prototype($defaults);
print $cat->sound; # 'silence' — resolved from prototype
$cat has no sound of its own, so the accessor walks up to $defaults and returns 'silence'. Setting $cat->sound('meow') writes to $cat directly — prototypes are never mutated by child writes. You can inspect the full chain with Object::Proto::prototype_chain($obj) and measure its depth with Object::Proto::prototype_depth($obj).
Mutability Controls
Objects can be locked (preventing structural changes) or frozen (permanent deep immutability). A frozen object's setters will croak on any attempt to modify — useful for configuration objects, default prototypes, or any value you want to guarantee stays constant:
$obj->lock; # prevent structural changes
$obj->unlock;
$obj->freeze; # permanent immutability — setters will croak
$obj->is_frozen; # true
Locking is reversible; freezing is not. The frozen check is implemented as a single bitflag test in C — it adds no measurable overhead to the normal (unfrozen) code path.
Cloning & Introspection
Object::Proto provides a full introspection API. You can clone objects (the clone is always fresh, unfrozen and unlocked, even if the original wasn't), list properties, inspect slot metadata, and walk the inheritance tree:
my $clone = Object::Proto::clone($obj); # shallow copy, unfrozen
my @props = Object::Proto::properties('Dog');
my $info = Object::Proto::slot_info('Dog', 'name');
my @chain = Object::Proto::ancestors('Dog');
slot_info returns a hashref with everything about a slot: its type, whether it's required, readonly, lazy, has a default, trigger, builder, and so on. Useful for building serializers, form generators, or debugging tools.
Singletons
Need a class that only ever has one instance? Mark it as a singleton and use ->instance() instead of new. The first call constructs the object; every subsequent call returns the same one:
object 'Config', 'db_host:Str', 'db_port:Int';
Object::Proto::singleton('Config');
my $c = Config->instance(db_host => 'localhost', db_port => 5432);
# Config->instance returns the same object every time after first call
This is handled at the C level — no Perl-side state variable or CHECK block trickery.
Function-Style Accessors
For the absolute fastest access, Object::Proto offers function-style accessors that bypass method dispatch entirely. These are compiled to custom ops at compile time — a direct array slot read/write with no entersub, no method resolution, no hash lookup:
use Object::Proto;
object 'Point', 'x:Num', 'y:Num';
Object::Proto::import_accessors('Point');
my $p = new Point 1.0, 2.0;
print x($p); # 1.0 — custom op, not a method call
y($p, 3.0); # set y to 3.0
Performance
Here are benchmarks from a mixed new + set + get workload:
Rate Pure Hash Pure Array XS OO Typed Raw Hash Ref XS func Raw Hash
Pure Hash 1824921/s -- -14% -48% -64% -66% -66% -70%
Pure Array 2124357/s 16% -- -40% -58% -60% -61% -65%
Object::Proto OO 3541833/s 94% 67% -- -31% -34% -35% -41%
Object::Proto T 5114883/s 180% 141% 44% -- -4% -6% -15%
Raw Hash Ref 5331773/s 192% 151% 51% 4% -- -2% -12%
Object::Proto fn 5427162/s 197% 156% 53% 6% 2% -- -10%
Raw Hash 6024884/s 230% 184% 70% 18% 13% 11% --
The function-style path (XS func) runs slightly faster than raw hash reference access — while giving you a real object with type constraints, constructors, and a class hierarchy. The method-style path (XS OO) is nearly 2x faster than a pure-Perl hash-based object.
For hot set + get loops on pre-constructed objects, the XS function path has been measured at over 32 million operations per second — faster than a hash $hash{key} access.
Extending the Type System from XS
If you write XS modules, you can register types with C-level check functions that run at near-zero cost (~5 CPU cycles vs ~100 for Perl callbacks):
/* In your BOOT section */
#include "object_types.h"
static bool check_positive_int(pTHX_ SV *val) {
return SvIOK(val) && SvIV(val) > 0;
}
BOOT:
object_register_type_xs(aTHX_ "PositiveInt", check_positive_int, NULL);
use MyTypes; # registers in BOOT
use Object::Proto;
object 'Counter', 'value:PositiveInt:required';
For Moo/Moose Users: Object::Proto::Sugar
If you prefer declarative Moo-style syntax, Object::Proto::Sugar wraps the XS layer with familiar keywords. Everything compiles down to the same custom ops underneath:
package Animal;
use Object::Proto::Sugar;
has name => ( is => 'rw', isa => Str, required => 1 );
has sound => ( is => 'rw', isa => Str, default => 'silence' );
has age => ( is => 'rw', isa => Int );
sub speak { $_[0]->sound }
package Dog;
use Object::Proto::Sugar;
extends 'Animal';
has breed => ( is => 'rw', isa => Str );
package main;
my $dog = new Dog name => 'Rex', sound => 'Woof', breed => 'Lab';
print $dog->speak; # Woof
print $dog->isa('Animal'); # 1
Every feature from the core module is available through the Sugar interface: lazy, builder, coerce, trigger, predicate, clearer, reader, writer, weak_ref, init_arg, roles (use Object::Proto::Sugar -role / with / requires), method modifiers (before, after, around), and function-style accessors via accessor => 1. The Sugar layer runs at compile time via BEGIN::Lift and then Devel::Hook via BIGIN and UNITCHECK hooks — by the time your first line of runtime code executes, everything has been compiled down to the same custom ops as the raw object keyword.
Sugar with Function-Style Accessors
Add accessor => 1 to a has declaration to get a function style accessor alongside the method style one. Then call import_accessors in your package so the functions are visible when your calling code compiles.
package Point;
use Object::Proto::Sugar;
has x => ( is => 'rw', isa => Num, accessor => 1 );
has y => ( is => 'rw', isa => Num, accessor => 1 );
package main;
Point->import_accessors; # install before compilation
my $p = new Point x => 1.0, y => 2.0;
print x($p); # 1.0 — compiled to custom op
y($p, 3.0); # direct array write
Sugar Benchmarks vs Moo and Mouse
Rate Moo Mouse Sugar Sugar (fn)
Moo 1264320/s -- -2% -55% -60%
Mouse 1290237/s 2% -- -54% -59%
Sugar (method) 2784378/s 120% 116% -- -12%
Sugar (func) 3174093/s 151% 146% 14% --
Sugar method-style is 2.2x Moo. Sugar function-style is 2.5x Moo.
A Note on Compatibility
Once again a reminder: because Object::Proto objects are arrays and Moo/Moose/Mouse objects are hashes, you cannot inherit across the boundary. extends 'My::Moo::Class' will not work. Use role or delegation to bridge the two worlds if needed. Within its own ecosystem — Object::Proto classes inheriting from other Object::Proto classes — everything works seamlessly, including multiple inheritance.
Object::Proto is available on CPAN and licensed under the Artistic License 2.0.
cpanm Object::Proto
cpanm Object::Proto::Sugar # optional, for Moo-like syntax
Top comments (0)