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 subclasses can inherit it.
Languages with dynamic typing—like Ruby, Python, and JavaScript—tend to enable more reusable code than statically-typed languages. For example, this Ruby function will turn any list into an HTML ul
tag:
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 have to commit to serving a particular type. This narrowing of focus 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 asking questions about the data 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 subclass methods are called on a supertype reference 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 that function to serve. Overloading means that we do repeat ourselves, but at least it 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.