Beefy Boxes and Bandwidth Generously Provided by pair Networks
Do you know where your variables are?
 
PerlMonks  

Perl OO with Class::Struct

by VinsWorldcom (Prior)
on Dec 13, 2013 at 14:45 UTC ( [id://1067022]=perlquestion: print w/replies, xml ) Need Help??

VinsWorldcom has asked for the wisdom of the Perl Monks concerning the following question:

I know object-oriented programming in that I can use others' modules and I've written some of my own. I've read perlobj and perlootut. However, most of my objects have been simply nested Perl data structures (e.g., HoH, HoAoH, etc.). I have no problem creating or accessing those types of objects.

I'm keen to learn more about how to do this more "?correctly?". So I started looking into Class::Struct since it's simple, for building classes and in Perl core.

Take the following code borrowed heavily from the examples in the Class::Struct perldoc:

use strict; use warnings; package Cat; use Class::Struct; struct (name => '$'); 1; package Litter; use Class::Struct; struct (cats => '@'); 1; package main; my $cat1 = Cat->new(name=>'Garfield'); my $cat2 = Cat->new(name=>'Felix'); my $litter = Litter->new(cats => [$cat1, $cat2]); for (@{$litter->cats}) { print $_->name . "\n" }

This works swimmingly until $litter is created like so:

my $litter = Litter->new(cats => [$cat1, 1]);

Which of course produces an error ...

Can't call method "name" without a package or object reference at ...

... because "1" doesn't have a method - in fact, it's not even an "object". What I really 'think' I need to do is define the Litter object like so:

package Litter; use Class::Struct; struct (cats => '@Cat'); 1;

Meaning the 'cats' method should contain an array of 'Cat' objects, not just an array of anything. The documentation for Class::Struct leads me to believe I can't do it.

Questions:

  1. Is the 'workaround' to override the accessor for 'cats' as shown in Example 2 of the Class::Struct perldoc to check that the value passed is an object of type 'Cat'?

  2. Sticking with Class::Struct, is there a 'better' way to define the data structure / classes / objects I'm looking for (Litter 'cats' = array of 'Cat') such that I avoid the '@Cat' definition which isn't legal?

  3. Have I already 'outgrown' the usefulness of Class::Struct and simply that it's in Perl core is not the best reason to use it?

Replies are listed 'Best First'.
Re: Perl OO with Class::Struct
by tobyink (Canon) on Dec 13, 2013 at 16:21 UTC

    Yes, if you need proper validation, you've outgrown Class::Struct. Here are a few options...

    Moose

    use strict; use warnings; { package Cat; use Moose; has name => ( is => 'ro', isa => 'Str', ); __PACKAGE__->meta->make_immutable; } { package Litter; use Moose; has cats => ( is => 'ro', isa => 'ArrayRef[Cat]', ); __PACKAGE__->meta->make_immutable; } my $cat1 = Cat->new(name=>'Garfield'); my $cat2 = Cat->new(name=>'Felix'); my $litter = Litter->new(cats => [$cat1, $cat2, 1]);

    Mouse

    (Same as Moose; just use Mouse instead of use Moose.)

    Moo

    use strict; use warnings; { package Cat; use Moo; use Types::Standard -types; has name => ( is => 'ro', isa => Str, ); } { package Litter; use Moo; use Types::Standard -types; has cats => ( is => 'ro', isa => ArrayRef[InstanceOf['Cat']], ); } my $cat1 = Cat->new(name=>'Garfield'); my $cat2 = Cat->new(name=>'Felix'); my $litter = Litter->new(cats => [$cat1, $cat2, 1]);

    Moops

    use Moops; class Cat :ro { has name => (isa => Str); } class Litter :ro { has cats => (isa => ArrayRef[InstanceOf['Cat']]); } my $cat1 = Cat->new(name=>'Garfield'); my $cat2 = Cat->new(name=>'Felix'); my $litter = Litter->new(cats => [$cat1, $cat2, 1]);

    MooX::Struct

    use strict; use warnings; use Types::Standard -types; use MooX::Struct Cat => ['$name']; use MooX::Struct Litter => ['@cats' => [ isa => ArrayRef[InstanceOf[Ca +t]] ]]; my $cat1 = Cat->new(name=>'Garfield'); my $cat2 = Cat->new(name=>'Felix'); my $litter = Litter->new(cats => [$cat1, $cat2, 1]);

    MooseX::Struct

    The release of Moose 2.02 broke MooseX::Struct, and nobody's been bothered enough to fix it. :-( However, this is how it should work according to the documentation:

    use strict; use warnings; use MooseX::Struct; immutable struct Cat => ( name => { isa => 'Str' }, ); immutable struct Litter => ( cats => { isa => 'ArrayRef[Cat]' }, ); my $cat1 = Cat->new(name=>'Garfield'); my $cat2 = Cat->new(name=>'Felix'); my $litter = Litter->new(cats => [$cat1, $cat2, 1]);
    use Moops; class Cow :rw { has name => (default => 'Ermintrude') }; say Cow->new->name
Re: Perl OO with Class::Struct
by davido (Cardinal) on Dec 13, 2013 at 16:09 UTC

    I would suggest that the answer to #3 is:

    Whether or not you've outgrown Class::Struct, in 2013 there is very little reason to use it. If you choose to use an OO helper (rather than writing OO by hand), and you want to stay light-weight and fast, use Moo. It's true that it is not core. But it is fast, minimal, and actively developed and used. And it's largely compatible with Moose classes.

    With Moo, your example code above would look like this:

    package Cat; use Moo; use MooX::Types::MooseLike::Base qw(:all); has name => ( is => 'ro', isa => Str ); 1; package Litter; use Moo; use MooX::Types::MooseLike::Base qw(:all); has cats => ( is => 'ro', isa => ArrayRef ); 1; package main; my $cat1 = Cat->new(name=>'Garfield'); my $cat2 = Cat->new(name=>'Felix'); my $litter = Litter->new(cats => [$cat1, $cat2]); for (@{$litter->cats}) { print $_->name . "\n" }

    ...but in this example Moo is doing more: It's assuring that the accessors "name" and "cats" can't be modified after the object has been instantiated. It's also assuring that "name" is a string.

    But one real advantage is that instead of isa => ArrayRef, you could say:

    isa => sub { my $param = shift; die "<<$param>> must be an array ref" unless ref $param eq 'ARRAY'; foreach my $element ( @{$param} ) { die "<<$element>> isn't a Cat object" unless ref $element eq 'Cat'; } }

    Now your object instantiation verifies that all of the elements passed by Litter->new( cats => [ ... ] ) are Cats objects. You could even get more "general" by skipping the "ref $element eq 'Cat'" test, and instead allowing any sufficiently Cat-like object to pass:

    die "<<$element>> must have a name attribute" unless $element->can('name');

    With Moo you could also define a BUILDARGS subroutine that silently drops any elements from cats => [...] that don't have a "name" attribute.... if you want that behavior instead.

    Now, going back to the MooX::Types::MooseLike::Base example, even these constructs are possible:

    isa => ArrayRef[ InstanceOf['Cat'] ]

    And if you want your Litter class to accept any object that has a name attribute (in other words, a litter of bobcats would be fine too, as long as they have a name attribute), you can...

    isa => ArrayRef[ HasMethods['name'] ]

    Which provides clear semantics and avoids complexity. Moo makes this sort of thing pretty simple. Moose, being Moo's big uncle also has similar syntactic sugar. But Moo is so light-weight it's a great fit for small projects.

    Update: So putting it all together, here's your original code with the Moo and MooX::Types::MooseLike::Base syntax:

    package Cat; use Moo; use MooX::Types::MooseLike::Base 'Str'; has name => ( is => 'ro', isa => Str ); 1; package Litter; use Moo; use MooX::Types::MooseLike::Base qw( ArrayRef HasMethods ); has cats => ( is => 'ro', isa => ArrayRef [ HasMethods ['name'] ] ); 1; package main; my @cat_names = qw( Garfield Felix ); my $litter = Litter->new( cats => [ map { Cat->new( name => $_ ) } @cat_names ] ); print $_->name . "\n" for @{ $litter->cats };

    Updated: Removed quotes from isa => checks since Moo requires a subref, and since that's what MooX::Types::MooseLike::Base provides.


    Dave

      Actually, isa => 'ArrayRef' doesn't work in Moo at all. (Unless you also use MooX::late which is my Moose/Moo compatibility shim.)

      In Moo, isa needs to be a coderef, or an object overloading &{}.

      use Moops; class Cow :rw { has name => (default => 'Ermintrude') }; say Cow->new->name

        You're right; I should have said, "isa => ArrayRef", which is compatible with Moo when used with MooX::Types::MooseLike::Base.

        Personally I always just use sub { ... } so that I can take finer control.

        I'll update my post to remove the quotes from the isa check.


        Dave

Re: Perl OO with Class::Struct
by educated_foo (Vicar) on Dec 13, 2013 at 15:30 UTC
    1. Yes. 2. No. 3. Not necessarily.

    Class::Struct is designed for creating C-like "structs," i.e. collections of data. It doesn't support validation and such, though you can add that if you want. There are lots of fancy things on CPAN that try to help you with validation, if you need that.

Re: Perl OO with Class::Struct
by dbuckhal (Chaplain) on Dec 17, 2013 at 07:43 UTC
    Just for fun... And I'm more of a dog fan, too. :)
    #!/usr/bin/perl use strict; use warnings; { package Dog; sub new { my ( $caller, $dog ) = @_; my $class = ref($caller) || $caller; my $dogs = { NAME => $dog, }; bless $dogs, $class; } sub name { my $self = shift; ( @_ ) ? $self->{NAME} = shift : return $self->{NAME}; } } { package Litter; sub new { my ( $caller, $list ) = @_; die "not an DOG ref" unless ref($list->[0]) eq "Dog"; my $class = ref($caller) || $caller; my $litter = { DOGS => $list, }; bless $litter, $class; } sub dogs { my $self = shift; ( @_ ) ? $self->{DOGS} = shift : return $self->{DOGS}; } } my $dog1 = Dog->new('Strider'); my $dog2 = Dog->new('Bella'); my $litter = Litter->new([$dog1, $dog2]); for ( @{$litter->dogs} ) { print $_->name . "\n" }

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://1067022]
Approved by Corion
Front-paged by boftx
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others browsing the Monastery: (5)
As of 2024-03-19 10:51 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found