Create your own
Lesson illustration

Ad-Hoc Polymorphism: Overloading vs. Type Classes

Hello! Welcome back to our study of programming language theory.

In the previous lesson, we introduced ad-hoc polymorphism, the principle of using a single function name or symbol to represent different operations for different types. We briefly surveyed a few mechanisms for achieving this, including function overloading and type classes.

Today, we will dive deep into a direct comparison of these two powerful mechanisms. This lesson is designed to meet the learning outcome: Compare two approaches to ad-hoc polymorphism: function overloading (e.g., C++) and type classes/traits (e.g., Haskell/Rust).

We will analyze their respective design philosophies, rules, and trade-offs, focusing on how they impact extensibility, type safety, and the overall structure of a program.

1. Function Overloading: Ad-Hoc Polymorphism by Name

Function overloading is perhaps the most direct implementation of ad-hoc polymorphism. It allows multiple functions to share the same name, provided their signatures (the number or types of their parameters) are different. The compiler resolves which specific function to call at compile time based on the arguments provided.

Let's start with a practical demonstration in C++.

Function Overloading | C++ Tutorial

This tutorial video, 'Function Overloading | C++ Tutorial', provides a clear, code-driven walkthrough of how function overloading works.

Please watch from the beginning until 03:44. Pay attention to: How functions are distinguished by the number of parameters (00:24). How they are distinguished by the type of parameters (02:09). The concept of an ambiguous call when the compiler cannot decide which version to use (03:01).

As the video demonstrates, the mechanism is straightforward: the compiler matches the function call's argument list to the available function definitions with the same name.

This image illustrates ad-hoc polymorphism via function overloading. The same function name, `print`, invokes different implementations based on the argument's type (integer, matrix, or shape), which is resolved by matching the function signature.

To formalize this and understand its nuances, let's consult a textual resource.

Function overloading

The Wikipedia article on 'Function overloading' provides a concise definition, outlines the rules, and discusses some of the complexities involved.

Please read the following sections: The introductory section ('Basic definition') for a clear summary. The 'Rules in function overloading' section, which clarifies that this is a compile-time mechanism. The 'Complications' section. This is particularly important as it highlights how name scoping and implicit type conversions can make overload resolution non-trivial.

The key takeaway is that function overloading is a name-based dispatch mechanism. The connection between volume(int) and volume(double, int) is purely nominal; they share the name volume, but are otherwise independent functions. The compiler's "best match" algorithm, especially when interacting with type conversions, can sometimes lead to behavior that is not immediately obvious.

2. Type Classes: Ad-Hoc Polymorphism by Contract

Type classes, pioneered by Haskell and adopted as "traits" in languages like Rust, offer a more structured approach. Instead of grouping functions by name, type classes group them by a shared set of behaviors or properties, defined in an abstract interface called a "class."

This image shows the definition of the `Eq` type class. It defines a contract for types that support equality. Any type `a` can be part of this class if it provides implementations for the `(==)` and `(/=)` methods.

A type class specifies a collection of functions with their type signatures. A specific data type is then declared to be an instance of that class by providing concrete implementations for those functions.

Let's see how this works in Haskell.

FP 5 - Types and Classes

In this segment from his 'FP 5 - Types and Classes' lecture, Graham Hutton explains how Haskell uses type classes to achieve what he calls 'overloaded functions'.

Watch from 31:37 to 36:55. Focus on: The structure of a type with a class constraint: (Num a) => a -> a -> a. The meaning of the constraint Num a: it restricts the polymorphic type variable a to only those types that are instances of the Num class. The three key example classes: Num (numeric types), Eq (equality types), and Ord (ordered types).

As you saw, a function like (+) :: (Num a) => a -> a -> a is polymorphic, but its polymorphism is constrained. It doesn't work for any type a, only for types a that have an instance of the Num class. This elegantly combines parametric and ad-hoc polymorphism.

To get a deeper understanding of the mechanics, let's turn to a classic Haskell tutorial.

