http://qs321.pair.com?node_id=11124003

Ionic has asked for the wisdom of the Perl Monks concerning the following question:

I'm trying to create a read-only "multi-dimensional" constant using the built-in constant pragma. I know that there's the Readonly module (amongst others), but I'm trying to stick to basics.

Using anonymous list references doesn't really work, because they can be modified, as documented:

Even though a reference may be declared as a constant, the reference may point to data which may be changed, as this code shows.
use constant ARRAY => [ 1,2,3,4 ]; print ARRAY->[1]; ARRAY->[1] = " be changed"; print ARRAY->[1];

Okay, so I'll use some sort of indirection, I thought, and came up with something like this:

use strict; use warnings; use Data::Dumper; use constant INVALID_DATA => ( q{invalid}, 0 ); use constant ADD_DATA => ( q{add}, 1 ); use constant REMOVE_DATA => ( q{remove}, 2 ); use constant MODES => ( \&ADD_DATA, \&REMOVE_DATA ); print {*STDERR} "Dumping MODES: " . Dumper ((MODES)); 1;
… but, this doesn't quite do what I expected. Output:
Dumping MODES: $VAR1 = sub { "DUMMY" }; $VAR2 = sub { "DUMMY" };
I was under the impression that constants are really subs that I could take the reference of, but this doesn't seem to be the case.

Alternatively, I've tried taking direct references, but this is... just merging the lists (which is obviously bad to begin with and what I wanted to avoid by using references) in a weird "reference all elements individually" way:

use constant MODES => ( \ADD_DATA, \REMOVE_DATA );
leading to
Dumping MODES: $VAR1 = \'add'; $VAR2 = \1; $VAR3 = \'remove'; $VAR4 = \2;

Is there any proper way to do this?

Replies are listed 'Best First'.
Re: Multi-dimensional constants
by haukex (Archbishop) on Nov 22, 2020 at 12:20 UTC
    I'm trying to create a read-only "multi-dimensional" constant using the built-in constant pragma. I know that there's the Readonly module (amongst others), but I'm trying to stick to basics.

    Note that core Perl has basically only one kind of inlineable constant, and that's Constant Functions, which is what constant uses, though you could just as well write sub MY_CONSTANT () { 42 } yourself. To make a list "constant" (i.e. read-only, not inlined), you will have to use some other method / CPAN module, as you said.

    Personally, I used to code very defensively, i.e. make all my constants Readonly as much as I could. But after having a bit of trouble with that module, and re-reading the Camel, I now believe that the convention of simply making variables whose values shouldn't be changed all uppercase is easiest. I usually only use constants for cases where I'd like code to be conditionally optimized away, or as replacements for magic numbers.

    I was under the impression that constants are really subs that I could take the reference of

    Yes, in addition to being inlineable by the compiler, they are also regular subroutines. What you're seeing is simply Data::Dumper's way of showing you a code reference; see its docs for details.

    Alternatively, I've tried taking direct references, but this is... just merging the lists (which is obviously bad to begin with and what I wanted to avoid by using references) in a weird "reference all elements individually" way

    This is documented at the bottom of Symbolic Unary Operators.

Re: Multi-dimensional constants
by tobyink (Canon) on Nov 22, 2020 at 19:26 UTC

    You really need to do what Readonly does. Either use Readonly, or use Internals::SvREADONLY() which is the built-in function which Readonly wraps. Internals::SvREADONLY() can be kind of annoying when you need to apply it recursively to references though, especially if those references can include cycles.

