Learning Perl - Overloading Operators


Learning Perl - Overloading Operators


In the last post we investigated prototype subroutines in Perl. In this post, we will look at operator overloading, which allows you to define how operators behave for objects of a class.

An operator in programming is a symbol or keyword that performs a specific operation on one or more operands (values or variables). There are many types of operators, such as arithmetic operators (like +, -, *, /) and comparison operators (like ==, !=, <, >).

In Perl, you can overload these operators for your own classes, allowing you to define custom behaviour when these operators are used with objects of that class. The following table can be used as a reference to operators that can be overloaded in Perl:

Operator Symbol Description
'+' Addition
'-' Subtraction
'*' Multiplication
'/' Division
'%' Modulus
'**' Exponentiation
'<<' Left bitshift
'>>' Right bitshift
'x' String/array repetition
'.' String concatenation
'<' Numeric less than
'<=' Numeric less or equal
'>' Numeric greater than
'>=' Numeric greater or equal
'==' Numeric equality
'!=' Numeric inequality
'<=>' Numeric comparison
'lt' String less than
'le' String less or equal
'gt' String greater than
'ge' String greater or equal
'eq' String equality
'ne' String inequality
'cmp' String comparison
'bool' Boolean context
'""' String context
'0+' Numeric context
'++' Increment
'--' Decrement
'abs' Absolute value
'neg' Negation
'not' Logical not
'~' Bitwise not
'atan2' Arctangent
'cos' Cosine
'sin' Sine
'exp' Exponential
'log' Logarithm
'sqrt' Square root
'${}' Dereference as scalar
'@{}' Dereference as array
'%{}' Dereference as hash
'&{}' Dereference as code
'*{}' Dereference as glob
'fallback' Fallback for unknown ops

All of these operators can be overloaded by defining methods in your class that correspond to the operator. For example, to overload the addition operator (+), you would define a method named add in your class. And then you can use the use overload pragma to associate that method with the + operator.

When overloading in Perl, the fallback option is important to set because it tells Perl what to do if an operator is used on your object but you haven’t provided an explicit overload for that operator. It will fallback to the default operator. If you do not set fallback then perl will throw an error when your object is used with an unimplemented operator.

Today we will create a new module that will represent a simple mathematical 3D vector. We will overload the addition, subtraction, multiplication, stringification operators for this example. Lets start by creating a new distribution called Math::Vector3D using Module::Starter:

module-starter --module="Math::Vector3D" --author="Your Name" --email="your email"
Enter fullscreen mode Exit fullscreen mode

This will create a new directory called Math-Vector3D with the basic structure of a Perl module. First we will add a new test file to test our module. We will start simple by just testing the instantiation of our object. Create a new file called t/01-vector3d.t with the following content.

use Test::More;
use_ok('Math::Vector3D');
ok(my $v = Math::Vector3D->new(1, 2, 3), 'Created a new vector');
isa_ok($v, 'Math::Vector3D', 'Object is of correct class');
eval {
    Math::Vector3D->new('a', 'b', 'c');
};
like($@, qr/Invalid vector component/, 'Invalid components raise an error');

done_testing();
Enter fullscreen mode Exit fullscreen mode

As you can see, we are testing the creation of a new vector object and checking that it is of the correct class. We also test that invalid components raise an error, as we only want our object to be instantiated with numbers.

Next, we will implement the new method in our module. Open the file lib/Math/Vector3D.pm and replace the function1 definition with the following code:

=head2 new

Constructor for the Math::Vector3D object.

    my $vector = Math::Vector3D->new(1, 2, 3);

=cut

sub new {
    my ($class, @vector) = @_;

    looks_like_number($_) or die "Invalid vector component: $_" for @vector;

    return bless { vector => \@vector }, $class;
}
Enter fullscreen mode Exit fullscreen mode

This new method takes a list of components for the vector, checks that they are all numbers using looks_like_number, and then blesses the array reference into the class. The looks_like_number function is not a built-in function, so we need to import it from the Scalar::Util module. The Scalar::Util module is one of many core modules that come bundled with Perl, so you don't need to install it separately. To use it, we will add the following line at the top of our file after the use warnings line:

use Scalar::Util qw(looks_like_number);
Enter fullscreen mode Exit fullscreen mode

Now we can run our test file to see if it passes:

prove -lv t/01-vector3d.t
Enter fullscreen mode Exit fullscreen mode

You should see output indicating that all tests passed. Next we will implement stringification for our vector object. This will allow us to convert our vector object to a string representation when we use it in a string context, such as when we print it. Open the test file and extend with these tests:

is($v->stringify, "(1, 2, 3)", 'Stringification method works');
is("$v", "(1, 2, 3)", 'Stringification works');
Enter fullscreen mode Exit fullscreen mode

As you can see we are testing the stringify method directly on the object, and also testing the stringification when we use the object in a string context. Now we will implement the stringify method in our module. Add the following code to lib/Math/Vector3D.pm first replacing the function2 definition:


