develooper Front page | perl.perl5.porters | Postings from November 2001

Re: restricted hashes, and other readonlyness (was "clamp...")

Thread Previous | Thread Next
From:
Michael G Schwern
Date:
November 2, 2001 11:44
Subject:
Re: restricted hashes, and other readonlyness (was "clamp...")
Message ID:
20011102144420.B23234@blackrider
On Fri, Nov 02, 2001 at 08:33:33AM +0000, Nick Ing-Simmons wrote:
> Michael G Schwern <schwern@pobox.com> writes:
> >There seem to be many "wants" here for read-only hashes:
> >
> >    1) protect against key typos
> >    2) protect against other parts of the program putting random crap
> >       in your hash (usually objects and subclasses)
> >    3) provide a constant hash (ie. fixed keys, fixed values)
> >
> >#1 is the "any read access to a key which doesn't exist is an error".
> 
> And write access ...

Write, right.


> >#2 are provided by a hash with a fixed keyset, nothing more,
> >nothing less.  delete(), exists(), keys() all work like regular
> >hashes.  This is the basic struct.
> 
> I happen to want 2a which stops parts of the program deleting random keys
> in my hash.

You're conflating "deleting a key" with "making a key not allowed".
If this is allowed:

    $foo{allowed_key} = undef;

then why not this:

    delete $foo{allowed_key};

They're just two levels of deletion which we find handy.  It allows
one to cram FOUR meanings into an individual hash value:

    1) There is a true value here
    2) There is a false value here
    3) There is an undefined value here
    4) There is no value here

#4 is a deleted value.  That last one is important, as you'll see
below.

Once you delete an allowed key, it does *not* become disallowed:

    delete $foo{allowed_key};
    $foo{allowed_key} = 'something else';

Preventing deletion is lumped in with locking the values in variant #3.


> >#3 is just a fixed keyset plus disallowing delete() and
> >$locked{$key} = 'foo';
> 
> There is also the small matter of turning on SvREADONLY on the values.

What does SvREADONLY do in this case that's different than disallowing
resetting locked keys?


> >On Thu, Nov 01, 2001 at 07:33:49AM -0800, Jeffrey Friedl wrote:
> >> This natural, drop-in approach feels like The Perl Way to me, which is why
> >> I feel that keys() should maintain the same returns-only-existing-keys
> >> semantics.
> 
> If we disallow delete then existing keys are the allowed keys.

Not quite.  Just because they're allowed doesn't mean it was ever
initialized.  You're probably thinking the initialization will be one
step.  

    struct \%hash, (this => 42, that => 23, whatever => 99);
    print keys %hash;  # this that whatever

I was thinking of a two step init, more along the lines of Jeff's
original clamp proposal.  This allows trusted parts of your program to
change a read-only hash.  It works nicely where the purpose is to
prevent sloppy code from mucking about with your hash, but to allow
your own code to do so post-initialization.

    allowed \%hash, qw(this that whatever);
    $hash{this} = 42;  $hash{that} = 23;
    mk_read_only \%hash;

    print keys %hash;   # this that

    mk_writeable \%hash;
    $hash{whatever} = 99;
    mk_read_only \%hash;

    print keys %hash;   # this that whatever

The two interfaces can live together just fine, just a few HV flag
flipping XS functions.

If you use the first, keys() == allowed keys by a happy coincidence,
no extra code needed.  If you use the second, keys() == initialized
keys, as you'd expect.

So it works both ways!


> >Finally, the argument that we can shave a few bytes of memory by
> >adding a few inconsistencies into the implementation seems a throwback
> >to pseudo-hashes.
> 
> Aside from the (now dead) abuse of keys - the other cause for which I
> am Devil's advocate is the minimalist one:
> 
> One (existing) flag bit SvREADONLY
>  - error on get of non-existing key
>  - error on set of non-existing key
>  - delete is not allowed
>  - keys is just existing code
>  - therefore exists on deleted key is non issue

Sorry to sound like a broken record, but these are the functional
semantics of a 5.005 pseudo-hash.  It didn't work out.  Here's why.

The first two features are the important features of a restricted
hash.  We're trying to prevent typos and sloppy code, so if you try to
get/set a disallowed key it's an error.  OK.