Re: Multi-dimensional constants (updated)
by AnomalousMonk (Archbishop) on Nov 22, 2020 at 06:23 UTC

    I'm not sure exactly what you're going for, but maybe this helps:

    Win8 Strawberry 5.8.9.5 (32) Sun 11/22/2020 1:19:09 C:\@Work\Perl\monks >perl use strict; use warnings; use constant INVALID_DATA => ( q{invalid}, 0 ); use constant ADD_DATA => ( q{add}, 1 ); use constant REMOVE_DATA => ( q{remove}, 2 ); use constant MODES => ( \&ADD_DATA, \&REMOVE_DATA ); warn((ADD_DATA)[0]); warn((ADD_DATA)[1]); warn((MODES)[0]); warn((MODES)[1]); warn((MODES)[0]->()); warn((MODES)[1]->()); ^Z add at - line 11. 1 at - line 12. CODE(0x1d2be34) at - line 14. CODE(0x1cf904c) at - line 15. add1 at - line 17. remove2 at - line 18.
    (Update: Also works under Perl version 5.30.3.1 64-bit.)

    Update 1: Changed example code above: removed unused use Data::Dumper; statement.

    Update 2: The syntax &{(MODES)[0]} also works, and you may consider it more elegant:

    Win8 Strawberry 5.8.9.5 (32) Sun 11/22/2020 1:34:08 C:\@Work\Perl\monks >perl use strict; use warnings; use constant ADD_DATA => qw(add 1); use constant REMOVE_DATA => qw(remove 2); use constant MODES => (\&ADD_DATA, \&REMOVE_DATA); warn &{(MODES)[0]}; warn &{(MODES)[1]}; ^Z add1 at - line 8. remove2 at - line 9.
    Also works under version 5.30.


    Give a man a fish:  <%-{-{-{-<

      I'm not sure exactly what you're going for, but maybe this helps …
      Thanks. It looks like I was thrown off by Data::Dumper not dereferencing the subroutine references and instead returning something weird, and also the general need to dereference the subroutine references in other code. Other than that, my idea seems to have been spot-on.

      Update 2: The syntax &{(MODES)[0]} also works, and you may consider it more elegant …
      I would, yes. However, this example snippet will return a syntax error (near ")["):
      my $tmp = 42 + (&((MODES)[1])())[1];
      The same goes for:
      my $tmp = 42 + (&((MODES)[1]))[1];
      Luckily, it can be worked around using:
      my $tmp_ref = (MODES)[1]; my $tmp = 42 + (&$tmp_ref())[1];
      I don't quite understand why the interpreter trips, but since it's easy to work around it using a temporary variable, I wanted to leave that here for reference as well.

        ... this example snippet will return a syntax error (near ")["):

        my $tmp = 42 + (&((MODES)[1])())[1];

        The brackets associated with the & sigil are curlies not parentheses.

        Win8 Strawberry 5.8.9.5 (32) Sun 11/22/2020 2:47:09 C:\@Work\Perl\monks >perl use strict; use warnings; use constant ADD_DATA => qw(add 1); use constant REMOVE_DATA => qw(remove 2); use constant MODES => (\&ADD_DATA, \&REMOVE_DATA); print 42 + (&{ (MODES)[0] })[1]; ^Z 43
        But I don't know if this is exactly elegant!


        Give a man a fish:  <%-{-{-{-<

Re: Multi-dimensional constants
by AnomalousMonk (Archbishop) on Nov 22, 2020 at 08:28 UTC

    Yet Another syntactic variation would be something like:

    Win8 Strawberry 5.8.9.5 (32) Sun 11/22/2020 3:11:34 C:\@Work\Perl\monks >perl -l use strict; use warnings; BEGIN { my %ops = ( ADD_DATA => [ qw(add 1) ], REMOVE_DATA => [ qw(remove 2) ], ); sub valid (*) { return exists $ops{$_[0]}; } sub modes (*) { return valid($_[0]) ? [ @{ $ops{$_[0]} } ] : die "bad '$_[0]'" +; } } print 42 + modes(ADD_DATA) ->[1]; print 'foo' . modes('REMOVE_DATA')->[0]; print 'invalid' if not valid(yyy); print modes('xxx'); ^Z 43 fooremove invalid bad 'xxx' at - line 11.

    If you don't like arrow notation and prefer something like (...)[n], then:

    Win8 Strawberry 5.8.9.5 (32) Sun 11/22/2020 3:14:57 C:\@Work\Perl\monks >perl -l use strict; use warnings; BEGIN { my %ops = ( ADD_DATA => [ qw(add 1) ], REMOVE_DATA => [ qw(remove 2) ], ); sub valid (*) { return exists $ops{$_[0]}; } sub modes (*) { return valid($_[0]) ? @{ $ops{$_[0]} } : die "bad '$_[0]'"; } } print 42 + (modes ADD_DATA) [1]; print 'foo' . (modes 'REMOVE_DATA')[0]; print 'invalid' if not valid yyy; print modes 'xxx'; ^Z 43 fooremove invalid bad 'xxx' at - line 11.

    In either case, the data in %ops is absolutely immutable.


    Give a man a fish:  <%-{-{-{-<

Re: Multi-dimensional constants
by navalned (Beadle) on Nov 22, 2020 at 16:53 UTC

    I would recommend trying `Readonly'.

    use Readonly; Readonly my @readonly => (['book', 'pen', 'pencil'], ['bat', 'ball', 'glove'], ['snake', 'rat', 'rabbit']); print $readonly[0][0], "\n"; $readonly[0][0] = "comicbook"; 1;

    Outputs the following:
    book
    Modification of a read-only value attempted at ./readonly.pl line 11
    Plus you should be able to fill in the array at runtime so that should give you more flexibility.

Re: Multi-dimensional constants
by Anonymous Monk on Nov 22, 2020 at 10:59 UTC
    sub { "DUMMY" };
    I was under the impression that constants are really subs that I could take the reference of
    They are, and you can, but Data::Dumper seems to be having problems deparsing subroutines created by use constant back into source code. You have to set $Data::Dumper::Deparse to a true value to make Data::Dumper use B::Deparse, but even then the constant is nowhere to be seen, despite the subroutine itself works:
    use strict; use warnings; use Data::Dumper; local $Data::Dumper::Deparse = 1; use constant INVALID_DATA => ( q{invalid}, 0 ); use constant ADD_DATA => ( q{add}, 1 ); use constant REMOVE_DATA => ( q{remove}, 2 ); use constant MODES => ( \&ADD_DATA, \&REMOVE_DATA ); print Dumper [ MODES ]; print Dumper [ (MODES)[0]() ];
      print Dumper [ (MODES)[0]() ];

      This statement compiles and runs as expected under Perl version 5.30.3.1 64-bit, but fails to compile under version 5.8.9.5 32-bit (both Strawberry). In the latter case, either of
          print Dumper [ (MODES)[0]->() ];
          print Dumper [ &{ (MODES)[0] } ];
      will work. I haven't done a binary search to find the version at which the quoted syntax begins to be supported (assuming, of course, that the root cause of the failure is a syntax enhancement).


      Give a man a fish:  <%-{-{-{-<