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:
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:
- Subtype polymorphism, in which code serves many types by being written in terms of a common supertype
- Parametric polymorphism, in which code serves many possibly unrelated types by being written in terms of a placeholder type
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:
- What are the two ways that languages have implemented parametric polymorphism, and how are programmers and their programs affected by the choice of implementation?
- How are constraints placed on types to restrict which types may be served by polymorphic code?
- How does Rust's approach to object-oriented programming compare to the more traditional approaches of C++ and Java?
- What does it mean for one type to be a subtype of another?
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.