=head2 stringify

Returns a string representation of the vector.

    my $string = $vector->stringify();

=cut

sub stringify {
    my ($self) = @_;
    return sprintf "(%s)", join(', ', @{$self->{vector}});
}

Enter fullscreen mode Exit fullscreen mode

And then add the overload declaration at the top of the file after the use Scalar::Util line:

use overload
    '""' => 'stringify',
    fallback => 1;
Enter fullscreen mode Exit fullscreen mode

This overload declaration tells Perl to use the stringify method when the object is used in a string context (like when interpolated in a double-quoted string). The fallback => 1 option allows Perl to fall back to the default behavior if the operator is not overloaded.

Now we can run our test file again to see if the stringification tests pass:

prove -lv t/01-vector3d.t
Enter fullscreen mode Exit fullscreen mode

You should see output indicating that all tests passed. Next, we will implement the addition operator (+) for our vector object. This will allow us to add two vectors together using the + operator. Open the test file and extend with these tests:

ok(my $v2 = Math::Vector3D->new(4, 5, 6));
is($v->add($v2), "(5, 7, 9)", 'Addition with another vector works');
is($v + $v2, "(5, 7, 9)", "Addition operator works");
Enter fullscreen mode Exit fullscreen mode

We create a new vector to be used in the addition tests, and then we test both the add method directly and the overloaded + operator. Now with our tests failing we will implement the add method in our module. Add the following code to lib/Math/Vector3D.pm.


=head2 add

Adds another vector to this vector.

    my $result = $vector->add(Math::Vector3D->new(4, 5, 6));

=cut

sub add {
    my ($self, $other) = @_;
    ref($self) eq 'Math::Vector3D' or die "Method called on non-Math::Vector3D object";
    ref($other) eq 'Math::Vector3D' or die "Argument must be a Math::Vector3D object";
    return Math::Vector3D->new(
        map { $self->{vector}[$_] + $other->{vector}[$_] } 0..2
    );
}

Enter fullscreen mode Exit fullscreen mode

This add method checks that both the object and the argument are of the correct class, and then returns a new Math::Vector3D object with the sum of the components. The map function is used to iterate over the indices of the vector components (0 to 2 for a 3D vector) and add the corresponding components of both vectors. When overloading numeric operators you also get a third argument which is the context in which the operator was called on the object. For example the third argument will be true if the operator was called before the object (5 + $obj) and false if it was called after the object ($obj + 5). It's important to understand the first argument $self will always be the object, the $other variable will be the operand/value, the third argument is whether $other comes before or after $obj. In all of our methods today we do not need to use this context but it is good to know it exists.

With the above in place before our tests will pass we need to add the overload declaration for the + operator. Add the following code replacing your existing overload declaration:

use overload
    '+' => 'add',
    '""' => 'stringify',
    fallback => 1;
Enter fullscreen mode Exit fullscreen mode

Now we can run our test file again to see if the addition tests pass:

prove -lv t/01-vector3d.t
Enter fullscreen mode Exit fullscreen mode

You should see output indicating that all tests passed. Next, we will implement the subtraction operator for our vector object. This will allow us to subtract one vector from another using the - operator. Open the test file and extend with these tests:

is($v->subtract($v2), "(-3, -3, -3)", 'Subtraction with another vector works');
is($v - $v2, "(-3, -3, -3)", "Subtraction operator works");
is($v2->subtract($v), "(3, 3, 3)", 'Subtraction with another vector works in reverse');
is($v2 - $v, "(3, 3, 3)", "Subtraction operator works in reverse");
Enter fullscreen mode Exit fullscreen mode

With the failing tests in place we will implement the subtract method in our module. Add the following code to lib/Math/Vector3D.pm:

=head2 subtract

Subtracts another vector from this vector.

    my $result = $vector->subtract(Math::Vector3D->new(4, 5, 6));

=cut

sub subtract {
    my ($self, $other) = @_;
    ref($self) eq 'Math::Vector3D' or die "Method called on non-Math::Vector3D object";
    ref($other) eq 'Math::Vector3D' or die "Argument must be a Math::Vector3D object";
    return Math::Vector3D->new(
        map { $self->{vector}[$_] - $other->{vector}[$_] } 0..2
    );
}
Enter fullscreen mode Exit fullscreen mode

This subtract method is similar to the add method, but it subtracts the components of the second vector from the first. The same checks for class and context are applied.

Now we will add the overload declaration for the - operator. Add the following code replacing your existing overload declaration:

use overload
    '+' => 'add',
    '-' => 'subtract',
    '""' => 'stringify',
    fallback => 1;
Enter fullscreen mode Exit fullscreen mode