Now #3: Disallowing delete.  Why disallow delete?  How does it help
protect the hash from typos and sloppy code?  As above, if you can do
this:

    $hash{allowed_key} = undef;

why not this

    delete $hash{allowed_key};

we're not trying to protect the values of allowed keys (not with this
variant) just that you don't assign to disallowed keys.  So #3 isn't a
necessary feature of a restricted hash.  It causes an unnecessary
difference between regular hashes and restricted hashes.  It was these
little unnecessary differences that made 5.005 pseudo-hashes hard to
use.

And delete() != disallow.  It's perfectly valid to reassign to a
disallowed key.  The implementation will probably be very similar to
how av_delete and av_exists work (ie. PL_sv_undef).

The last two are just corollaries of #3.


> I want folk to explain clearly the draw back of that and why it is
> not sufficient for appication X. That is which applications need to
> delete members of enumerated-hash

Ok, back to the Class::DBI example again.  If you look inside
Class::DBI (it maps RDBMS tables to classes, and rows to objects)
you'll find a function called _safe_exists() (a copy is below).  

This determined if an object was a regular hash (or if we're using
5.6, when the semantics of pseudo-hashes became more hash-like) or a
pseudo-hash.  If it was a pseudo-hash, exists() didn't tell me what I
wanted to know (at least, it didn't tell me the same thing that a
regular exists() does).

Why?  Well, Class::DBI was written on top of Class::Accessor.  What
Class::Accessor does is simply given a list of keys it creates
accessor methods for them.  So I thought: hmmm, class with a
predeclared, fixed set of attributes... nice place for a pseudo-hash!
I think you'll agree, it does sound tailor-made.  Database-mapping
seems ideally suited for structs.

Wrong.  The problems began when Class::DBI (which isa Class::Accessor)
decided that it needed to look at $self->{column1} and determine if it
had been loaded from the database yet.  With a normal hash, we simply
use exists $self->{column1}.  But with a pseudo-hash, exists() always
returns true!  So that's no good.

This goes back to what I said above about exists/delete providing a
fourth type of value on a hash.

What do you do?  Well, you do something like the below, which doesn't
quite work because it uses defined() for pseudo-hashes which means
that columns which are NULL are never properly cached.  The other
option is to keep a second, mirrored struct which simply says if a
column is cached or not.

So that's my real-world, battle-tested case of where a hash-as-struct
fell on it's face.  This relatively simple problem disallows a whole
class of otherwise perfectly suited Perl modules (ie. object
persistence/RDBM mapping) from effectively using restricted hashes.

I'll try to dig up more 5.005 pseudo-hash battle stories.


# In perl < 5.6 exists() doesn't quite work the same on pseudohashes
# as on regular hashes.  In order to protect ourselves we define our own
# exists function.
use constant PERL_VERSION => $];
sub _safe_exists {
    my($hash, $key) = @_;

    # Because PERL_VERSION is constant this logic will
    # be optimized away in Perl >= 5.6 and reduce to a simple
    # statement.
    # Either its 5.6 or its a hash.  Either way exists() is
    # safe.
    if( PERL_VERSION >= 5.006 ) {
        return exists $hash->{$key};
    }
    else {
        # We can't use ref() since that won't work on objects.
        if( UNIVERSAL::isa($hash, 'HASH') ) {     # hash
            return exists $hash->{$key};
        }
        # Older than 5.6 and its a pseudohash.  exists() will always return
        # true, so we use defined() instead as a cheap hack.
        else {
            return defined $hash->{$key};
        }
    }
}



-- 

Michael G. Schwern   <schwern@pobox.com>    http://www.pobox.com/~schwern/
Perl6 Quality Assurance     <perl-qa@perl.org>	     Kwalitee Is Job One
....and I pull out the Magnum from under the desk where I keep it in case
someone laughs at a joke that's so dry it's got a built in
water-fountain, and blow the lot of them away as a community Service.
	-- BOFH

Thread Previous | Thread Next


nntp.perl.org: Perl Programming lists via nntp and http.
Comments to Ask Bjørn Hansen at ask@perl.org | Group listing | About