This blog has devoted a fair amount of attention to the popular and multifaceted object-oriented system Moose and its lightweight subset Moo. I’ve also covered Object::Pad, the testbed of concepts and syntax for Corinna, the proposed next-generation Perl core OO system. But what if your project is too memory‑, performance‑, or dependency-constrained for these options?
It turns out that CPAN has a rich history of lighter-weight OO modules to meet many different needs. If you can live with their trade-offs, they’re worth investigating instead of rolling your own layer over Perl’s OO. Here are a few.
Class::Struct’s main claim to fame is its inclusion in the standard Perl distribution, so there’s no need to install dependencies from CPAN. It provides a syntax for defining classes as C‑style structs at either compile time or runtime. (There’s no speed advantage to the former; it just means that your class will be built as if you had written the accessors yourself as subs.) Here’s an example:
#!/usr/bin/env perl
use v5.24; # for strict, say, and postfix dereferencing
use warnings;
package Local::MyClass;
use Class::Struct (
foo => '$',
bar => '@',
baz => '%',
);
package main;
my $obj = Local::MyClass->new(
foo => 'hello',
bar => [1, 2, 3],
baz => { name => 'Mark'},
);
say $obj->foo, ' ', $obj->baz('name');
say join ',', $obj->bar->@*;
# replace the name element of baz
$obj->baz(name => 'Sharon');
# replace the second element of bar
$obj->bar(1, 'replaced');
say $obj->foo, ' ', $obj->baz('name');
say join ',', $obj->bar->@*;
And here’s the output:
hello Mark
1,2,3
hello Sharon
1,replaced,3
Note that Class::Struct supports accessors for scalar, array, and hash types, as well as other classes (not demonstrated). Consult the module’s documentation for the different ways to define and retrieve them.
Class::Accessor does one thing: it makes accessors and mutators (also known as getters and setters) for fields in your class. Okay, it actually does another thing: it provides your class with a new method to initialize those fields. Those accessors can be read-write, read-only, or write-only. (Why would you want write-only accessors?) You can define any of them using either its historical class methods or a Moose-like attribute syntax.
If you’re trying to squeeze every bit of performance out of your code and can sacrifice a little flexibility in altering accessor behavior, you can opt for Class::Accessor::Fast or Class::Accessor::Faster. The former still uses hash references under the hood to represent objects and the latter uses array references. The main Class::Accessor documentation contains an efficiency comparison of the three for your edification.
Here’s an example script using Class::Accessor::Faster and the Moose-like syntax:
#!/usr/bin/env perl
use v5.12; # for strict and say
use warnings;
package Local::MyClass;
use Class::Accessor::Faster 'moose-like';
has readwrite => (is => 'rw');
has readonly => (is => 'ro');
package main;
my $obj = Local::MyClass->new( { # must be a hash reference
readwrite => 'hello',
readonly => 'world',
} );
say $obj->readwrite, ' ', $obj->readonly;
$obj->readwrite('greetings');
say $obj->readwrite, ' ', $obj->readonly;
# throws an error
$obj->readonly('Cleveland');
And here is its output:
hello world
greetings world
'main' cannot alter the value of 'readonly' on objects of class 'Local::MyClass' at ./caf.pl line 24.
Class::Tiny both does less and more than Class::Accessor. All of its generated accessors are read-write, but you can also give their attributes lazy defaults. Its generated constructor takes arguments via either a Class::Accessor-style hash reference or a plain list of key/value pairs, so that’s a little more convenient. It also supports Moose-style BUILDARGS, BUILD, and DEMOLISH methods for argument adjustment, validation, and object cleanup, respectively.
It’s a toss-up as to which of the previous two is “better.” You’ll have to examine their respective features and determine which ones map to your needs.
Here’s an example script that shows a few of Class::Tiny’s unique features:
#!/usr/bin/env perl
use v5.12; # for strict and say
use warnings;
package Local::MyClass;
use Class::Tiny qw<foo bar>,
{
baz => 'default baz',
timestamp => sub { time },
};
package main;
my $obj = Local::MyClass->new( # plain key-values OK
foo => 'hello',
bar => 'world',
);
say $obj->foo, ' ', $obj->bar;
say 'Object built on ', scalar localtime $obj->timestamp;
$obj->foo('greetings');
$obj->bar('Cleveland');
say $obj->foo, ' ', $obj->bar;
say $obj->baz;
And its output:
hello world
Object built on Tue Sep 7 09:00:00 2021
greetings Cleveland
default baz
For an even more minimalist approach, consider Object::Tiny. Its accessors are read-only, it gives you a simple constructor, and that’s it. Its documentation lists a number of reasons why it can be superior to Class::Accessor, including lower memory usage and less typing. There’s also a fork called Object::Tiny::RW that adds read-write support to its accessors.
Class::Tiny’s documentation contains a feature table comparison of it, Object::Tiny, and Class::Accessor. This may help you decide which to use.
Here’s an example script:
#!/usr/bin/env perl
use v5.12; # for strict and say
use warnings;
package Local::MyClass;
use Object::Tiny qw<foo bar>;
package main;
my $obj = Local::MyClass->new(
foo => 'hello',
bar => 'world',
);
say $obj->foo, ' ', $obj->bar;
# has no effect unless you use Object::Tiny::RW
$obj->foo('greetings');
say $obj->foo, ' ', $obj->bar;
And its output:
hello world
hello world
Add some speed with XS
If the above options are still too slow and you don’t mind requiring a C compiler to install them, there are variants that use Perl’s XS interface instead of pure Perl code:
If you’re eyeing Moose and Moo’s support for roles (also known as traits) as an alternative to inheritance but still want to keep things light with one of the above modules, you’re in luck. The Role::Tiny module lets you compose methods into consuming classes with Moo-like syntax and will pull in Common Lisp Object System-style method modifier support from Class::Method::Modifiers if you need it. It does mean another couple of CPAN dependencies, so if that’s a problem in your situation you’ll just have to live without roles.
Here’s an example script with a role and a consuming class that uses Class::Tiny. The role requires that its consumers implement a required_method, provides a foo method that uses it, and a method modifier for bar.
#!/usr/bin/env perl
use v5.12; # for strict and say
use warnings;
package Local::MyRole;
use Role::Tiny;
requires 'required_method';
sub foo {
my $self = shift;
say $self->required_method();
}
before bar => sub {
warn 'About to call bar...';
};
package Local::MyClass;
use Class::Tiny {name => ''};
use Role::Tiny::With;
with 'Local::MyRole';
sub bar {
my ($self, $greeting) = @_;
say "$greeting ", $self->name;
}
sub required_method {
my $self = shift;
return 'Required by Local::MyRole';
}
package main;
my $obj = Local::MyClass->new(name => 'Mark');
$obj->bar('hello');
$obj->name('Sharon');
$obj->bar('salutations');
$obj->foo();
And its output:
About to call bar... at ./rt.pl line 17.
hello Mark
About to call bar... at ./rt.pl line 17.
salutations Sharon
Required by Local::MyRole
What’s your favorite?
There will always be those who insist on writing everything longhand, but modules like these can save a lot of time and typing as well as reduce errors. Do you have a favorite, maybe something I missed? Let me know in the comments.
7 thoughts on “Cutting the fat: Lightweight Perl OO modules”
[…] Cutting the fat: Lightweight Perl OO modules […]
If Moo is the kids version of Moose, then Mo is the baby: https://metacpan.org/dist/Mo/view/ReadMe.pod Sometimes astoundingly usable and practical, if you further along the line want to ‘mature’ stuff. Otherwise interesting study in minimalism…
I am hesitant to recommend Mo because it seems like more of a golfing hack than anything else. It also has not seen updates since 2016 and there are a number of (to my eye) critical unaddressed issues on GitHub. If it works for you, great.
I also wouldn’t see it as serious contender for your list. Lazy as I am, I just use it to avoid fatpacking all the otherwise needed packages in one script for my colleagues to use or for distributing it to alot of machines in use, all with perl (yay!), but an ancient one (5.8.8, sadnoises). Inline-Mo solved much of that for me, and if code from those scripts has to be recycled for bigger /more long-term solutions -> upgrade to Moo(se) is easy.
But it seems in the past I just glossed over Class::Struct and forgot about it. Since it’s included — even in 5.8.8 — maybe that’s an alternative, and without colleagues being suspicious about the strange code at the beginning of the script (inlined Mo).
Anyway, nice overview, and thx for reminding me of the batteries already included!
If you’re looking for your objects to themselves be lightweight, check out TOBYINK’s Monjon based on Moo but using a blessed scalar reference instead of a hashref, supporting complex attributes inside-out
What I’m learning is that different people have different ideas on what “lightweight” means. Does it mean memory? Storage? Speed? Features? Dependencies? In Monjon’s case it’s all about saving memory so long as your attributes are small. But it’s slower than Moo and actually depends on it. So it’s all about tradeoffs: what are you willing to sacrifice in order to optimize for something else?
Thank you, this was a nice summary of the various OO for perl. Your examples were simple and to the point
Comments are closed.
{"id":"11","mode":"button","open_style":"in_modal","currency_code":"USD","currency_symbol":"$","currency_type":"decimal","blank_flag_url":"https:\/\/phoenixtrap.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/phoenixtrap.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":500,"top_media_type":"featured_image","featured_image_url":"https:\/\/phoenixtrap.com\/wp-content\/uploads\/2021\/02\/image-200x200.jpg","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":true,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Leave a tip!","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"The Phoenix Trap","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"The Phoenix Trap","form_subtitle":"Do you like what you see? Leave a one-time or recurring tip!","currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for being a supporter!","payment_confirmation_title":"The Phoenix Trap","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"What is the name on your credit card?"},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":null,"terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"What is your email address?"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}
[…] Cutting the fat: Lightweight Perl OO modules […]
If Moo is the kids version of Moose, then Mo is the baby: https://metacpan.org/dist/Mo/view/ReadMe.pod
Sometimes astoundingly usable and practical, if you further along the line want to ‘mature’ stuff. Otherwise interesting study in minimalism…
I am hesitant to recommend Mo because it seems like more of a golfing hack than anything else. It also has not seen updates since 2016 and there are a number of (to my eye) critical unaddressed issues on GitHub. If it works for you, great.
I also wouldn’t see it as serious contender for your list. Lazy as I am, I just use it to avoid fatpacking all the otherwise needed packages in one script for my colleagues to use or for distributing it to alot of machines in use, all with perl (yay!), but an ancient one (5.8.8, sadnoises). Inline-Mo solved much of that for me, and if code from those scripts has to be recycled for bigger /more long-term solutions -> upgrade to Moo(se) is easy.
But it seems in the past I just glossed over Class::Struct and forgot about it. Since it’s included — even in 5.8.8 — maybe that’s an alternative, and without colleagues being suspicious about the strange code at the beginning of the script (inlined Mo).
Anyway, nice overview, and thx for reminding me of the batteries already included!
If you’re looking for your objects to themselves be lightweight, check out TOBYINK’s Monjon based on Moo but using a blessed scalar reference instead of a hashref, supporting complex attributes inside-out
https://metacpan.org/pod/Monjon
What I’m learning is that different people have different ideas on what “lightweight” means. Does it mean memory? Storage? Speed? Features? Dependencies? In Monjon’s case it’s all about saving memory so long as your attributes are small. But it’s slower than Moo and actually depends on it. So it’s all about tradeoffs: what are you willing to sacrifice in order to optimize for something else?
Thank you, this was a nice summary of the various OO for perl. Your examples were simple and to the point