Advanced FP for the Enterprise Bee: Typeclasses

Garth Gilmour
Feb 5 · 8 min read
Image for post
Image for post
Separate Frames from a Beehive

Introduction

Thus far in our series, we have seen that FP libraries (like Arrow and Cats) add extra operations to existing types. This is accomplished via . A Typeclass (or Type Class if you prefer) is an alternative way of associating a set of functions with a set of fields.

Typeclasses are integral to Haskell, and Swift provides a similar feature called Protocols. They are not directly available in Kotlin or Scala, but those languages give us the tools we need to simulate them.

This article will explain what Typeclasses are, why they are useful, and how they can be written in Kotlin and Scala. We will then review a practical example of Typeclasses from the Arrow library.

What’s Wrong With Regular Classes?

Before we begin, let’s take a minute to examine what’s wrong with conventional classes. We will review the standard reasons for writing traditional C++/Java/C# style classes, and then see if we can refute them. If you’re already convinced of the worth of Typeclasses, feel free to skip this section.

. This argument would seem to be irrefutable. Why wouldn’t you want to put data, and the functions using that data, in the same place? After all, anyone cursed to maintain poorly written C code knows the misery that results when this is not the case.

However, as I first learned from reading Effective C++, the interface of a class extends beyond the definition of the type. You will always have helper functions, which accept an instance of the type and perform additional work. Even if your classes were perfect, you would still need helper functions for optional and/or context specific functionality.

This is why Microsoft added extension methods to C#, back when they were designing the LINQ query syntax. Other typed languages, including Kotlin, have done the same.

. Once again this is a very strong argument. Uncontrolled access to the fields within an object makes managing state impractical and maintenance a nightmare. However in Pure FP we require our data to be immutable, so uncontrolled change is not going to be a concern.

. The argument here is that you want the freedom to change the data types used in your fields. For example, if a number needs to be changed from an int to a long or float, you can do so without the effect of the change rippling out across your codebase.

This is very desirable, but does not require OO. All you need is a set of functions, that make up an interface to the underlying data. For example you could use a set of PL/SQL stored procedures to hide the underlying format of a database table in Oracle.

. When I was learning to code, inheritance and polymorphism were all the rage. They were the killer features of OO. Unfortunately several decades of real world usage has shown that implementation inheritance is often more trouble than it is worth. Polymorphism does remain very useful, especially via interfaces. But, as we will see, Typeclasses can provide polymorphism at least as well.

What’s a Typeclass?

A simple Use Case for Typeclassses is supporting conversion to an external format. Let’s say that we sometimes wish to marshal and un-marshal the contents of our entity objects. The format could be either XML or JSON, depending on the context in which the entity objects are being used.

Here’s an interface to represent the functionality we need:

We are defining two extension methods on a type parameter T. The marshal method will convert the objects state into some textual format, whereas the unmarshal method will do the reverse.

The unmarshal method would more properly be declared at the level of the class, but we will make it an instance method for the purposes of the example.

Next we create some simple entity types:

Having defined both our interface and entity types, we can create an implementation for each type. Here are some simple Marshaller objects for the Person and Entity types that convert instances to and from JSON:

The details of the implementation are encapsulated, via a surrounding JsonMarshalling object and the accessor functions forPerson and forOrder.

We repeat the process for marshalling via XML. As you would expect the structure of the code is the same:

We now have everything we need in place. In the demo below we import the JsonMarshalling type and use the accessor functions to obtain the implementations of our Marshaller interface. We need the extension methods to be in scope whilst we convert the data in sample Person and Order objects. This is easily done thanks to the built in run method.

This is the output produced. As you can see we have successfully marshalled to and from JSON:

{ “person”: Jane }
Person called Dave
{ “order”: 123.45 }
Order of value 456.78

Given that Typeclasses are all about optional functionality, it should be easy to switch to the XmlMarshalling implementation:

Now we can marshall to and from XML:

<person>Jane</person>
Person called Dave
<order>123.45</order>
Order of value 456.78

The same result could have been achieved by using plain extension functions. But then the compiler could not detect if a type implemented marshal without unmarshal, or vice-versa. Typeclasses give us a way to bundle related extensions into a single abstraction. This improves the quality of our design and the maintainability of the resulting code.

A Scala Comparison

We have seen that we can successfully enhance existing types by adding sets of extension methods. But there was quite a bit of plumbing involved. Would this have been any simpler in Scala? Let’s port the example and find out…

Here’s the Scala version of our original interface:

And the corresponding entity types:

It’s when we implement the interface that differences emerge:

We have marked the instances as being implicit. This gives the compiler permission to use them, on our behalf, when the client code requests it. Implicit declarations are a ‘chainsaw feature’ of Scala, and have been redesigned for version 3.

Another major difference is that Scala 2 does not support extension methods directly. Instead implicit declarations do all the heavy lifting for us.

We define an implicit type called JsonMarshallingOps, which takes a T and provides marshal and unmarshal methods. These delegate the work to Marshaller instances which, because they themselves are declared implicit, will be discovered by the compiler:

