Better Cohesion with the Type Class Pattern

Jared Carroll ·

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

Java’s Comparable and 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 String. 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 Person.

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 String comparison.

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 User comparison.

Not Monkey-patching

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.

Mixing Styles

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.