When I first started writing Perl in my early 20’s, I tended to follow a lot of the structured programming conventions I had learned in school through Pascal, especially the notion that every function has a single point of exit. For example:
sub double_even_number {
# not using signatures, this is mid-1990's code
my $number = shift;
if (not $number % 2) {
$number *= 2;
}
return $number;
}
This could get pretty convoluted, especially if I was doing something like validating multiple arguments. And at the time I didn’t yet grok how to handle exceptions with eval and die, so I’d end up with code like:
sub print_postal_address {
# too many arguments, I know
my ($name, $street1, $street2, $city, $state, $zip) = @_;
# also this notion of addresses is naive and US-centric
my $error;
if (!$name) {
$error = 'no name';
}
else {
print "$name\n";
if (!$street1) {
$error = 'no street';
}
else {
print "$street1\n";
if ($street2) {
print "$street2\n";
}
if (!$city) {
$error = 'no city';
}
else {
print "$city, ";
if (!$state) {
$error = 'no state';
}
else {
print "$state ";
if (!$zip) {
$error = 'no ZIP code';
}
else {
print "$zip\n";
}
}
}
}
}
return $error;
}
What a mess. Want to count all those braces to make sure they’re balanced? This is sometimes called the arrow anti-pattern, with the arrowhead(s) being the most nested statement. The default ProhibitDeepNests perlcritic policy is meant to keep you from doing that.
The way out (literally) is guard clauses: checking early if something is valid and bailing out quickly if not. The above example could be written:
sub print_postal_address {
my ($name, $street1, $street2, $city, $state, $zip) = @_;
if (!$name) {
return 'no name';
}
if (!$street1) {
return 'no street1';
}
if (!$city) {
return 'no city';
}
if (!$state) {
return 'no state';
}
if (!$zip) {
return 'no zip';
}
print join "\n",
$name,
$street1,
$street2 ? $street2 : (),
"$city, $state $zip\n";
return;
}
With Perl’s statement modifiers (sometimes called postfix controls) we can do even better:
...
return 'no name' if !$name;
return 'no street1' if !$street1;
return 'no city' if !$city;
return 'no state' if !$state;
return 'no zip' if !$zip;
...
Guard clauses aren’t limited to the beginnings of functions or even exiting functions entirely. Often you’ll want to skip or even exit early conditions in a loop, like this example that processes files from standard input or the command line:
while (<>) {
next if /^SKIP THIS LINE: /;
last if /^END THINGS HERE$/;
...
}
Of course, if you are validating function arguments, you should consider using actual subroutine signatures if you have a Perl newer than v5.20 (released in 2014), or one of the other type validation solutions if not. Today I would write that postal function like this, using Type::Params for validation and named arguments:
use feature qw(say state);
use Types::Standard 'Str';
use Type::Params 'compile_named';
sub print_postal_address {
state $check = compile_named(
name => Str,
street1 => Str,
street2 => Str, {optional => 1},
city => Str,
state => Str,
zip => Str,
);
my $arg = $check->(@_);
say join "\n",
$arg->{name},
$arg->{street1},
$arg->{street2} ? $arg->{street2} : (),
"$arg->{city}, $arg->{state} $arg->{zip}";
return;
}
print_postal_address(
name => 'J. Random Hacker',
street1 => '123 Any Street',
city => 'Somewhereville',
state => 'TX',
zip => 12345,
);
Note that was this part of a larger program, I’d wrap that print_postal_address call in a try block and catch exceptions such as those thrown by the code reference $check generated by compile_named. This highlights one concern of guard clauses and other “return early” patterns: depending on how much has already occurred in your program, you may have to perform some resource cleanup either in a catch block or something like Syntax::Keyword::Try’s finally block if you need to tidy up after both success and failure.
7 thoughts on “Get out early with Perl statement modifiers”
Well done on avoiding unless. perlcritic thanks you.
I find if !$foo harder to process. The only reason for that policy is to avoid double negatives, but that’s not what it enforces. Like many core policies it is a poor implementation of a niche preference.
There’s no double negative in the conditionals. That’s a simple enough use case for unless.
Fair, but if I’m using perlcritic I have to either annotate it with ## no critic or change my config.
Which is a great argument for why this policy shouldn’t be used by default, but not much else.
The perlcritic default policies have quite a number of things that have no relevance to perl as it’s written in production and the forbidding unless is absolutely one of them and should be disabled — perl critic’s default policies should be something you go through and pick which ones to turn on, the example configuration is terrible.
Avoiding actual double negatives in unless is absolutely a great idea, though.
But
return unless my $user = $rs->find($user_id);
and similar constructs as a gaurd clause are absolutely idiomatic and perfectly fine perl.
I went for a quick look and found variations of this pattern in:
so while there’s not necessarily a consensus — and you should absolutely make your own choices for your own codebases — it’s well worth treating it as a perfectly acceptable option.
(though as well as avoiding double negatives, please be aware that if you use unless/else except for purposes of trolling you’ll probably annoy the crap out of absolutely everybody 😉
– mst
I am rather of the view that you should (if possible) validate all the input before returning. Something like this:
{"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}
Well done on avoiding unless. perlcritic thanks you.
I find if !$foo harder to process. The only reason for that policy is to avoid double negatives, but that’s not what it enforces. Like many core policies it is a poor implementation of a niche preference.
There’s no double negative in the conditionals. That’s a simple enough use case for unless.
Fair, but if I’m using perlcritic I have to either annotate it with
## no critic
or change my config.Which is a great argument for why this policy shouldn’t be used by default, but not much else.
The perlcritic default policies have quite a number of things that have no relevance to perl as it’s written in production and the forbidding unless is absolutely one of them and should be disabled — perl critic’s default policies should be something you go through and pick which ones to turn on, the example configuration is terrible.
Avoiding actual double negatives in unless is absolutely a great idea, though.
But
return unless my $user = $rs->find($user_id);
and similar constructs as a gaurd clause are absolutely idiomatic and perfectly fine perl.
I went for a quick look and found variations of this pattern in:
Carton
Dancer2
DBIx::Class
IO::Async
Mojolicious
Moo
so while there’s not necessarily a consensus — and you should absolutely make your own choices for your own codebases — it’s well worth treating it as a perfectly acceptable option.
(though as well as avoiding double negatives, please be aware that if you use unless/else except for purposes of trolling you’ll probably annoy the crap out of absolutely everybody 😉
– mst
I am rather of the view that you should (if possible) validate all the input before returning. Something like this:
$errors = ”;
$errors .= “no name\n” unless $name;
$errors .= “no street 1\n” unless $street1;
…
return $errors if $errors;