When we invoke marshal on a Person object the compiler will:

  • Create a JsonMarshallingOps object, passing in the Person via value
  • Find instances of Marshaller[T] to satisfy the implicit parameter lists
  • Insert a call to the marshal method of the JsonMarshallingOps

This seems very convoluted. But when we examine the client code we only need to perform the imports:

{ "person": Jane }
Person called Dave
{ "order": 123.45 }
Order of value 456.78

In summary the Scala implementation is more complex than Kotlin. However that complexity can more easily be abstracted into a library.

Examples of Typeclasses in Arrow

Having seen the essence of the Typeclass Pattern in two languages, let’s examine a practical example from the Arrow source code. We can take the traverse operator from our first article and try to understand how it works.

This is the first declaration we meet (with package names simplified):

fun <G, A, B> List<A>.traverse(
arg1: Applicative<G>,
arg2: Function1<A, Kind<G, B>>): Kind<G,Kind<ForListK, B>> =
arrow..List.traverse().run {arrow..ListK(this@traverse)
.traverse<G, A, B>(arg1, arg2)
as Kind<G,Kind<ForListK, B>>
}

As we know, the first parameter to traverse is an Applicative and the second is a function. The type parameter of the Applicative corresponds to the type returned from the function.

We also know, from an earlier article, that the Kind type is being used to simulate Higher Kinded Types in Kotlin. So when we see Kind<G, B> we know that G is the container type and B the content. In the case where the function returned an Option<Person> then G would be theOption and B the Person.

The Typeclass functionality starts with the call to List.traverse(). If we drill down we see that this function always returns an instance of ListKTraverse.

This type declares a set of extension functions for lists:

interface ListKTraverse : Traverse<ForListK> {

override fun <G, A, B> Kind<ForListK, A>.traverse(
AP: Applicative<G>,
f: (A) -> Kind<G, B>): Kind<G, ListK<B>> = fix().traverse(AP, f)


}

The run utility function is being called against an instance of this type, which in turn is being passed into a newly created instance of ListK. It is here that we finally find the implementation of traverse:

fun <G, B> traverse(
GA: Applicative<G>,
f: (A) -> Kind<G, B>): Kind<G, ListK<B>> =
foldRight(Eval.now(GA.just(emptyList<B>().k()))) { a, eval ->
GA.run {
f(a).apEval(eval.map {
it.map { xs -> { a: B -> (listOf(a) + xs).k() } }
})
}
}.value()

That’s certainly a lot of plumbing to get our heads around. But if we take a step back the overall structure is similar to our initial (naive) example, just with an extra extension method to avoid the client code having to call run.

Combining our own Typeclasses with Arrows

To consolidate what we have learned so far, let’s take the competition example from the HKT article and extend it to use a Typeclass.

As we know the first step is to define an interface:

We declare two extension functions, judgeFairly and judgeUnfairly. Because we want them to apply to any type that takes two type parameters we use the Kind2 type to simulate a Higher Kinded Type.

The next step is to declare implementations for some Arrow types:

Having done this, we can run our extension functions against some sample objects:

This gives us the output we expected:

winner
loser
loser
winner
winner
loser
loser

Typeclasses and the Expression Problem

We have seen that Typeclasses provide a way to ‘layer on’ extra functionality onto existing types. As such Typeclasses are one possible solution to the Expression Problem. This problem originates from the following observations:

  • Object Oriented languages make it easy to extend an existing hierarchy of types. They achieve this via subclassing and (regular) polymorphism.
  • Functional languages make it easy to add additional operations to an existing set of types. They achieve this by pattern matching, i.e. switching elegantly and efficiently on the actual type of a parameter.
  • Neither approach can do both. OO does not cope well with adding new operations to an established interface. FP does not cope will with adding new types to an established hierarchy.

Typeclasses represent a middle ground, where we can both extend the hierarchy and the operations with the minimum amount of refactoring. Of course, if the interface is stable, then the extra effort is overhead. So, as with most ‘A or B’ questions, it turns out that we are better off with both regular classes and Typeclasses.

Conclusions

Typeclasses provide us with an alternative way to associate functions and data. They provide many of the advantages of regular classes, but also allowing us to selectively include what we need. This is great when we are enhancing existing types, or need to switch between implementations. Both Kotlin and Scala provide enough functionality to construct Typeclasses as ‘second class citizens’, but it’s a job more suited for library writers.

Thanks

I am grateful to Richard Gibson and the Instil training team for reviews, comments and encouragement on this series of articles. All errors are of course my own.

Google Developers Experts

Experts on various Google products talking tech.

Garth Gilmour

Written by

Helping developers develop software better. Coding for 30 years, teaching for 20. Google Developer Expert. Trainer at Instil. Also martial arts and philosophy.

Google Developers Experts

Experts on various Google products talking tech.

Garth Gilmour

Written by

Helping developers develop software better. Coding for 30 years, teaching for 20. Google Developer Expert. Trainer at Instil. Also martial arts and philosophy.

Google Developers Experts

Experts on various Google products talking tech.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store