Perl OOP: using instance or class methods in an interchangeable way

Almost twelve years ago I got a job interview and the interviewer asked me why I liked Perl. Back in those days, my Perl knowledge was really limited, but I answered without any doubt saying that Perl was fantastic because it allows, pretty much always, to program in an OOP manner or a procedural manner and the same library can be used in both ways depending on the client/user preferences.
This short article, tries to clearify my feelings and that old sentence. The idea is that a method (instance sub) can be used in an OOP way and as a class method. For those who are familiar with Java, it means that the same sub can act as a method and a static function.

A simple example

Let’s build a very simple class, named Person, that stores the name and the username of a person. Then implement a method, named equals that checks if two instances of a Person are the same. To keep things simple, assume that two Persons are the same if the username matches, without any regard to the name.
In the following there is a simple implementation that uses Moo as a shortcut to declarative classes. Please note that it does not matter how you build your classes: for the purpose of this article the method usage remains the same.

package Person;

use Moo;

has name       => ( is => 'ro' );
has username   => ( is => 'ro' );


sub equals {
    my ( $self, $comparing ) = @_;

    return undef if ( ! $self );  # only for class method !
    return undef if ( ! $comparing || ! $comparing->isa( ref $self ) );

    $self->username eq $comparing->username;
}



The equals method is the one that can behave at the same time as an instance method or a class subroutine. The idea is that the method accepts two object references, $self (the usual myself) and the $comparing reference that is the object to compare against. The return value of the method, the last line, is the comparison of the username field.
Note that the method will return undef (false) in the case the $comparing object is not an instance of the class itself. Similarly, it will return undef in the case $self is empty, which will never happen in the OOP style since -> requires a valid object to call a method on. However, in the case the method is used as a class sub, it can happen that even $self is not an object.
Having defined the method as above, it is possible to use it as:
  • $a->equals( $b ) where $a will become $self and $b will become $comparing. This is the OOP method call;
  • Person::equals( $a, $b ) where $a will become $self and $b will become $comparing. This is the class function call, and in this case $a could be empty.

Why is this supposed to work?
In the -> call, the arrow operator passes its left argument as first parameter to the sub, i.e., to $self. This is a well known OOP way of doing method calls and is used also in Java and C++ to define this. In the :: call, Perl invokes a sub belonging to a package without adding any new parameter in front of the argument list, therefore providing only the two explicit arguments.

Example at work

It is very simple to prove how the above class works:

package main;


my $luca = Person->new( name      => 'Luca Ferrari',
			username  => 'fluca1978' );

my $personA = Person->new( name     => 'Luca F.',
			   username => 'fluca1978' );

my $personB = Person->new( name     => 'Sofia',
			   username => 'blackcat' );

# OOP usage
say $luca->name    , " vs ", $personA->name, " = " , ( $luca->equals( $personA ) ? 'SAME!' : 'different people' );
say $luca->name    , " vs ", $personB->name, " = " , ( $luca->equals( $personB ) ? 'SAME!' : 'different people' );
say $personB->name , " vs ", $personA->name, " = " , ( $personB->equals( $personA ) ? 'SAME!' : 'different people' );

# class usage
say $luca->name    , " vs ", $personA->name, " = " , ( Person::equals( $luca, $personA ) ? 'SAME!' : 'different people' );
say $luca->name    , " vs ", $personB->name, " = " , ( Person::equals( $luca, $personB ) ? 'SAME!' : 'different people' );
say $personB->name , " vs ", $personA->name, " = " , ( Person::equals( $personA, $personB ) ? 'SAME!' : 'different people' );



The result of the above application is:

Luca Ferrari vs Luca F. = SAME!
Luca Ferrari vs Sofia = different people
Sofia vs Luca F. = different people

Luca Ferrari vs Luca F. = SAME!
Luca Ferrari vs Sofia = different people
Sofia vs Luca F. = different people



The same class in another language (Java)

One advantage of Perl is that, as you will see, the code to write is far less. Assume we want to implement the same class Person in Java:

public class Person {
   public String name;
   public String username;

   public boolean equals( Person comparing ) {
       return this.username.equals( comparing.username );
   }

   public static boolean equals( Person a, Person b ) {
       if ( a == null || b == null
	       || ! a instanceof Person || ! b instanceof Person )
	     return false;

      return a.equals( b );
   }
}



I’ve used two public members just to keep the code short. The important thing to note in this implementation is that we need two equals methods: one for the object and one for the class.

The operator overloading approach

There is more: it is possible to overload operators for a class, so that Perl will use the method when needed. For example, instead of $luca->equals( $person ) it is possible to write a more natural $luca eq $person.
WARNING: operator overloading is a powerful feature but require much more care about mixing different types and operators!
The class Person changes as follows:

package Person;

use overload
	'eq' => 'equals'
	;

use Moo;

has name       => ( is => 'ro' );
has username   => ( is => 'ro' );



sub equals {
    my ( $self, $comparing ) = @_;

    return 0 if ( ! defined $self );  # only for class method !
    return 0 if ( ! defined $comparing || ! $comparing->isa( ref $self ) );

    $self->username eq $comparing->username;
}



There are two main changes:
  • introduction of the overload module usage, that defines that the eq operator will be mapped onto the equals class method;
  • the equals method now checks explicitly about defined-ness of its argument, since overloading would require to define a way to check for boolean values (out of the scope of this article).

having changed the class, it is now possible to write the code as follows:

say $luca->name    , " vs ", $personA->name, " = " , ( $luca eq $personA  ? 'SAME!' : 'different people' );
say $luca->name    , " vs ", $personB->name, " = " , ( $luca eq $personB  ? 'SAME!' : 'different people' );
say $personB->name , " vs ", $personA->name, " = " , ( $personB eq $personA  ? 'SAME!' : 'different people' );



which produces the same exact result as before.
Just keep in mind that this approach can be risky if you start overloading too much operators and get clashes.

Conclusions

Perl is extremely flexible and helps developers to meet their own taste in using libraries and modules. While I believe that OOP is by far much more convenient than the subroutine approach, it does not mean that you cannot use both. And also, this helps reducing the number of code to write.

The article Perl OOP: using instance or class methods in an interchangeable way has been posted by Luca Ferrari on August 24, 2023

Tags: perl