A Gentle Introduction to Haskell: Classes

This chapter from 'A Gentle Introduction to Haskell' provides a detailed explanation of type classes, contrasting them with parametric polymorphism and OO concepts.

Please read the introductory section (Section 5) and the following subsection that details class and instance declarations. Focus on: The distinction made between parametric and ad-hoc polymorphism. The syntax for class and instance declarations. How the context (Eq a) propagates to the type of a function like elem.

The core idea here is contract-based dispatch. A type class defines a contract (e.g., "supports equality comparison"). Functions can require this contract, and any type can fulfill it by providing an instance.

3. A Comparative Analysis

Now we can directly compare these two approaches along several important axes.

FeatureFunction Overloading (e.g., C++)Type Classes / Traits (e.g., Haskell/Rust)
Grouping PrincipleBy Name. Functions are related only by sharing a name. add(int, int) and add(string, string) are coincidentally named the same.By Semantic Contract. Functions are grouped into a class that represents a coherent set of behaviors (e.g., Eq for equality, Show for string conversion).
ExtensibilityOpen for new types. You can always add a new overload for a new type you define.Open for new types AND new behaviors. You can add an instance for a new type. You can also define a new type class for new behaviors.
Retroactive ConformanceYes. You can define a new print(MyType) function at any time, even if MyType comes from a library.Yes (and it's a key feature). You can define an instance Show MyType even if both Show and MyType are from external libraries. This is often called "orphan instances."
CoherenceNot guaranteed. There's no rule that add(int, int) and add(float, float) must be semantically related to "addition." One could implement subtraction.Guaranteed by convention (and sometimes laws). The compiler ensures an implementation exists, and the community expects it to follow semantic laws (e.g., x == x should always be True for any Eq instance).
Dispatch MechanismName resolution. The compiler finds the "best fit" from a set of functions with the same name, which can be complex with implicit conversions.Instance resolution. The compiler searches for a unique, specific instance that matches the type and the required class. The mechanism is generally more constrained and predictable.
IntegrationSeparate feature. Overloading is a distinct language feature for ad-hoc polymorphism.Integrated with parametric polymorphism. It creates constrained parametric polymorphism, allowing functions to be generic over a class of types.

Your experience with C++ and Haskell likely gives you a practical feel for these differences. The C++ approach feels direct and locally convenient, while the Haskell/Rust approach provides a more global, architectural structure for managing polymorphism.

The following reading provides an excellent summary of these differences from the perspective of Haskell's designers.

A Gentle Introduction to Haskell: Classes

We return to 'A Gentle Introduction to Haskell' to read its direct comparison of type classes with features in other languages like C++.

Please read the subsection 'Comparison to Other Languages'. It explicitly contrasts Haskell's classes with C++ classes and overloading, reinforcing the points in our table.

Conclusion

We have now conducted a detailed comparison of function overloading and type classes as two distinct approaches to ad-hoc polymorphism.

Key Takeaways:

  • Function Overloading is a simple, name-based mechanism. It's easy to add new overloads, but it lacks a way to group operations semantically, and its resolution rules can be complex.
  • Type Classes are a powerful, contract-based mechanism. They provide strong semantic grouping, integrate elegantly with parametric polymorphism, and allow for remarkable extensibility through retroactive conformance (orphan instances).
  • Design Trade-off: The choice between them reflects a language's design philosophy. C++ prioritizes a direct, multi-paradigm approach where overloading is a convenient tool. Haskell and Rust prioritize principled abstraction, where type classes/traits provide a unified and type-safe system for managing both generic and specialized behavior.

Next Lesson Preview:

Having covered parametric and ad-hoc polymorphism, we will turn our attention to the third major category: subtype polymorphism. We will explore its deep connection to the concept of inheritance in object-oriented programming and compare its strengths and weaknesses against the polymorphic styles we have already studied.

Can't find a good explanation? Sign up and we'll make it for you

Sign up