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.
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.
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.Trivia: Along with
&PL_sv_undef
, they are the only three statically-allocated scalars.They are read-only because we wouldn't want
4 == 5
to start returning a true value because&PL_sv_no
was accidentally changed.