Perl Cookbook

Perl CookbookSearch this book
Previous: 13.4. Managing Class DataChapter 13
Classes, Objects, and Ties
Next: 13.6. Cloning Objects
 

13.5. Using Classes as Structs

Problem

You're used to structured data types more complex than Perl's arrays and hashes, such as C's structs and Pascal's records. You've heard that Perl's classes are comparable, but you aren't an object-oriented programmer.

Solution

Use the standard Class::Struct module to declare C-like structures:

use Class::Struct;          # load struct-building module

struct Person => {          # create a definition for a "Person"
    name   => '$',          #    name field is a scalar
    age    => '$',          #    age field is also a scalar
    peers  => '@',          #    but peers field is an array (reference)
};

my $p = Person->new();      # allocate an empty Person struct

$p->name("Jason Smythe");                   # set its name field
$p->age(13);                                # set its age field
$p->peers( ["Wilbur", "Ralph", "Fred" ] );  # set its peers field

# or this way:
@{$p->peers} = ("Wilbur", "Ralph", "Fred");

# fetch various values, including the zeroth friend
printf "At age %d, %s's first friend is %s.\n",
    $p->age, $p->name, $p->peers(0);

Discussion

The Class::Struct::struct function builds struct-like classes on the fly. It creates a class of the name given in the first argument, and gives the class a constructor named new and per-field accessor methods.

In the structure layout definition, the keys are the names of the fields and the values are the data type. This type can be one of the three base types, '$' for scalars, '@' for arrays, and '%' for hashes. Each accessor method can be called without arguments to fetch the current value, or with an argument to set the value. In the case of a field whose type is an array or hash, a zero-argument method call returns a reference to the entire array or hash, a one-argument call retrieves the value at that subscript,[1] and a two-argument call sets the value at that subscript.

[1] Unless it's a reference, in which case it uses that as the new aggregate, with type checking.

The type can even be the name of another named structure  - or any class, for that matter  - which provides a constructor named new.

use Class::Struct;

struct Person => {name => '$',      age  => '$'};
struct Family => {head => 'Person', address => '$', members => '@'};

$folks  = Family->new();
$dad    = $folks->head;
$dad->name("John");
$dad->age(34);

printf("%s's age is %d\n", $folks->head->name, $folks->head->age);

If you'd like to impose more parameter checking on the fields' values, supply your own version for the accessor method to override the default version. Let's say you wanted to make sure the age value contains only digits, and that it falls within reasonably human age requirements. Here's how that function might be coded:

sub Person::age {
    use Carp;
    my ($self, $age) = @_;
    if    (@_  > 2) {  confess "too many arguments" } 
    elsif (@_ == 1) {  return $struct->{'age'}      }
    elsif (@_ == 2) {
        carp "age `$age' isn't numeric"   if $age !~ /^\d+/;
        carp "age `$age' is unreasonable" if $age > 150;
        $self->{'age'} = $age;
    } 
} 

If you want to provide warnings only when the -w command-line flag is used, check the $^W variable:

if ($^W) { 
    carp "age `$age' isn't numeric"   if $age !~ /^\d+/;
    carp "age `$age' is unreasonable" if $age > 150;
}

If you want to complain if -w is set, but to raise an exception if the user doesn't ask for warnings, do something like the following. Don't be confused by the pointer arrow; it's an indirect function call, not a method call.

my $gripe = $^W ? \&carp : \&croak;
$gripe->("age `$age' isn't numeric")   if $age !~ /^\d+/;
$gripe->("age `$age' is unreasonable") if $age > 150;

Internally, the class is implemented using a hash, as most classes are. This makes your code easy to debug and manipulate. Consider the effect of printing out a structure in the debugger, for example. But the Class::Struct module also supports an array representation. Just specify the fields within square brackets instead of curly ones:

struct Family => [head => 'Person', address => '$', members => '@'];

Empirical evidence suggests that selecting the array representation instead of a hash trims between 10% and 50% off the memory consumption of your objects, and up to 33% of the access time. The cost is less informative debugging information and more mental overhead when writing override functions, such as Person::age above. Choosing an array representation for the object would make it difficult to use inheritance. That's not an issue here, because C-style structures employ the much more easily understood notion of aggregation instead.

The use fields pragma in the 5.005 release of Perl provides the speed and space arrays with the expressiveness of hashes, and adds compile-time checking of an object's field names.

If all the fields are the same type, rather than writing it out this way:

struct Card => { 
    name    => '$',
    color   => '$',
    cost    => '$',
    type    => '$',
    release => '$',
    text    => '$',
};

you could use a map to shorten it:

struct Card => map { $_ => '$' } qw(name color cost type release text);

Or, if you're a C programmer who prefers to precede the field name with its type, rather than vice-versa, just reverse the order:

struct hostent => { reverse qw{
    $ name
    @ aliases
    $ addrtype
    $ length
    @ addr_list
}};

You can even make aliases, in the (dubious) spirit of #define, that allow the same field to be accessed under multiple aliases. In C you can say:

#define h_type h_addrtype
#define h_addr h_addr_list[0]

In Perl, you might try this:

# make (hostent object)->type() same as (hostent object)->addrtype()
*hostent::type = \&hostent::addrtype;

# make (hostenv object)->addr() same as (hostenv object)->addr_list(0)
sub hostent::addr { shift->addr_list(0,@_) }

As you see, you can add methods to a class  - or functions to a package  - simply by declaring a subroutine in the right namespace. You don't have to be in the file defining the class, subclass it, or do anything fancy and complicated. It would be much better to subclass it, however:

package Extra::hostent;
use Net::hostent;
@ISA = qw(hostent);
sub addr { shift->addr_list(0,@_) } 
1;

That one's already available in the standard Net::hostent class, so you needn't bother. Check out that module's source code as a form of inspirational reading. We can't be held responsible for what it inspires you to do, though.

See Also

perltoot (1), perlobj (1), and perlbot (1); the documentation for the standard Class::Struct module; the source code for the standard Net::hostent module; the documentation for the Alias module from CPAN; Recipe 13.3


Previous: 13.4. Managing Class DataPerl CookbookNext: 13.6. Cloning Objects
13.4. Managing Class DataBook Index13.6. Cloning Objects