Overloading

Dear Computer

Chapter 12: Polymorphism Revisited

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:

Ruby
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:

Ruby
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:

Rust
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:

Rust
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:

Rust
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:

Rust
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.

← Impossible Arrays