Home > Software design >  Why does Types::Standard break Scalar::Util::readonly?
Why does Types::Standard break Scalar::Util::readonly?

Time:05-13

Perl 5.30.0. Libraries are up-to-date as of today.

I expect Scalar::Util::readonly to return some truthy value if a hash is readonly, and indeed it does :

perl -MReadonly -M'Scalar::Util qw(readonly)' -E'
   say readonly(%ENV);
   Readonly::Hash %ENV => %ENV;
   say readonly(%ENV);
'
0
134283264

... Except when I also want to use Types::Standard, then Scalar::Util::readonly no longer works ?!

perl -MReadonly -M'Scalar::Util qw(readonly)' -MTypes::Standard -E' 
   say readonly(%ENV);
   Readonly::Hash %ENV => %ENV;
   say readonly(%ENV);
'
0
0

I looked at open issues for Types::Standard, but nothing jumps out as directly describing my issue.
What is going on here ?

CodePudding user response:

That's not the proper way to use readonly.

It's impossible to pass a hash to a sub. Only scalars can be passed as arguments to subs. Prototypes can be used to make it look like you are passing a hash to a sub, but that's not the case here.

$ perl -E'
   use Scalar::Util qw( readonly );
   say prototype( "readonly" ) // "[none]";
'
$

That prototype means that

readonly( %ENV )

means

&readonly( scalar( %ENV ) )

It doesn't check if %ENV is read-only; it checks if the value obtained from evaluating %ENV in scalar context is read-only. This is completely wrong.

Scalar::Util::readonly can't be used to check if a hash (or array) is readonly, only scalars.


So how does one check if a hash is read-only?

Well, Perl provides a builtin sub works just like Scalar::Util::readonly called Internals::SvREADONLY. Unlike readonly, SvREADONLY works on arrays as hashes as well as scalars.

$ perl -E'say prototype( "Internals::SvREADONLY" ) // "[none]";'
\[$%@];$

This prototype causes a reference to the first argument to be passed instead of the argument itself. As such,

Internals::SvREADONLY( %x )

is short for

&Internals::SvREADONLY( \%x )

The thing is, the hash returned by Readonly::Hash isn't actually read-only. So Internals::SvREADONLY is of no more use than Scalar::Util::readonly.

$ perl -E'
   use Readonly qw( );
   say Internals::SvREADONLY( %x ) ?1:0;
   Readonly::Hash %x => %x;
   say Internals::SvREADONLY( %x ) ?1:0;
'
0
0

Readonly::Hash uses tie to intercept attempts to change the hash.

$ perl -E'
   use Devel::Peek qw( Dump );
   use Readonly    qw( );
   Readonly::Hash %x => %x;
   Dump( %x );
'
SV = PVHV(0x561f1e51b340) at 0x561f1e5435a8
  REFCNT = 1
  FLAGS = (RMG,OOK,SHAREKEYS)                       <--- No READONLY flag.
  MAGIC = 0x561f1e558290
    MG_VIRTUAL = &PL_vtbl_pack
    MG_TYPE = PERL_MAGIC_tied(P)                    <--- tie() magic was added
    MG_FLAGS = 0x02                                      to intercept attempts
      REFCOUNTED                                         to change the hash.
    MG_OBJ = 0x561f1e515680
    SV = IV(0x561f1e515670) at 0x561f1e515680
      REFCNT = 1
      FLAGS = (ROK)
      RV = 0x561f1e5d39b8
      SV = PVHV(0x561f1e51b400) at 0x561f1e5d39b8
        REFCNT = 1
        FLAGS = (OBJECT,SHAREKEYS)
        STASH = 0x561f1e5d3c88  "Readonly::Hash"
        ARRAY = 0x0
        KEYS = 0
        FILL = 0
        MAX = 7
  AUX_FLAGS = 0
  ARRAY = 0x561f1e541950
  KEYS = 0
  FILL = 0
  MAX = 7
  RITER = -1
  EITER = 0x0
  RAND = 0x2685e09f

The following is how the module checks if it has already made a hash read-only:

tied( %x ) =~ 'Readonly::Hash'

So why the difference in output after Readonly::Hash is used?

While the question is moot, it's still an interesting question to answer.

Well, that's due to a bug in Readonly::Hash: It returns the wrong value in scalar context.

$ perl -E'
   use Readonly qw( );
   my %x = ( a=>4, b=>5, c=>6 );
   say scalar( %x );
   Readonly::Hash %x => %x;
   say scalar( %x );
'
3
1

When %x is used in scalar context, Perl returns the number of elements in a hash.[1]

On the other hand, the magic added by Readonly::Hash causes it to return a true value when the hash isn't empty, and a false value when the hash is empty.

And therein lies the difference.

Perl returns a count as a temporary aka mortal scalar. It's created to contain the returned value and will be freed after the caller has a chance to copy it. There's no point in spending time to make it read-only.[2]

Readonly::Hash, on the other hand, returns not just any true or false scalars. It returns the very same true and false scalars returned by every Perl operators that returns a true or false. Not copies, but the very same scalars, &PL_sv_yes and &PL_sv_no.[3] These scalars are read-only.[4]


So why does Types::Standard have an effect?

While the question is moot, it's still an interesting question to answer.

Unfortunately, I haven't figured that one out.


  1. This wasn't always the case. It never returned just true/false, but the old value was practically only useful as a true/false value.

  2. With some difficulty, you could modify it (my $r = \scalar(%x); $$r), but there would be no point. Doing so would have no effect on the hash.

  3. Trivia: Along with &PL_sv_undef, they are the only three statically-allocated scalars.

  4. They are read-only because we wouldn't want 4 == 5 to start returning a true value because &PL_sv_no was accidentally changed.

  •  Tags:  
  • perl
  • Related