Encapsulation is often called the core of object-oriented programming. Data is bundled with the functions that operate on that data. However, too much behavior can lead to monolithic, incohesive classes.
The type class pattern preserves a class’s core behavior but defines orthogonal behaviors externally. Type classes allow you to “extend” both user-defined and native classes with new functionality long after they were originally designed, and without modifying their source code. This results in a design based on powerful, generic algorithms written in terms of type classes.
Although type classes originated in functional programming languages, they have made their way into several mainstream object-based languages. In this post, we’ll take a look at these latest implementations.
Comparing Objects in Java
Comparator generic interfaces are used to support the comparison of objects. These interfaces are useful in generic collection algorithms such as searching, sorting, or finding the minimum or maximum value.
Comparable defines a single method:
compareTo(T o). This interface is used to compare the receiver with another object of the same class. The ordering implemented by this interface should be the class’s natural ordering.
Comparator defines a single method:
compare(T o1, T o2). This interface is used to provide an alternative ordering of a class, or an ordering for a class that does not have a natural ordering.
An algorithm written in terms of
Comparable requires its parameters to have a subtype relationship via inheritance. This renders the algorithm useless for third-party or native classes that don’t have a natural ordering, i.e., they don’t already implement
Comparable. We’re also out of luck if we don’t have access to their source code. However, using
Comparator removes any subtype requirements. We also can provide
Comparator instances for native classes like
Comparator is a type class.
The following example uses
Comparator to implement a comparison of a user-defined class. The
Comparator is then used by a generic sorting algorithm.
By externalizing the definition of comparison for
Person objects, we keep our
Person class small and cohesive.
In fact, this design is more encapsulated than an alternative in which
Person defines its comparison logic. Since
PersonWeightComparator is defined externally, it must go through
Person‘s public interface. So this new behavior has no dependency on the implementation details (data) of
Person. However, defining comparison logic directly on
Person, creates another method dependent on the implementation details of
This next example uses
Comparator to implement a length comparison for the native
String class. The
Comparator is then used in a generic algorithm to find the longest string in a collection of strings.
String does in fact implement
Comparable, however it compares
Strings lexicographically. This example demonstrates how a type class can be used to “extend” native classes, in this case, an alternative implementation of
Cleaner Comparisons in Scala
Type classes in Scala are implemented almost identically to Java. However, Scala adds one significant improvement.
In our Java examples, we had to explicitly pass the type class instance, our
Comparator, to the various
Collections methods. In Scala, we can define generic algorithms that accept a type class instance as an implicit parameter. Clients are not required to supply an argument for an implicit parameter. Instead, it can be defined elsewhere and the compiler will find it and pass it for you. This results in much cleaner, natural looking client code.
The following example uses the Scala library’s Ordering type class to implement two different types of comparisons of a user-defined class. These type classes are then used as arguments for both implicit and explicit parameters of a generic sorting function: Sorting.quickSort.
In the first call to
Sorting.quickSort, the Scala compiler finds our implicit
User.IdOrdering type class and passes it as the second argument. It found
User.IdOrdering because its definition is preceded by the
implicit keyword. This implicitness makes the sort function call appear as if the
User class defined the ordering. We get straightforward client code and avoid polluting our
User class with comparison logic.
The next sorting call demonstrates how we can still explicitly pass an argument for an implicit parameter. This allows us to override the implicit
User.IdOrdering and provide another implementation of
Type classes appear to be another way of monkey-patching classes, sort of like re-opening classes in Ruby. However, monkey-patching changes the behavior of that class throughout the entire program and possibly, throughout the lifetime of the program. Type classes provide much more fine-grained control, allowing you to vary class behavior on a per-function basis.
A typical object-oriented design relies on abstract interfaces and run-time polymorphism. Type classes also use abstract interfaces, but trade run-time polymorphism for compile-type type checking. Scala, with its support for both object-oriented and functional programming, provides a great environment to experiment with introducing type classes into a traditional object-oriented design.