4

I'm pretty new to perl, and I'm getting stuck on a homework problem. I have an object with a class variable that counts the number of instances created. Then I have a subclass with an instance variable.

My first question is, how do I make the class variable hidden from the user? I tried using closures but couldn't figure out how to make inheritance work with that. And the fact that it's a class variable made it worse because the code that increments it executed twice and it said I had two instances when I had one. Not exactly sure why it happened but it makes sense. I tried using scalars but the variable again wasn't incrementing correctly. Haven't tried "inside-out objects" yet and I'm not sure I want to, it seems way over my head. I'm getting the feeling that encapsulating class variables is different than encapsulating instance variables, but I can't find anything that explains how to do it.

My second questions is, as I mentioned, I can't get encapsulation to work with inheritance. With closures when you call the super constructor from the subclass you get a reference to the subroutine right, so there's no way (that I know of) to add the instance variables to that.

Here's my base class:

#!/usr/bin/perl -w
use strict;
package Base;

my $count = 1;

sub new {
    my $class = shift;
    my $self = {
        _Count => $count # not hidden
    };
    $count++; # increment count
    bless $self, $class;
    return $self;
}

sub Count { # getter
    my $self = shift;
    return $self->{_Count};
}
1;

Here's my subclass:

#!/usr/bin/perl -w
use strict;
package Sub;
use Base;
our @ISA = qw(Base);

sub new {
    my $class = shift;
    my $self = $class->SUPER::New();
    $self->{_Name} = undef; # not hidden
    return $self;
}

sub Name { #getter/setter
    my($self, $name) = @_;
    $self->{_Name} = $name if defined($name);
    return $self->{_Name};
}
1;
maus
  • 121
  • 1
  • 10
  • 3
    I think we need some clarity on what you mean by "encapsulation". If you mean the fact that the contents of `$self` are accessible to anyone who wants to look, that's kind of part of the territory. Hash-based Perl objects don't have private data members as other languages might understand them, and trying to produce those features in Perl is unlikely to end pleasantly. – darch Dec 07 '12 at 21:06
  • Yes, I mean that I want the properties of the object to not be accessible outside of the class. It most definitely isn't pleasant, I've been studying closures and stuff for 2 days now and still don't understand them. – maus Dec 07 '12 at 21:21
  • I suppose in that case you have to build your own closure-based approach to OO. It's an odd requirement, especially since Perl's experience with inside-out objects was so harrowing, but it can be done. – darch Dec 07 '12 at 21:26
  • 2
    More to the point, are you sure that denying access (rather than just documenting them as private) is a requirement? There's a couple of ways to do it, inside-out objects being the more straightforward, but they are complicated to reimplement and not necessary for most real programming tasks. – darch Dec 07 '12 at 21:34
  • @darch Yes. Do inside-out objects work well with inheritance? In the subclass, is it easy to add new properties? – maus Dec 07 '12 at 22:40
  • Possible duplicate of [How can I set a static variable that can be accessed by all subclasses of the same base class (Perl/Moose)?](https://stackoverflow.com/questions/7587157/how-can-i-set-a-static-variable-that-can-be-accessed-by-all-subclasses-of-the-sa) – Evan Carroll Nov 10 '17 at 07:07

5 Answers5

4

If you are using bare Perl 5 (rather than employing an OO framework), the usual way to do class variables is as a lexical visible only to the accessor:

{
    my $count = 0;

    sub Count {
        my ($self, $new_count) = @_;

        if (defined $new_count) { # NB only works if undef is not a legit value
            $count = $new_count;
        }

        return $count;
     }
}

$count is only visible in the enclosing block; not even other methods on the same class can see it. But anyone can manipulate it with either $base_obj->Count or Base->Count, and any such manipulation will affect the shared variable.

You can also employ closure to provide really-hidden instance variables. This is not worth doing unless you are fulfilling the arbitrary rules of a homework assignment.

package Base;

sub new {
    my ($class, $name) = @_;
    die "Need name!" unless defined $name;

    my $age;

    return bless sub {
        my ($attribute, @args) = @_;

        if ($attribute eq 'name') {
            if (@args) {
                die "Attempt to set read-only attribute!";
            }
            return $name;
        }

        if ($attribute eq 'age') {
            if (@args) {
                 ($age) = @args;
            }
            return $age;
        }

        die "Unknown attribute $attribute";
    } => $class;
}

sub name {
    my ($self, @args) = @_;

    return $self->(name => @args);
}

sub age {
    my ($self, @args) = @_;

    return $self->(age => @args);
}

What happens here is that the blessed sub returned by new closes over two lexicals, $name and $age. When new returns, those lexicals go out of scope and the only way to access them from that point forward is through the closure. The closure can inspect its arguments to permit or deny access to the values it holds. So long as it never returns a reference, it can be sure that it has the only direct access to those variables.

This works with inheritance, too, without too much added subtlety:

package Derived;
use base 'Base';
sub new {
    my ($class, $name, $color) = @_;

    my $base_instance = $class->SUPER::new($name);

    return bless sub {
        my ($attribute, @args) = @_;

        if ($attribute eq 'color') {
            if (@args) {
                 ($color) = @args;
            }
            return $color;
        }

        # base class handles anything we don't, possibly by dying
        return $base_instance->($attribute, @args);
    } => $class;
}

This emulates what languages with distinct storage for base- and derived-class instance data do, either handling the request locally or passing it on to the base class instance, which has been added to the closure. Deeper inheritance trees will result in closures that close over closures that close over closures, each of them optionally also closing over instance variables needed by that particular class.

This is a pretty big mess to produce and really hard to inspect and debug, which is why I'm going to emphasize one more time that you should never do this. But it is very useful to understand, to which end I refer you to SICP.

darch
  • 4,200
  • 1
  • 20
  • 23
3

As a module-local my variable, $count is already hidden from users of the module/class. It appears as if you're using instance variable _Count as a "current ID" type variable, so that each object (instance) created gets a new ID starting from 1. (If instead it is meant to track the number of active instances, then you need to decrement it in DESTROY and there's no need to store a copy in the object.) If your test code is only creating one instance then its Count() method should return 1 but $count will be 2, since it started as 1 and was incremented after storing the old value in the object.

