package Hash::AsObject; use strict; use vars qw($VERSION $AUTOLOAD); $VERSION = '0.09'; sub VERSION { return $VERSION unless ref($_[0]); scalar @_ > 1 ? $_[0]->{'VERSION'} = $_[1] : $_[0]->{'VERSION'}; } sub can { # $obj->can($method) # $cls->can($method) die "Usage: UNIVERSAL::can(object-ref, method)" unless @_ == 2; return 1; # We can `do' anything } sub import { return unless ref($_[0]); scalar @_ > 1 ? $_[0]->{'import'} = $_[1] : $_[0]->{'import'}; } # sub isa { # # # $obj->isa($class) # # $cls->isa($class) # # return UNIVERSAL::isa(@_); # # # Hash::AsObject::isa() # return unless scalar @_; # # my $invocant = shift; # # # Hash::AsObject->isa($foo, $etc) # # $obj->isa($other, $etc) # die "Too many arguments" if scalar(@_) > 1; # # my $iref = ref($invocant); # # # Hash::AsObject->isa # # Hash::AsObject::isa($class_name_or_other_scalar) # die "Too few arguments" unless $iref or scalar @_; # # # Hash::AsObject->isa($foo) # if ($iref) { # if (scalar @_) { # # $obj->isa(...) # } else { # # $obj->isa # } # } else { # return UNIVERSAL::isa($invocant, @_); # } # # $obj->isa('Hash::AsObject') # # $obj->isa($other) # # # XXX Not quite right XXX # return __PACKAGE__->isa($_[0]) # unless ref $_[0]; # Hash::AsObject::isa($class_name_or_other_scalar) # # # --- The most likely case: # # $obj->isa($cls); -- most likely case # # $cls->isa($other_cls); -- OK, this might happen # # Hash::AsObject::isa($non_scalar, ...); -- unlikely!! # return ref($_[0])->isa($_[1]) # if scalar @_ > 1; # # $obj->isa; -- not very likely # return $_[0]->{'isa'} if exists $_[0]->{'isa'}; # return undef; # } sub AUTOLOAD { my $self = shift; my $key = $AUTOLOAD; # --- Figure out which hash element we're dealing with if (defined $key) { $key =~ s/.*:://; } else { # --- Someone called $obj->AUTOLOAD -- OK, that's fine, be cool # --- Or they might have called $cls->AUTOLOAD, but we'll catch # that below $key = 'AUTOLOAD'; } # --- We don't need $AUTOLOAD any more, and we need to make sure # it isn't defined in case the next call is $obj->AUTOLOAD # (why the %*@!? doesn't Perl undef this automatically for us # when execution of this sub ends?) undef $AUTOLOAD; # --- Handle special cases: class method invocations, DESTROY, etc. if (ref($self) eq '') { # --- Class method invocation if ($key eq 'import') { # --- Ignore $cls->import return; } elsif ($key eq 'new') { # --- Constructor my $elems = scalar(@_) == 1 ? shift # $cls->new({ foo => $bar, ... }) : { @_ } # $cls->new( foo => $bar, ... ) ; return bless $elems, __PACKAGE__; } else { # --- All other class methods disallowed die "Can't invoke class method '$key' on a Hash::AsObject object"; } } elsif ($key eq 'DESTROY') { # --- This is tricky. There are four distinct cases: # (1) $self->DESTROY($val) # (2) $self->DESTROY() # (2a) $self->{DESTROY} exists and is defined # (2b) $self->{DESTROY} exists but is undefined # (2c) $self->{DESTROY} doesn't exist # Case 1 will never happen automatically, so we handle it normally # In case 2a, we must return the value of $self->{DESTROY} but not # define a method Hash::AsObject::DESTROY # The same is true in case 2b, it's just that the value is undefined # Since we're striving for perfect emulation of hash access, case 2c # must act just like case 2b. return $self->{'DESTROY'} # Case 2c -- autovivify unless scalar @_ # Case 1 or exists $self->{'DESTROY'}; # Case 2a or 2b } # --- Handle the most common case (by far)... # --- All calls like $obj->foo(1, 2) must fail spectacularly die "Too many arguments" if scalar(@_) > 1; # We've already shift()ed $self off of @_ # --- If someone's called $obj->AUTOLOAD if ($key eq 'AUTOLOAD') { # --- Tread carefully -- we can't (re)define &Hash::AsObject::AUTOLOAD # because that would ruin everything return scalar(@_) ? $self->{'AUTOLOAD'} = shift : $self->{'AUTOLOAD'}; } else { # --- Define a stub method in this package (to speed up later invocations) my $src = <<"EOS"; sub Hash::AsObject::$key { my \$v; if (scalar \@_ > 1) { \$v = \$_[0]->{'$key'} = \$_[1]; return undef unless defined \$v; } else { \$v = \$_[0]->{'$key'}; } if (ref(\$v) eq 'HASH') { bless \$v, 'Hash::AsObject'; } else { \$v; } # return ref(\$v) eq 'HASH' ? bless(\$v, 'Hash::AsObject') : \$v; } EOS eval $src; die "Couldn't define method Hash::AsObject::$key -- $@" if $@; unshift @_, $self; goto &$key; } } 1; =head1 NAME Hash::AsObject - treat hashes as objects, with arbitrary accessors/mutators =head1 SYNOPSIS $h = Hash::AsObject->new; $h->foo(123); print $h->foo; # prints 123 print $h->{'foo'}; # prints 123 $h->{'bar'}{'baz'} = 456; print $h->bar->baz; # prints 456 =head1 DESCRIPTION A Hash::AsObject is a blessed hash that provides read-write access to its elements using accessors. (Actually, they're both accessors and mutators.) It's designed to act as much like a plain hash as possible; this means, for example, that you can use methods like C to get or set hash elements with that name. See below for more information. =head1 METHODS The whole point of this module is to provide arbitrary methods. For the most part, these are defined at runtime by a specially written C function. In order to behave properly in all cases, however, a number of special methods and functions must be supported. Some of these are defined while others are simply emulated in AUTOLOAD. =over 4 =item B $h = Hash::AsObject->new; $h = Hash::AsObject->new(\%some_hash); $h = Hash::AsObject->new(%some_other_hash); Create a new L. If called as an instance method, this accesses a hash element 'new': $h->{'new'} = 123; $h->new; # 123 $h->new(456); # 456 =item B This method cannot be used to access a hash element 'isa', because Hash::AsObject doesn't attempt to handle it specially. =item B Similarly, this can't be used to access a hash element 'can'. =item B $h->{'AUTOLOAD'} = 'abc'; $h->AUTOLOAD; # 'abc' $h->AUTOLOAD('xyz') # 'xyz' Hash::AsObject::AUTOLOAD recognizes when AUTOLOAD is begin called as an instance method, and treats this as an attempt to get or set the 'AUTOLOAD' hash element. =item B $h->{'DESTROY'} = []; $h->DESTROY; # [] $h->DESTROY({}) # {} C is called automatically by the Perl runtime when an object goes out of scope. A Hash::AsObject can't distinguish this from a call to access the element $h->{'DESTROY'}, and so it blithely gets (or sets) the hash's 'DESTROY' element; this isn't a problem, since the Perl interpreter discards any value that DESTROY returns when called automatically. =item B When called as a class method, this returns C<$Hash::AsObject::VERSION>; when called as an instance method, it gets or sets the hash element 'VERSION'; =item B Since L doesn't export any symbols, this method has no special significance and you can safely call it as a method to get or set an 'import' element. When called as a class method, nothing happens. =back The methods C and C are special, because they're defined in the C class that all packages automatically inherit from. Unfortunately, this means that you can't use L to access elements 'can' and 'isa'. =head1 CAVEATS No distinction is made between non-existent elements and those that are present but undefined. Furthermore, there's no way to delete an element without resorting to C<< delete $h->{'foo'} >>. Storing a hash directly into an element of a Hash::AsObject instance has the effect of blessing that hash into Hash::AsObject. For example, the following code: my $h = Hash::AsObject->new; my $foo = { 'bar' => 1, 'baz' => 2 }; print ref($foo), "\n"; $h->foo($foo); print ref($foo), "\n"; Produces the following output: HASH Hash::AsObject I could fix this, but then code like the following would throw an exception, because C<< $h->foo($foo) >> will return a plain hash reference, not an object: $h->foo($foo)->bar; Well, I can make C<< $h->foo($foo)->bar >> work, but then code like this won't have the desired effect: my $foo = { 'bar' => 123 }; $h->foo($foo); $h->foo->bar(456); print $foo->{'bar'}; # prints 123 print $h->foo->bar; # prints 456 I suppose I could fix I, but that's an awful lot of work for little apparent benefit. Let me know if you have any thoughts on this. =head1 BUGS Autovivification is probably not emulated correctly. The blessing of hashes stored in a Hash::AsObject might be considered a bug. Or a feature; it depends on your point of view. =head1 TO DO =over 4 =item * Add the capability to delete elements, perhaps like this: use Hash::AsObject 'deleter' => 'kill'; $h = Hash::AsObject->new({'one' => 1, 'two' => 2}); kill $h, 'one'; That might seem to violate the prohibition against exporting functions from object-oriented packages, but then technically it wouldn't be exporting it B anywhere since the function would be constructed by hand. Alternatively, it could work like this: use Hash::AsObject 'deleter' => 'kill'; $h = Hash::AsObject->new({'one' => 1, 'two' => 2}); $h->kill('one'); But, again, what if the hash contained an element named 'kill'? =item * Define multiple classes in C? For example, there could be one package for read-only access to a hash, one for hashes that throw exceptions when accessors for non-existent keys are called, etc. But this is hard to do fully without (a) altering the underlying hash, or (b) defining methods besides AUTOLOAD. Hmmm... =back =head1 VERSION 0.06 =head1 AUTHOR Paul Hoffman =head1 CREDITS Andy Wardley for L, which was my inspiration. Writing template code like this: [% foo.bar.baz(qux) %] Made me yearn to write Perl code like this: foo->bar->baz($qux); =head1 COPYRIGHT Copyright 2003-2007 Paul M. Hoffman. All rights reserved. This program is free software; you can redistribute it and modify it under the same terms as Perl itself.