Custom Operators

Dear Computer

Chapter 6: Expressions

Custom Operators

When Bjarne Stroustrup was designing C++, he aimed for type parity, the notion that the types users invent must have the same power and syntactic form as the types built in to the language. If builtin types may be acted upon by all manner of operators like + and [], then so must user-defined types. To define an operator's behavior for one of our own types, we write a method. This practice is operator overloading.

This C++ program demonstrates how the ++ and unary * operators may be overloaded for a class that represents an index that wraps around when it reaches some maximum value:

C++
class CircularIndex {
  public:
    CircularIndex(int period) :
      period(period),
      x(0) {
    }

    CircularIndex& operator++() {
      x = (x + 1) % period;
      return *this;
    }

    int operator*() {
      return x;
    }

  private:
    int period;
    int x;
};

int main() {
  CircularIndex i(3);
  for (int j = 0; j < 8; ++j) {
    std::cout << *i << " ";       // prints 0 1 2 0 1 2 0 1
    ++i;
  }
  return 0;
}

The main function shows how operators are applied to a CircularIndex instance just as they would be applied to a pointer or int. C++ allows programmers to overload almost of all of its builtin operators. It does not allow new operators to be defined, so we can't add methods for ** or ~> or $@!#.

The advantages of operator overloading are felt most in mathematics, the natural sciences, and other disciplines that make heavy use of compact notation. When operators are overloaded, code uses the same symbolic vocabulary that is found in proofs and scientific literature.

Not every language designer agrees that type parity is a noble goal. Java provides no option to overload operators, and its designer James Gosling reflected on his decision to not implement features like it in an interview:

I had this personal rule that by and large I didn't put anything in just because I thought it was cool. Because I had a user community the whole time, I'd wait until several people ganged up on me before I'd stick anything in. If somebody said, “Oh, wouldn't that be cool,” by and large I ignored them until two or three people came up to me and said “Java ought to do this.” Then I'd start going, well, maybe people will actually use it.

There's this principle about moving, when you move from one apartment to another apartment. An interesting experiment is to pack up your apartment and put everything in boxes, then move into the next apartment and not unpack anything until you need it. So you're making your first meal, and you're pulling something out of a box. Then after a month or so you've used that to pretty much figure out what things in your life you actually need, and then you take the rest of the stuff—forget how much you like it or how cool it is—and you just throw it away. It's amazing how that simplifies your life, and you can use that principle in all kinds of design issues: not do things just because they're cool or just because they're interesting.

Some developers claim that operator overloading makes code too unpredictable. There's no way to enforce an operator to behave in the same way as it does for other types. We could make + perform a subtraction, for example.

Ruby supports type parity. Most of its operators can be overloaded in user-defined classes. Additionally, operators can be monkey patched into builtin classes to override or add to their behavior. This Ruby script adds the subscript operator [] to the Integer class so that clients can extract a designated bit from its binary representation:

Ruby
class Integer
  def [](place)
    # Shift bit into ones-place and mask.
    (self >> place) & 1
  end
end

# prints 0101
print 5[3]
print 5[2]
print 5[1]
print 5[0]

Normally we can't subscript into integers.

Haskell goes a step further than C++ and Ruby by allowing programmers to define brand new operators that don't look anything like the builtin ones. This Haskell program defines the # operator to behave just like the subscript operator in the Ruby program above:

Haskell
import Data.Bits

(#) :: Int -> Int -> Int
(#) x place = (shiftR x place) .&. 1

infixl 9 #

bits = [5 # 3, 5 # 2, 5 # 1, 5 # 0]    -- [0, 1, 0, 1]

Recall that operators in Haskell are functions, and the operator is defined just like a function, with parameters and an expression that yields a return value. The only difference is that the symbol must be parenthesized. The custom operator's associativity and precedence are set with a family of infix commands. The command infixl makes the operator left-associative, and infixr makes the operator right-associative. The command infix makes the operator non-associative, which means explicit parentheses must be inserted to break any precedence ties. The first parameter to the infix* commands is the precedence level, which must be in [0, 9].

← Conditional ExpressionsEvaluation Order →