Overwhelmed By Types

Dear Computer

Chapter 12: Polymorphism Revisited

Overwhelmed By Types

What are programmers most afraid of? Repeating themselves. They tirelessly endeavor to never write the same code twice. They identify a recurring pattern of execution and then loop over the pattern. They parameterize an algorithm and express it as a reusable function. They factor out common state and behaviors into a superclass so that multiple subclasses can inherit it.

Code tends to be more reusable and less repeated in languages with dynamic typing—like Ruby, Python, and JavaScript—rather than static typing. For example, this Ruby function turns any array into an unordered list HTML element:

Ruby
def listToHtml(items)
  bullets = items.map { |item| "<li>#{item}</li>" }
  "<ul>#{bullets.join}</ul>"
end
def listToHtml(items)
  bullets = items.map { |item| "<li>#{item}</li>" }
  "<ul>#{bullets.join}</ul>"
end

The list could hold numbers, strings, bands, symptoms, ultimatums, enemies of the state, or anything really, as long the objects implement to_s. The function works with all manner of objects because types play a diminished role in dynamically typed languages. The operations a value supports matter more than its type.

Languages with static typing—like C and Java—don't have it so easy. When we write a function in a statically typed language, we commit to serving a particular type. Narrowing the interface to a particular type certainly has advantages: the compiler has enough information to typecheck our code, and the executable will run faster because the compiler will generate machine code specific to the type instead of querying the type at runtime. But the glimmer of these advantages starts to fade as our codebase acquires lots of similar but not identical types. Are we going to need to write one version of listToHtml for integers, another for floats, yet another for strings, and still another for each new type we define?

Good news! We will not have to repeat ourselves, because polymorphism has our back. Statically typed languages grant programmers these two different polymorphic superpowers with which we write a single chunk of code that serves many types:

We have touched upon both of these polymorphisms in earlier chapters. Through the lens of C++, we examined how methods called on a superclass reference actually invoke subclass behaviors if the methods are virtual. Through the lens of Haskell, we examined how functions and other types can receive type parameters and how type constraints may be placed on type parameters through typeclasses.

There's also ad hoc polymorphism—or overloading—in which we write a version of a function for each type that we wish to serve. Overloading means that we do repeat ourselves, but the repetition at least gives us a common interface.

In this chapter, we revisit these two static polymorphisms. By its end, you'll be able to answer the following questions:

At the end of the day, a programmer using a statically typed language will not have to repeat themselves any more than a programmer using a dynamically typed language. However, both polymorphisms must be employed with care to ensure the safety and speed that programmers expect in a statically typed language.

We begin our discussion by examining parametric polymorphism in Java, C++, and Rust.

Parametric Polymorphism in Java →