The other day I was doing a code review of of some Perl code and I fell over a magic number. I pointed it out to the author, emphasizing the concept of using a constant pragma instead of a regular mutable variable as recommended by the Perl::Critic policy: Perl::Critic::Policy::ValuesAndExpressions::ProhibitMagicNumbers
He changed the code accordingly to use a constant, but copied the value of the constant into a regular variable so it could be used in string interpolation, stating that the use of constants in interpolated strings is ugly.
I completely agree, but the proposed solution did not make much sense either.
This is an example, so you can visualize the code (this is not the actual code):
use v5.10;
use constant TRUTH => 42;
my $truth = TRUTH;
say "The truth is $truth";
A nice constant and proper string interpolation using a regular variable. And the alternative demonstrating the use of the constant in conjunction with the string output:
use v5.10;
use constant TRUTH => 42;
say 'The truth is '.TRUTH;
I recommended use of Readonly instead.
use v5.10;
use Readonly;
Readonly::Scalar my $truth => 42;
say "The truth is $truth";
This gives us the best of both worlds:
- We have an immutable variable
- We can use it in a string interpolation
Readonly comes with additional benefits, so if you by accident change the immutable variable your Perl application dies with the error (your application name and line number might vary).:
Modification of a read-only value attempted at immutable.pl line 11
Example:
#!/usr/bin/env perl
use strict;
use warnings;
use v5.10;
use Readonly;
Readonly::Scalar my $truth => 42;
$truth = 1;
say "The truth is $truth";
The policy Perl::Critic::Policy::ValuesAndExpressions::ProhibitMagicNumbers recommends the use of the constant pragma or Readonly, but another related Perl::Critic policy is not recommending the use of the constant pragma, namely: Perl::Critic::Policy::ValuesAndExpressions::ProhibitConstantPragma. It's wide use easily demonstrates the uglyness also as described in the examples above and in addition the the documentation in Readonly makes compelling case. I have however always enjoyed Perl's constant pragma. I wrote about this along time ago and the actual write-up is no longer available and got lost at some point.
In order to explain it is time to redo the write-up. The constant pragma has one key benefit and that is the Perl compiler can optimize based on this. Let me demonstrate:
#!/usr/bin/env perl
use strict;
use warnings;
use v5.10;
use constant DEBUG => 1;
if (DEBUG) {
print STDERR "Hello Happy World of Debugging\n";
}
exit 0;
Let the compiler back-end (B) and it's companions (B::Terse) do their bidding:
$ perl -MO=Terse constant_optimization.pl
LISTOP (0x7ff13f15cbe8) leave [1]
OP (0x7ff13f15cb00) enter
COP (0x7ff13f15cc30) nextstate
LISTOP (0x7ff13f15cc90) scope
COP (0x7ff13f15ccd8) null [193]
LISTOP (0x7ff13f15cd38) print
OP (0x7ff13ec0c8f8) pushmark
UNOP (0x7ff13f15cdc8) rv2gv
SVOP (0x7ff13ec0c8a0) gv GV (0x7ff13f02ac38) *STDERR
SVOP (0x7ff13f15cd80) const PV (0x7ff13f8d0ab8) "Hello Happy World of Debugging\n"
COP (0x7ff13f15cb48) nextstate
UNOP (0x7ff13f15cba8) exit
SVOP (0x7ff13ec0c938) const IV (0x7ff13f8d0a70) 0
constant_optimization.pl syntax OK
But if set the constant to false (0
):
> perl -MO=Terse constant_optimization.pl
LISTOP (0x7fac8f504b30) leave [1]
OP (0x7fac8f89d780) enter
COP (0x7fac8f89d6d8) null [193]
OP (0x7fac8f504bc8) null [5]
COP (0x7fac8f89d630) nextstate
UNOP (0x7fac8f89d738) exit
SVOP (0x7fac8f89d7c8) const IV (0x7fac900554b8) 0
constant_optimization.pl syntax OK
The product is only 7 lines, the output of Hello Happy World of Debugging
string is optimized out, where as the first version is 13 lines and includes the statements wrapping in our flow-control relying on the constant's value.
If we repeat the exercise using Readonly:
#!/usr/bin/env perl
use strict;
use warnings;
use v5.10;
use Readonly;
Readonly::Scalar my $DEBUG => 0;
if ($DEBUG) {
print STDERR "Hello Happy World of Debugging\n";
}
exit 0;
Firstly we have with the immutable variable set to 0
$ perl -MO=Terse readonly_optimization.pl
LISTOP (0x7fc797820e60) leave [1]
OP (0x7fc797821df0) enter
COP (0x7fc797820ea8) nextstate
UNOP (0x7fc797820f08) entersub
UNOP (0x7fc797820f80) null [158]
OP (0x7fc797820f48) pushmark
OP (0x7fc795c0c938) padsv [1]
SVOP (0x7fc797820fc8) const IV (0x7fc7970b16e0) 0
UNOP (0x7fc795c0c8a0) null [17]
SVOP (0x7fc795c0c8f8) gv GV (0x7fc797895018) *Readonly::Scalar
COP (0x7fc797821f20) nextstate
UNOP (0x7fc797821f80) null
LOGOP (0x7fc797821fc0) and
OP (0x7fc797820e28) padsv [1]
LISTOP (0x7fc797820c30) scope
COP (0x7fc797820c78) null [193]
LISTOP (0x7fc797820cd8) print
OP (0x7fc797820da8) pushmark
UNOP (0x7fc797820d68) rv2gv
SVOP (0x7fc797820de8) gv GV (0x7fc79602ac38) *STDERR
SVOP (0x7fc797820d20) const PV (0x7fc7970b19c8) "Hello Happy World of Debugging\n"
COP (0x7fc797821e38) nextstate
UNOP (0x7fc797821e98) exit
SVOP (0x7fc797821ed8) const IV (0x7fc7970b1a28) 0
readonly_optimization.pl syntax OK
Secondly we have with the immutable variable set to 1
> perl -MO=Terse readonly_optimization.pl
LISTOP (0x7fcce402e060) leave [1]
OP (0x7fcce402e7f0) enter
COP (0x7fcce402e0a8) nextstate
UNOP (0x7fcce402e108) entersub
UNOP (0x7fcce402e180) null [158]
OP (0x7fcce402e148) pushmark
OP (0x7fcce3d09f48) padsv [1]
SVOP (0x7fcce402e1c8) const IV (0x7fcce512fef8) 1
UNOP (0x7fcce3d09eb0) null [17]
SVOP (0x7fcce3d09f08) gv GV (0x7fcce5237418) *Readonly::Scalar
COP (0x7fcce402e920) nextstate
UNOP (0x7fcce402e980) null
LOGOP (0x7fcce402e9c0) and
OP (0x7fcce402e028) padsv [1]
LISTOP (0x7fcce402de30) scope
COP (0x7fcce402de78) null [193]
LISTOP (0x7fcce402ded8) print
OP (0x7fcce402dfa8) pushmark
UNOP (0x7fcce402df68) rv2gv
SVOP (0x7fcce402dfe8) gv GV (0x7fcce4822838) *STDERR
SVOP (0x7fcce402df20) const PV (0x7fcce51301c8) "Hello Happy World of Debugging\n"
COP (0x7fcce402e838) nextstate
UNOP (0x7fcce402e898) exit
SVOP (0x7fcce402e8d8) const IV (0x7fcce5130228) 0
readonly_optimization.pl syntax OK
The output is very similar and no optimization is made, the value for the flow-control part is visible as either 0
or 1
, depending on what listing you are reading, but apart from that they are identical in number of lines and contents.
Conclusion: when you have parts of your code which could benefit from immutability, like magic numbers, use Readonly, When you however want to benefit from the compiler optimization use the constant pragma.
But as always with Perl, There Is More Than One Way To Do It and Perl::Critic::Policy::ValuesAndExpressions::ProhibitMagicNumbers also points to Const::Fast and according to the documentation for Const::Fast it is an alternative to Readonly - this would however require another blog post.
All code examples used in this post are available on GitHub