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"
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();
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;
}
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);
Now we can run our test file to see if it passes:
prove -lv t/01-vector3d.t
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');
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}});
}
And then add the overload declaration at the top of the file after the use Scalar::Util
line:
use overload
'""' => 'stringify',
fallback => 1;
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
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");
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
);
}
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;
Now we can run our test file again to see if the addition tests pass:
prove -lv t/01-vector3d.t
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");
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
);
}
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;
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");
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";
}
}
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;
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.
Top comments (1)
One thing that is difficult when using
overload
module is understanding pretty complicated precedence rules. Not operator precedence (these are the same as for overloaded operator) but overload precedence itself. Which implementation is used if we have$a + $b + $c
and each of those instances are of different types and each type implementsuse overload '+' => ...
?While I like Perl when working with operators I always choose Raku. It allows to define any operator with fine precedence control using built-in
is tighter
/is looser
traits. It has ability to overload circumfix operators (brackets), which I don't think is possible in Perl. And most important - the implementation is chosen based on operator method signature, which is gamechanger in terms of readability and debugging.I don't want to discourage anyone from using Perl, it is just that its overloading capabilities are very limited. If you need prefixes
∑ @t
, infixes@a ∩ @b
, circumfixes⟨$x, $y⟩
, postfixes$x!
and ability to define any operator (not just overload) it may not be the right tool.Some comments may only be visible to logged-in visitors. Sign in to view all comments.