Polymorphism

There's more to what we mean than what we say.

The meaning of every word ever spoken or written depends on context. The word “cold” means something good when applied to soda pop, but not when applied to a human being. An athlete may throw a ball, a game, or a tantrum, but the meaning of the word “throw” is different in each case. It’s not only a matter of what’s being thrown, but of what it means to “throw” something.

In computer programming, the principle that an expression may have different meanings depending on what it refers to is called polymorphism. Just as the meaning of a verb may depend on its object, the meaning of a function call may depend on its arguments, in which case we say the function is overloaded. For example, in C++, ‘operator +’ is overloaded to mean either concatenation or addition, depending on whether its arguments are textual or numeric.

string x = "hello", y = "world";
assert(x + y == "helloworld");

int x = 40, y = 2;
assert(x + y == 42);

Even if your programming language doesn’t directly support function overloading, you can roll your own by switching on type (as in this Python example) or delegating to some other language-level mechanism (as in this Rust example).

Verbs may depend on the type of their subject, rather than their object. The request “Please draw a card for me” means different things to a blackjack dealer and a cartoonist. Computer programmers don’t have any single term for this kind of polymorphism, but it’s often implemented through some form of “virtual dispatch,” such as overriding a method. (You may also hear phrases like “adding an instance of a typeclass” or “implementing a trait.”) Again, here are examples in Python and Rust.

Polymorphism comes in myriad forms, but they all amount to mappings from types to concrete behaviors. Whenever you write a polymorphic function, and must choose an implementation technique, consider the following questions:

  • Is the implementation resolved at compile time, or run time? For example, C++ function overloads are resolved at compile time, but virtual function calls aren’t resolved until run time. People sometimes call this distinction “early binding” vs. “late binding.”

  • Is the interface explicit, or implicit? For example, a C++ class declaring pure virtual methods is an explicit interface, whereas template parameters implicitly must support whatever syntax the template body uses. Analogously, a React component in JavaScript may or may not explicitly declare its prop-types.

  • Can the set of supported types be extended by the caller? Or would supporting a new type require changes to the implementation?

  • Should support for new types require opt-in by the caller? For example, Java and Go both support explicit interfaces; but whereas a Java class must opt into supporting a given interface (via inheritance), a Go struct implicitly supports any interface that happens to declare a compatible set of methods.

One other caveat is worth mentioning: Polymorphism, like all abstraction, enables flexibility at the expense of transparency. To whatever extent explicit is better than implicit, polymorphism comes at a cost. Of course, avoiding abstraction entirely would be silly. Explicit is not always better than implicit… but that’s a topic for another post. Part of the art (and responsibility) of computer programming is the design of abstractions that make code easy to write and maintain. As you practice that art, remember that polymorphism is among the most powerful tools in your toolbox.