It is typical in perl to store instance variables in the $self hash as you are doing, without hiding them, although sometimes a prefix is used to avoid collisions. They are protected more by convention (it's not safe to rely on implementation details because they might change) than language features.

Take a look at the Moose suite of modules if you want higher-level control over perl classes.

dbrobins
  • 499
  • 1
  • 3
  • 9
3

In Perl, fields are not usually hidden by enforcing this through the semantics of the language, but rather through a contract in the form of documentation. However, fields can be hidden through the use of closures. It is also worth noting that Perl does not semantically differentiate between class methods and instance methods.

One of the standard ways to implement objects is a blessed hash, like you do. This hash contains all instance variables / fields. It is customary to start "private" fields with an underscore. Usually, the contract (the documentation) will not state how these fields are stored, but will require the user of the class to go through various method calls.

Class variables should not be stored with the instance. It is better to use global variables, or lexical variables. In the code you gave, $count is just a counter, but you never access it as a class variable. Instead, you assign each instance an unique ID. To use it as a class variable, provide an appropriate accessor (I stripped out unneccessary stuff like returns):

{
  package Base;

  my $count = 0;

  sub new {
    my ($class) = @_;
    my $self = {
      ID => $count++,
    };
    bless $self, $class;
  }

  sub Count  { $count }
  sub ID     { my ($self) = @_; $self->{ID} }
  sub report { my ($self) = @_; "I am the Base object ".($self->ID)."." }
}

=head1 Base

A generic base class

=head2 Base->Count

Return the object count.

=head2 $base->ID

Give the unique ID of this object.

=head2 $base->report

Returns a string containing a short description.

=cut

The subclass has no business meddling with the count. This is enforced by the scope of the variable $count above, denoted via the outer curly braces. The subs are closures over this variable.

{
  package Sub;
  use parent -norequire, qw(Base); # remove `-norequire` if Base in different file

  sub new {
    my ($class) = @_;
    my $self = $class->SUPER::new;
    $self->{Name} = undef;
    $self;
  }

  sub Name :lvalue {
    my ($self) = @_;
    $self->{Name};
  }
  sub report {
    my ($self) = @_;
    "I am the Sub object ".($self->ID)." called ".($self->Name).".";
  }
}

=head1 Sub

A generic subclass. It subclasses Base.

=head2 $sub->Name [= SCALAR]

Gets or sets the name of $sub.

    my $oldname = $sub->Name;
    $sub->name = "new name";

=cut

As you can see, the Sub constructor calls the Base initializer, then adds a new field. It has no class methods or class variables. The class has no access to the $count variable, except via the accessor class method. The contract is stated via POD documentation.

(In the Name method, I use an :lvalue annotation. This allows me to simply assign to the appropriate field in the object. However, this disallows argument checking.)

The testcase

my $base1 = Base->new; my $base2 = Base->new;
print "There are now " . Base->Count . " Base objects\n";
my $sub1 = Sub->new;   my $sub2 = Sub->new;
print "There are now " . Base->Count . " Base objects\n";

$sub2->Name = "Fred";

print $_->report . "\n" for ($base1, $sub1, $base2, $sub2);

prints

There are now 2 Base objects
There are now 4 Base objects
I am the Base object 0.
I am the Sub object 2 called .
I am the Base object 1.
I am the Sub object 3 called Fred.

Beautiful, isn't it? (Except $sub1, that object is missing its name.)

The documentation can be viewed with perldoc -F FILENAME, and would output something like

Base
       A generic base class

   Base->Count
       Return the object count.

   $base->ID
       Give the unique ID of this object.

   $base->report
       Returns a string containing a short description.

Sub
       A generic subclass. It subclasses Base.

   $sub->Name [= SCALAR]
       Gets or sets the name of $sub.

           my $oldname = $sub->Name;
           $sub->name = "new name";

only typeset more nicely, if you are on a *nix system.

Tested under v5.12.4.

Edit: Inside-out objects

While inside-out objects provide better encapulation, they are a bad idea: difficult to understand, difficult to debug, and difficult to inherit they provide more problems than solutions.

{
   package Base;
   my $count = 0;
   sub new    { bless \do{my $o = $count++}, shift }
   sub Count  { $count }
   sub ID     { ${+shift} }
   sub report { my ($self) = @_; "I am the Base object ".($self->ID)."." }
}
{
   package Sub;
   my @_obj = ();
   my $count = 0;
   sub new    {
     my ($class) = @_;
     $count++;
     $_obj[$count - 1] = +{
       parent => Base->new(),
       Name   => undef,
     };
     bless \do{my $o = $count - 1}, shift;
   }
   sub Name :lvalue { $_obj[${+shift}]{Name} }
   sub AUTOLOAD {
     my $self = shift;
     my $package = __PACKAGE__ . "::";
     (my $meth = $AUTOLOAD) =~ s/^$package//;
     $_obj[$$self]{parent}->$meth(@_)
   }
   sub report {
     my ($self) = @_;
     "I am the Sub object ".($self->ID)." called ".($self->Name).".";
   }
}

This implementation has the exact same interface, and completes the test case with the same output. This solution is far from optimal, supports only single inheritance, does some intermediate stuff (autoloading, dynamic method calls), but it does suprisingly work. Each object is actually just a reference to an ID that can be used to look up the actual hash containing the fields. The array holding the hashes is not accessible from the outside. The Base class has no fields, therefore no object array had to be created.

Edit2: Objects as coderefs

Yet another bad idea, but it is fun to code:

{
    package Base;
    my $count = 0;
    sub new {
        my ($class) = @_;
        my $id = $count++;
        bless sub {
            my ($field) = @_;
            die "Undefined field name" unless defined $field;
            if ($field eq "ID") { return $id }
            else                { die "Unrecognised name $field" }
        }, $class;
    }
    sub Count  { $count }
    sub ID     { my ($self) = @_; $self->("ID") }
    sub report { my ($self) = @_; "I am the Base object " . $self->ID . "." }
}
{
    package Sub;
    use parent -norequire, qw(Base);
    sub new {
        my ($class) = @_;
        my $name = undef;
        my $super = $class->SUPER::new;
        bless sub {
            my ($field, $val ) = @_;
            die "Undefined field name" unless defined $field;
            if ($field eq "Name") { defined $val ? $name = $val : $name }
            else                  { $super->(@_) }
        }, $class;
    }
    sub Name { my $self = shift; $self->("Name", @_) }
    sub report {
        my ($self) = @_;
        "I am the Sub object ".($self->ID)." called ".($self->Name).".";
    }
}

The test case has to be adapted to $sub2->Name("Fred"), and the documentation updated accordingly, as we cannot use an lvalue annotation here safely.

amon
  • 57,091
  • 2
  • 89
  • 149
  • Thank you for your answer, it sounds great but I still want self to be inaccessible from outside the class, instead of just document it as private. – maus Dec 07 '12 at 22:54
  • @maus I added a horrid example for one possible implementation of inside-out objects. The contract of the class wasn't changed, and the test cases run as before. However, the code is fragile, bad, and wouldn't be used in production. Moose is the way to go, probably. – amon Dec 07 '12 at 23:40
3

To quote perldoc perlmodlib, "Perl does not enforce private and public parts of its modules as you may have been used to in other languages like C++, Ada, or Modula-17. Perl doesn't have an infatuation with enforced privacy. It would prefer that you stayed out of its living room because you weren't invited, not because it has a shotgun."

The standard convention in Perl is to put everything into the $self hash and use an underscore prefix to indicate which items should be treated as private... and then trust users of the class to respect that indication. The same convention is also applied to methods. If you use one of my modules and you choose to peek under the covers and modify the contents of $self directly or call $obj->_some_private_method, then you're going off into the woods and may break something, or what works fine in this version may break when you upgrade to the next version; if that happens, you get to keep both pieces.

If you're going to insist on making data inaccessible to anyone outside the class itself, there are ways to do that, but a) they add complexity which is, in almost all cases, unnecessary and b) as you've already seen, they have a tendency to make inheritance a lot more of a hassle to work with.

My question to you, then, is what are you actually attempting to accomplish and why do you feel the need to make your object data Sooper-Sekret and completely inaccessible? What benefit will you gain by doing so which isn't provided by simply marking things that you think should be treated as private, then trusting others to leave them alone (unless they have good reason to do otherwise)?

Dave Sherohman
  • 45,363
  • 14
  • 64
  • 102
  • Like I mentioned in my question, it's a homework assignment. I think my instructor went a little overboard with the requirements. – maus Dec 07 '12 at 22:58
2

First, I'm not sure exactly what you mean by "hidden from the user", but it looks like you may be looking for package scoped variables (our) vs. instance scoped.

package MyBaseClass;
use warnings;
use strict;

our $counter = 0;
sub new { 
    my $class = shift;
    $counter++;
    return bless {}, $class;
}
sub howManyInstances {
    return $counter;
}
1;

On your second question, I'm not sure what closures have to do with inheritance. Here's a simple subclass:

package MySubClass;
use warnings;
use strict;
use parent 'MyBaseClass';  # use parent schema, don't mess with @ISA
sub new {
    my $class = shift;
    my $self = $class->SUPER::new(@_);
    $self->{_name} = undef;  
    return $self;
}
# Your setter/getter looks ok as is, though lowercase is tradional for methods/subs
1;

Now, if this were real code you would not do it like this - you would use Moo or Moose.

RickF
  • 1,812
  • 13
  • 13
  • I want the properties to be inaccessible from outside the class. I was trying to use closures for this by blessing a subroutine, but in the subclass I don't know how to add new properties after calling the super constructor, since the return value isn't a hash. – maus Dec 07 '12 at 22:44
  • To make $counter inaccessible, just replace `our` with `my`. – RickF Dec 10 '12 at 14:51
  • Unless your assignment explicitly requires using closures or subclassing a blessed coderef, just don't. You're probably barking up the wrong tree. – RickF Dec 10 '12 at 15:07