Bulletmagnet
Bulletmagnet

Reputation: 6032

How to make multiple variables readonly in Perl?

In Perl, it is idiomatic to write

$_ = '2021-04-09 10:23:42';
my ($year, $month, $day) = m/(\d\d\d\d)-(\d\d)-(\d\d)/;

How can I use this with Readonly ? Trying Readonly::Array

Readonly::Array my ($year, $month, $day) => m/(\d\d\d\d)-(\d\d)-(\d\d)/;

results in

Type of arg 1 to Readonly::Array must be array (not list) at rdonly.pl line 4, near "m/(\d\d\d\d)-(\d\d)-(\d\d)/;"

Trying Readonly::Scalar

Readonly::Scalar my ($year, $month, $day) => m/(\d\d\d\d)-(\d\d)-(\d\d)/;
++$year;
die $year unless $year == 2022;

results in

1 at rdonly.pl line 9.

($year is not read-only and has an incorrect value)

Upvotes: 3

Views: 336

Answers (2)

ikegami
ikegami

Reputation: 386331

Interface 1

Readonly::ManyScalars my ($year, $month, $day) => [ /^(\d\d\d\d)-(\d\d)-(\d\d)\z/ ]
   or die("Invalid input\n");
while ( Readonly::ManyScalars my ($k, $v) => [ each %h ] ) {
   ...
}

Implementation:

use Readonly qw( );

sub Readonly::ManyScalars {
   my $vals = pop(@_);
   for (0..$#_) {
      Readonly::Scalar $_[ $_ ] => $vals->[ $_ ];
   }

   # As similar to list assignment as we can.
   return wantarray ? @_ : 0+@$vals;
}

This version has the advantage of being similar to the existing Readonly subs. Note that you need to create an array from the assigned values (e.g. by wrapping the expression in square brackets as shown above).


Interface 2

(ro my $year, ro my $month, ro my $day) = /^(\d\d\d\d)-(\d\d)-(\d\d)\z/
   or die("Invalid input\n");
while ( (ro my $k, ro my $v) = each(%h) ) {
   ...
}

Implementation:

use Readonly        qw( );
use Variable::Magic qw( wizard cast );

my $wiz = wizard(
   data => sub { [ 0, $_[1] ] },
   set => sub {
      Readonly::Scalar(${ $_[1][1] }, ${ $_[0] });
      $_[1][0] = 1;
   },
   free => sub {
      Readonly::Scalar(${ $_[1][1] }, undef) if !$_[1][0];
   },
);

sub ro(\$) :lvalue { cast(my $proxy, $wiz, $_[0]); $proxy }

This version has the advantage that not all the variables have to be read-only, and you do stuff like (ro my $x, undef, ro my $z) = ... rather than using dummy vars.

Upvotes: 3

Dada
Dada

Reputation: 6626

I do not have a clean way of doing this. However, combining refaliasing (or Data::Alias on older Perls) and Readonly does the trick, although it looks quite dirty:

use Readonly;

$_ = '2021-04-09 10:23:42';

\(my @ymd) = (\my $year, \my $month, \my $day);
my @matching = m/(\d\d\d\d)-(\d\d)-(\d\d)/;
map { Readonly::Scalar $ymd[$_] => $matching[$_] } 0 .. $#ymd;

print "$year/$month/$day"; # print 2021/04/09
++$year; # dies with "Modification of a read-only value attempted at tmp.pl line ..."

The result is pretty similar as what you wanted. In particular, the content of @ymd is also read-only, which means that you won't have the surprise of discovering that $year was modified through @ymd.

Using List::MoreUtils::pairwise allows to replace

map { Readonly::Scalar $ymd[$_] => $matching[$_] } 0 .. $#ymd;

With

pairwise { Readonly::Scalar $a => $b } @ymd, @matching;

Which looks a bit nicer.

If you'd prefer not to use the refaliasing feature, you can use Data::Alias: you just need to replace \(my @ymd) = (\my $year, \my $month, \my $day); with alias my @ymd = my ($year, $month, $day);

Finally, there are several ways to put that code in a sub in order to factorize the code a bit and improve readability. For instance:

$_ = '2021-04-09 10:23:42';

init_readonly(my $year, my $month, my $day,
              [m/(\d\d\d\d)-(\d\d)-(\d\d)/]);

print "$year/$month/$day";
++$year;
die $year unless $year == 2022;

sub init_readonly {
    my $values = pop @_;
    pairwise { Readonly::Scalar $a => $b } @_, @$values;
}

(Data::Alias is not needed anymore since within a sub, @_ contains aliases to the arguments)

Upvotes: 1

Related Questions