This can take a lot of work to use effectively. To help address that, several systems have been developed over the years to reduce boilerplate and provide modern (or “postmodern”) OO features that developers from other languages expect. My favorite for a while has been Moo: it’s got the features I need 90% of the time like built-in constructors, roles (an alternative to composition through inheritance), attributes, type validation, and method modifiers for enhanced polymorphism. And if I need to dig around in the guts of classes, attributes, and the like I can always upgrade to Moo’s big brother Moose and its meta-object protocol with minimal effort.
Corinna, Object::Pad, and porting dbcritic
But there’s a new kid on the block. Curtis “Ovid” Poe has been spearheading Corinna, an effort “to bring effective OO to the Perl core and leapfrog [emphasis his] the capabilities of many OO languages today.” No CPAN modules, no chain of dependencies; just solid OO features and syntax built-in. And while Corinna is a ways off from shipping, Paul “LeoNerd” Evans(maybe I should get a cool nickname too?) has been implementing some of these ideas as new Perl keyword syntax in his Object::Pad module.
Both Ovid and LeoNerd have been asking developers to try out Object::Pad, not just as a new toy, but to get feedback on what works and what needs to be added. So I thought I’d try porting an older small Moo-based project named dbcritic to this new reality. In the process, I learned some of the advantages and disadvantages of working with Object::Pad. Hopefully, this can inform both it and Corinna’s evolution as well as other curious developers’ evaluations. You can follow my coding efforts in this GitHub branch.
First, the marquee result: the code for App::DBCritic (the class I started with) is cleaner and shorter, with 33 lines shaved off so far. Mainly this is due to Object::Pad’s more concise attribute syntax (called “slots” in its documentation) and lack of explicit support for Moo’s attribute coercion. I only used the latter for one attribute in the Moo version and I’m not sure it worked particularly well, so it wasn’t hard to jettison. But if your code supports coercions extensively, you’ll have to look into Object::Pad’s BUILD or ADJUST phase blocks for now.
After, an Object::Pad slot. No coercion and builder code is handled in a later ADJUST block:
has $schema :reader :param = undef;
Speaking of ADJUST blocks, it took a little bit of insight from the #perl IRC channel to realize that they were the appropriate place for setting slot defaults that are computed from other slots. Previously I was using a maze of dependencies mixing Moo lazy attributes and builder methods. Clarifying the main set of optional constructor arguments into a single ADJUST block helped untangle things, so this might be an indication that lazy attributes are an antipattern when trying to write clean code. It’s also worth noting that Object::Pad ADJUST blocks run on object construction, whereas Moo lazy attributes are only built when needed. This tends to matter for database access.
The ADJUST block for the $schema slot:
ADJUST {
my @connect_info = ( $dsn, $username, $password );
if ($class_name and eval "require $class_name") {
$schema = $class_name->connect(@connect_info);
}
elsif ( not ( blessed($schema) and $schema->isa('DBIx::Class::Schema') ) ) {
local $SIG{__WARN__} = sub {
if ( $_[0] !~ / has no primary key at /ms ) {
print {*STDERR} $_[0];
}
};
$schema = App::DBCritic::Loader->connect(@connect_info);
}
croak 'No schema defined' if not $schema;
}
Object::Pad’s slots have one great advantage over Moo and Moose attributes: they directly support Perl array and hash data structures, while the latter only supports scalars and references contained in scalars. This means methods in your class can eliminate a dereferencing step, again leading to cleaner code. I used this specifically in the @violations array and %elements hash slots and was very pleased with the results.
The @violations and %elements slots and their ADJUST blocks:
I did have some development lifecycle issues with Object::Pad, but they’re mainly a result of its future-facing syntax. I had to give up using perltidy and perlcritic in my build and test phases, respectively: perltidy doesn’t understand slot attributes like :reader and :param and will emit an error file (but code still compiles), and several of the perlcritic policies I use report problems because its PPI parser doesn’t recognize the new syntax. I could add exceptions in the perlcriticrc file and litter my code with more ## no critic annotations than it already had, but at this point, it was easier to just disable it entirely.
Overall I’m satisfied with Object::Pad and by extension some of the syntax that Corinna will introduce. I’m going to try porting the rest of dbcritic and see if I can work around the issues I listed above without giving up the kwalitee improvement tools I’m used to. I’ll post my findings if I feel it merits another blog.
What do you think? Is this the future of object-oriented Perl? Let me know in the comments below.
We have a huge codebase of over 700,000 lines of Perl spread across a couple dozen Git repositories at work. Sometimes refactoring is easy if the classes and methods involved are confined to one of those repos, but last week we wanted to rename a method that was potentially used across many of them without having to QA and launch so many changes. After getting some help from Dan Book and Ryan Voots on the #perl libera.chat IRC channel, I arrived at the following solution.
First, if all you want to do is alias the new method call to the old while making the least amount of changes, you can just do this:
*new_method = \&old_method;
This takes advantage of Perl’s typeglobs by assigning to the new method’s name in the symbol table a reference (indicated by the \ character) to the old method. Methods are just subroutines in Perl, and although you don’t need the & character when calling one, you do need it if you’re passing a subroutine as an argument or creating a reference, as we’re doing above.
I wanted to do a bit more, though. First, I wanted to log the calls to the old method name so that I could track just how widely it’s used and have a head start on renaming it elsewhere in our codebase. Also, I didn’t want to fill our logs with those calls—we have enough noise in there already. And lastly, I wanted future calls to go directly to the new method name without adding another stack frame when using caller or Carp.
With all that in mind, here’s the result:
sub old_method {
warn 'old_method is deprecated';
no warnings 'redefine';
*old_method = \&new_method;
goto &new_method;
}
sub new_method {
# code from old_method goes here
}
Old (and not-so-old) hands at programming are probably leaping out of their seats right now yelling, “YOU’RE USINGGOTO! GOTOISCONSIDEREDHARMFUL!” And they’re right, but this isn’t Dijkstra’s goto. From the Perl manual:
The goto &NAME form is quite different from the other forms of goto. In fact, it isn’t a goto in the normal sense at all, and doesn’t have the stigma associated with other gotos. Instead, it exits the current subroutine (losing any changes set by local) and immediately calls in its place the named subroutine using the current value of @_. […] After the goto, not even caller will be able to tell that this routine was called first.
Computer scientists call this tail call elimination. The bottom line is that this achieves our third goal above: immediately jumping to the new method as if it were originally called.
The other tricky bit is in the line before, when we’re redefining old_method to point to new_method while we’re still inside old_method. (Yes, you can do this.) If you’re running under use warnings (and we are, and you should), you first need to disable that warning. Later calls to old_method will go straight to new_method without logging anything.
And that’s it. The next step after launching this change is to add a story to our backlog to monitor our logs for calls to the old method, and gradually refactor our other repositories. Then we can finally remove the old method wrapper.
{"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}