Photo by Ankush Minda on Unsplash

The need for higher kinds

Some of the worst software engineering disasters arise from trying to achieve code reuse too soon. What if we tried to share map? If we did this, we could then write a function that consumed either a Sequence or a List:

The classic way of doing this in object-oriented programming would be to extract a common interface or abstract class. But look what happens in the attempt below:

Both implementations of map now have a return type of Mappable. If we call List#map we lose the type information for List, so we can’t go on to chain methods that would be available for a List. Something like the following wouldn’t work as size doesn't exist on Mappable:

We want to have an operation like map work across a range of classes. To understand why, imagine if we had to learn a different operator for each different collection we wanted to transform with a mapper! Just as whatever subclass of List we encounter will have a contains method through polymorphism, whatever kotlin-stdlib collection we encounter, we can reasonably expect to find a map function.

In functional programming, we talk of “parametric polymorphism.” This is not polymorphism through subclassing, or through implementation of interfaces. Rather, it is the polymorphism we obtain through generics. With generics we can write a map function for List that does not care about the contents of the List — it will handle List<Int> just as well as List<String>.

With our multiple implementations of map across different containers, aren’t we also obtaining a higher level of parametric polymorphism? When we write our containers like List and Sequence, we imagine a ghostly “shape” for all of these containers. A shape that includes a map function. If you know RxJava, think of how Observable and Single etc. all implement map but don’t obtain this implementation from sharing a common super-class.

But what if we could make the compiler see what we see? What if we could make the compiler understand “a data type that implements map"?

Higher kinds in Scala

But if we try this in Kotlin, we run into a problem. If we sidestep some of the syntax errors, we get:

The compilation error says that we aren’t allowed type arguments for type parameters. In a way, we have run into a limitation of generics on the JVM — Java 5 generics are first-order. There are no generics of a higher kind. In other words, we can have String (proper), and Sequence<A> (first-order), but not Mappable<F<_>> (higher-order). We can parameterise, or abstract over, the choice of contents A but not the choice of the container F.

The red book

Recall that functions that take functions as parameters are called “higher-order functions.” Similarly, type constructors like Mappable that take other type constructors as parameters are called “higher-kinded type constructors.”

In this context, Sequence is a type constructor. To paraphrase the red book, there are no values of type Sequence but we can apply it to the type String to produce Sequence<String>. Likewise, we can apply the type constructor Sequence to the type constructor Mappable to get Mappable<Sequence<_>>. The Stack Overflow question below explains this in more detail:

Can we get around the compiler limitation somehow? In a famous paper called “Lightweight higher-kinded polymorphism” Jeremy Yallop and Leo White explain a way we can implement higher-kinded type constructors in a language that was not built for them.

Let’s apply the paper’s technique to Kotlin. Since we can’t write Mappable<F<A>>, we instead writeMappable<Kind<F, A>>.

The containers we abstract over need to be represented using this Kind. Option requires a type parameter, it’s not like the pre-generics version of collections classes like Hashtable. We can’t write a type parameter of a type parameter, so something like Mappable<Kind<Option<*>, A> is out of the question. Instead, we write a “witness” for Option called ForOption:

The first line is the class declaration, a simple class with no type parameters that can be applied to a Kind<F,A>. The second line is a “trick” to allow us to view Kind<ForOption, A> as OptionOf<A>, moving the type parameters from being siblings in the Kind to being applied one to the other.

The fix method is for transforming from higher back to first-order:

Since the boilerplate surroundingForOption would need to be generated every time we wanted to use higher-kinded type constructors, we can use an annotation processor to emit the code for the witness class, type alias, and the fix function:

Now that we have a way of representing F<Option<_>>, we can now write a Mappable interface:

We can "tell” the compiler that Option conforms to the shape of Mappable in the following way:

Remember that OptionOf<A> is just an alias of Kind<ForOption, A>. The “implementation” of Mappable’s map simply downcasts Kind<ForOption, A> to Option<A> (via fix) and calls the pre-existing internal implementation of map for Option<A>. The downcast is safe as, by convention, we ensure that there is only one implementation of Kind<ForOption, A> i.e., Option<A>. This squares the circle for higher-kinded polymorphism in Kotlin: we can now abstract over a “container that implements map.”

While our Mappable only has one function for now, we can imagine writing other functions on Mappable that take advantage of the critical map function. This is in fact what happens in the arrow-core library. We used the name Mappable to make things easy, but the formal name for this concept isFunctor:

Signature for Functor (Mappable) including extension methods

The extension annotation causes these additional functions from Functor to become available on OptionOf<A> as extension functions through code generated by the annotation processor. Since having map enables other operations that, in turn, call map, this can greatly increase code reuse — telling the compiler that our container is mappable will allow these extra operations that might not be present in the contract of the container itself. All while preserving the precious type information that would otherwise be lost.

So what?

Notice how the key methods parameterise over a container F:

Remember, Kind<F, List<NewsItem>> is essentially the F<List<NewsItem>> that we cannot normally represent using Kotlin. Because we abstract over F, it is possible to substitute a different F at the entry point to the application. For the standard implementation, we supply an F that is the Arrow class IO:

However, we could easily swap this with another container. Imagine a requirement for interop with some pre-existing RxJava code. The RxJava class Single has a similar “shape” to IO in that it allows lazy execution of operations and failure with exceptions. We can substitute IO for Single at the entry points to the application:

This kind of deep change would be nearly impossible in traditional OO programming.

References

Higher-kinded Polymorphism: What is it, why you want it by David Raab

Higher-kinded Types in a Lower-Kinded Language by Jacob Bass

Simulating Higher-Kinded Types in Java by John McClean

How KEEP-87 & Typeclasses will change the way we write Kotlin by Sebastian Sellmair

Android