Now run your tests again and all should pass. The next method we will implement is mutiply this will work slightly differently to the add and subtract we will allow normal multiplication between a vector and an number however when you multiple two vectors together we will calculate the Dot product of the vectors. The dot product (also called the scalar product) of two vectors is a single number obtained by multiplying their corresponding components and then adding those products together. Add the following tests to your test file:

is($v->multiply(2), "(2, 4, 6)", 'Multiplication with a number works');
is($v * 2, "(2, 4, 6)", "Multiplication operator with a number works");
is($v->multiply($v2), 32, 'Dot product with another vector works');
is($v * $v2, 32, "Multiplication operator returns dot product with another vector");
Enter fullscreen mode Exit fullscreen mode

Then to implement the multiply method in our module, add the following code to lib/Math/Vector3D.pm:

=head2 multiply

Multiplies this vector by a scalar or another vector.

    my $result = $vector->multiply(2);
    ... or
    my $dot = $vector->multiply(Math::Vector3D->new(2, 2, 2));

=cut

sub multiply {
    my ($self, $other) = @_;
    if (ref($other) eq 'Math::Vector3D') {
        # Dot product
        return $self->{vector}[0] * $other->{vector}[0]
            + $self->{vector}[1] * $other->{vector}[1]
            + $self->{vector}[2] * $other->{vector}[2];
    } elsif (looks_like_number($other)) {
        # Scalar multiplication
        return Math::Vector3D->new(
            map { $self->{vector}[$_] * $other } 0..2
        );
    } else {
        die "Argument must be a scalar or Math::Vector3D object";
    }
}
Enter fullscreen mode Exit fullscreen mode

This multiply method checks if the argument is a number or a Math::Vector3D object. If it's another vector, it calculates the dot product. If it's a number, it multiplies each component of the vector by that number. If the argument is neither, it raises an error.

Now update the overload declaration to include the * operator. Replace your existing overload declaration with the following code:

use overload
    '+' => 'add',
    '-' => 'subtract',
    '*' => 'multiply',
    '""' => 'stringify',
    fallback => 1;
Enter fullscreen mode Exit fullscreen mode

Now run your tests again and all should pass.

This concludes today's post. We have learned the basics of operator overloading in Perl by building a simple 3D vector class. We covered how to overload arithmetic and stringification operators, and how to write tests to verify our implementation. With these techniques, you can make your own Perl classes behave more naturally and intuitively when used with Perl's built-in operators.

If you do feel inspired, you could continue building on this module. For example, you might add a magnitude method and then overload the comparison operator <=> to compare vectors. If you do come up with your own extensions or improvements, please do share them below!

In the next post we will look at phasers in Perl. Phasers are special blocks that control when certain code is executed during the compile and run phases of your program. We'll explore what each phaser does, when it runs, and practical examples of how you can use them.

Related Blogs

Learning Perl – Introduction
Perl has long been known as the “duct tape of the Internet,” or "the Swiss Army chainsaw of scripting...
Learning Perl - Variables
I will attempt to explain things in this post in a way that is easy to understand, even for those who...
Learning Perl - Arrays
As stated in the previous post, Perl has three types of variables: scalars, arrays and hashes. Today...
Learning Perl - Hashes
In the last post we covered the basics of arrays, today we will look at hashes in more detail. What...
Learning Perl - Conditional Statements
So far we have covered basic variables in Perl, today we are going to look at how to use these...
Learning Perl - Loops and Iteration
In previous posts, we explored variables, arrays, hashes, and conditional statements in Perl. Now...
Learning Perl - Scalars
Before moving onto more complex topics lets come back to how we represent data in Perl. The most...
Learning Perl - References
In the last post we learnt how to create a reference to a scalar, an array, and a hash. In this post,...
Learning Perl - Ternary Operators
In a previous post, we learned about conditional statements in Perl. The ternary operator is an...
Learning Perl - Subroutines
Subroutines are one of the most important building blocks in programming. They allow you to organise...
Learning Perl - Regular Expressions
Regular expressions also known as regex or regexp, are a powerful way to match patterns in text. Most...
Learning Perl - Modules
A module in Perl is a reusable piece of code that can be included in your scripts to provide...
Learning Perl - CPAN
In the last post I showed you how to create a new module and how to use it in your code. In this post...
Learning Perl - Plain Old Documentation
When you write and program in any language it is good practice to document your code. Each language...
Learning Perl - Testing
In this post we will look at how to test Perl code using the Test::More module. Like documentation,...
Learning Perl - Exporting
In programming, we often want to share functionality across different parts of our code. In Perl,...
Learning Perl - Object Orientation
Object-Oriented Programming (OOP) is a widely used programming paradigm that enables the creation of...
Learning Perl - Inheritance
In the last post we discussed Object Oriented Programming in Perl, focusing on the basics of creating...
Learning Perl - File Handles
In programming file processing is a key skill to master. Files are essential for storing data,...
Learning Perl - Prototypes
Today we are going to discuss Perl subroutine prototypes, which are a way to enforce a certain...