Overloading
Subtype polymorphism and parametric polymorphism save a programmer from having to repeat code for different types. There's a third polymorphism that we briefly examined when discussing Ruby and Haskell: ad hoc polymorphism. We don't save ourselves from writing code with ad hoc polymorphism, but we do save ourselves from having to come up with a new interface for familiar operations. There are two kinds of abstractions that may be eligible for overloading: named functions and operators.
Suppose we create a new Ingredient
type for a game. Players add Ingredient
values together to craft items. In a language with ad hoc polymorphism for operators, we can overload the familiar +
operator instead of developing a new vocabulary through named methods.
Programming languages vary in the degree to which they support ad hoc polymorphism. In Java, we can overload a method as long as each overload has a different signature, but we can't overload the builtin operators. In Ruby, we can overload methods and operators for our custom classes, but we can't have multiple implementations with different signatures. In C++, we can overload both methods and the builtin operators and have multiple implementations. Rust is like Haskell; we can overload functions and operators only if the overloaded interface is declared by a trait.
In Ruby, we overload the +
operator for our new types by defining the +
method. This Vector2
class supports addition:
class Vector2
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def +(other)
Vector2.new(x + other.x, y + other.y)
end
end
The Ruby interpreter translates an expression of the form a + b
into the method call a.+(b)
. This script demonstrates how clients can use the existing interface of mathematical notation instead of using some new notation:
a = Vector2.new(1, 4)
b = Vector2.new(2, 0)
sum = a + b # [3, 4]
In Rust, we overload a builtin operator by implementing a special trait. The +
operator is associated with the Add
trait, which is defined as this in the standard library:
pub trait Add<RHS = Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
This trait is hard to understand initially because it has been thoroughly abstracted. It could have been written more simply like this:
pub trait Add {
fn add(self, other: Self) -> Self;
}
But this less abstract version only allows us to add a Vector2
to a Vector2
and get back a Vector2
. The more abstract version allows us to add a value of some arbitrary type RHS
to a Vector2
and get back a value of some other arbitrary type Output
. That means we could support mixed mode arithmetic like Vector2::new(3, 4) + 19
.
The official and abstract Add
trait requires its implementing types to define the Output
type and the add
function. This implementation provides these definitions so that two Vector2
values may be added together to produce a new Vector2
:
use std::ops::Add;
impl Add for Vector2 {
type Output = Self;
fn add(self, other: Self) -> Self::Output {
Vector2 {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
The Rust compiler turns the +
operator into an add
function call. The savings on interface is once again seen in client code:
let a = Vector2 {x: 1.0, y: 4.0};
let b = Vector2 {x: 2.0, y: 0.0};
let sum = a + b;
Rust defines traits for many of its builtin operators so that our types can use the exact same notation as the builtin types.