Type classes in Scala

Type classes in Scala

Type classes in Scala

Type classes are a powerful and flexible concept that adds ad-hoc polymorphism to Scala. They are not a first-class citizen in the language, but other built-in mechanisms allow to writing them in Scala. This is the reason why they are not so obvious to spot in code and one can have some confusion over what the ‘correct’ way of writing them is. This blog post summarizes the idea behind type classes, how they work and the way of coding them in Scala.

Idea

Type classes were introduced first in Haskell as a new approach to ad-hoc polymorphism. Philip Wadler and Stephen Blott described it in How to make ad-hoc polymorphism less ad hoc. Type classes in Haskell are an extension to the Hindley–Milner type system, implemented by that language. Type class is a class (group) of types, which satisfies some contract-defined trait, additionally, such functionality (trait and implementation) can be added without any changes to the original code. One could say that the same could be achieved by extending a simple trait, but with type classes,there is no need to predict such a demand beforehand. There is no special syntax in Scala to express a type class, but the same functionality can be achieved using constructs that already exist in the language. That’s what makes it a little difficult for newcomers to spot a type class in code. A typical implementation of a type class uses some syntactic sugar as well, which also doesn’t make it clear right away what we are dealing with. So let’s start doing our baby steps to implement a type class and understand it. .

Implementation

Let’s write a type class that adds a function for getting the string representation of a given type. We make it possible for a given value to show itself. This is a .toString equivalent. We can start by defining a trait: https://gist.github.com/anonymous/eb721f6ec58dc8ff31fa984ca8b7eb9c We want to have show functionality, but defined outside of each specific type definition. Let’s start by implementing show for an Int. https://gist.github.com/anonymous/45420f7f5d60e2b660ebf2f64ecc7e7f We have defined a companion object for Show to add functionality there. intCanShow holds an implementation of Show trait for Int. This is a just the first step. Of course, usage is still very cumbersome, to use this function we have to: https://gist.github.com/anonymous/f5f20c3a8a3d861fc3adce229e87eeba The full implementation containing all needed imports can be found in the repo. The next step is to write the show function, in Show’s companion object, to avoid calling intCanShow explicitly. https://gist.github.com/anonymous/7055ceee759416491cf7c9e900ba2c33 The show function takes some parameter of type A and an implementation of the Show trait for that type A. Marking the intCanShow value as implicit allows the compiler to find this implementation of Show[A] when there is a call to: https://gist.github.com/anonymous/cf9adef550137df9f1d0130bbc227ed0 That is basically a type class. We’re going to transform it a little bit to make it look more like a real code (all required parts are there). We have a trait that describes the functionality and implementations for each type we care about. There is also a function which applies an implicit instance’s function to the given parameter. There is a more common way of writing the show function by having an implicit parameter. Instead of writing: https://gist.github.com/anonymous/3bf76379332879fe99c87442e984d45f we can use implicitly and rewrite it to: https://gist.github.com/anonymous/174c0dbecb9075d4a8f1565348ff6fe8 We also used the context bound syntax: A: Show, which is a syntactic sugar in Scala, mainly introduced to support type classes, it basically does the rewrite we have done above (without the use of implicitly), more information can be found here. There is one more trick (convention) often used in type classes. Instead of using implicitly we can add an apply function (to the Show companion object) with only an implicit parameter list: https://gist.github.com/anonymous/bc17bf2d0b2a3c42a8d5ee49680ba92d and use it in show function: https://gist.github.com/anonymous/6f82791f1d82a44deda3b0644fcbb8b0 This, of course, can be shortened even more: https://gist.github.com/anonymous/7d4813073ee7071ec942a2a842e56e99 We can improve our type class with the possibility of calling the show function as if it were a method on the given object – with a simple .show notation. By convention it is very often called a Ops class. https://gist.github.com/anonymous/7c39583e1d89362410f99389f82d59bd The Ops class allow us to write our clients’ code like this: https://gist.github.com/anonymous/cda6cd80ece10c23fae9a5d1bb17730d To avoid a runtime overhead it is possible to make the ShowOps a value class and move the type class’ constraint to the show function, like this: https://gist.github.com/anonymous/359919ba30007a430fde5e0639d7fcb8 After some of the rewrites placed above, the companion object of Show looks like this: https://gist.github.com/anonymous/b7614c328a278f47bcf92d89a717660f Now we can add one more instance of our type class, the one responsible for showing strings. It’s similar to the one showing ints. https://gist.github.com/anonymous/cc70df0f3afc28fcf2230d6cb739edf4 In fact, this is so similar that we want to abstract it – what can be done with a function to create instances for different types? We can rephrase it as a “constructor” for type class instances. https://gist.github.com/anonymous/28b67faa63ee2a8cafd93bfbf9d75893 The snippet above presents a helper function instance that abstracts the common code and its usage for Int and String instances. With Scala 2.12 we can use Single Abstract Methods, in result, the code is even more concise. https://gist.github.com/anonymous/b7f1529fb5e4e23c4dabaea686a60d5f This is a simple type class that defines two ways of calling the show function (show() and .show). It also defines instances for two types: Int and String. https://gist.github.com/anonymous/b3bac2b3a2614c35b14365e2a0823a55 We may encounter a need to redefine some default type class instances. With the implementation above, if all default instances were imported into scope we cannot achieve that. The compiler will have ambiguous implicits in scope and will report an error. We may decide to move the show function and the ShowOps implicit class to another object (let say ops) to allow users of this type class to redefine the default instance behaviour (with Category 1 implicits, more on categories of implicits). After such a modification, the Show object looks like this: https://gist.github.com/anonymous/23a4133df11b2aac27a6157f84e18b32 Usage does not change, but now the user of this type class may import only: https://gist.github.com/anonymous/6ab1a92c59aa6467f94b22b21da2ef2a Default implicit instances are not brought as Category 1 implicits (although they are available as Category 2 implicits), so it’s possible to define our own implicit instance where we use such type class. This is a basic type-class that we have been coding from the very beginning.

Own types

Own types

The creator of a type class often provides its instances for popular types, but our own types are not always supported (it depends on the library provider, whether some kind of products/coproducts derivation is implemented). Nothing stops us from writing our implementation for the type class. That, of course, looks exactly the same as if we would like to redefine the default instance that was provided by the implementer of the type class. While implementing our own instance the code follows the same pattern but could be implemented in a different location than the trait and the ops classes of the type class. Moreover, the type class is in our code base we may add this instance next to the default instances defined in type class trait’s companion object. As an example, let’s define a way to show a Foo case class and its instance outside of the type class companion object: https://gist.github.com/anonymous/1cef8a3fa66f842dd3c10faf9fbbdc00

Shapeless

This paragraph is a little off-topic, but worth mentioning. The way type classes are implemented in Scala (with implicits) makes it possible to automatically derive type-class instances for our own created types using Shapeless. For example, we could derive the show function (from previous paragraphs) for every case-class (actually for every product type) defined in our code. We would need to define instances for basic types and define show for product types, but it would have reduced so much boilerplate in our code! Similar derivation can be achieved with runtime reflection or compile-time macros.

Simulacrum

Simulacrum is a project that adds syntax for type classes using macros. Whether to use it or not depends on your preferences. If it is used, it’s trivial to find all type classes in our code and reduce some boilerplate. Moreover, a project that uses @typeclass has to depend on the macro paradise compiler plugin. The equivalent of our Show example with an instance only for Int would look like this: https://gist.github.com/anonymous/831995c790362e62d67419ce3372cf3b As you can see, the definition of a type class is very concise. On the usage side nothing changes – we would use it like this: https://gist.github.com/anonymous/206bce715e31240444a52c98b0d76083 There is an additional annotation @op that may change the name of the generated function and/or add some alias to the generated method (i.e. |+| notation for summing). Proper imports can be found in repo.

Implicits

Type classes use implicits as a mechanism for matching instances with code that uses them. Type classes come with benefits and costs related to implicits. It is possible to define multiple instances of type class for the same type. The compiler uses implicit resolution to find an instance that is the closest in the scope. In comparison, a type class in Haskell can only have one instance. In Scala, we can define an instance and pass it as a parameter explicitly (not relying on implicit resolution), which makes the usage less convenient, but may be useful. Our Show example needs a little modification to allow usage in a scenario, where we would like to pass instances explicitly. Let’s add a showExp function to the ShowOps class: https://gist.github.com/anonymous/8434d7018f07a05a4c8a5ab43d2606b7 Now, it’s possible to only run the .showExp function or define and provide an instance of Show to showExp explicitly: https://gist.github.com/anonymous/ac7ec362d3fd170d6c2c93e27466858d The first invocation uses the implicit found in scope, to the second invocation we pass the hipsterString instance of Show. The other way (more common) to achieve the same result – without adding an extra function, but fully relying on implicits – is to create a Category 1 implicit that would take precedence over the default instance (a Category 2 implicit). This would look like this: https://gist.github.com/anonymous/3fc28ea266e6ec2081ee8043b794dc49 "baz" would use the default instance defined in Show, but "bazbaz" would use hipsterString instance. The Scala way of implementing a type class (using implicits) could also cause some problems, which are described in the next paragraph.

Problems

With the power of implicits comes a cost. We can’t have two type class instances for some type T with the same precedence. This doesn’t sound like a terrible problem, but it does cause some real issues. It’s quite easy to get a compiler error (about ambiguous implicits) while using libraries like Cats or Scalaz, which rely heavily on type classes and build their types as a hierarchy (by subtyping). That is in detail described here. The problem is mainly related to the way type classes are implemented. Very often both ambiguous implicits implement exactly the same behavior, but the compiler can’t know about it. There are ongoing discussions on how to fix this. Errors may also be misleading, because the compiler doesn’t know what a type class is, e.g. for our Show type class used in such a way: https://gist.github.com/anonymous/84eb196baf4987d8544e967432ab25dd the compiler can only say that value show is not a member of Boolean. A similar error message is even reported when ambiguous implicits definitions are found, but the .show notation was used.

Open-source examples

Open source is a perfect place to look for examples of type classes. I would like to name two projects:

  • Cats uses type classes as a primary way to model things and simulacrum. Instances are implemented in separate traits, Ops are grouped in syntax traits.
  • Shapeless relies heavily on type classes. The power of shapeless is the ability to work on HLists and derive type classes to add new functionality.

Future of type classes

There are different attempts and discussions on how to add syntax for type classes:

There is also some ongoing discussion on the coherence of type classes:

Summary

Type classes as a concept are quite easy, but there are various corner cases when it comes to its implementation in Scala. The concept is rather used in libraries than in business applications, but it’s good to know the type classes and potential risks of using them. Scale fast with Scalac – Scala development company ready to solve all your challenges. [/av_textblock]

blank

See also

Author

blank
Łukasz Indykiewicz

Latest Blogposts

08.04.2021 / By Daria Karasek

How Outsourcing Can Solve Technical Debt

Technical debt has become widely prevalent nowadays. Since technology is constantly evolving, many businesses have to choose between acquiring new solutions or sticking with tried-and-tested ones. No right answer can be given to this hard choice. But even though debt sounds threatening for many, it doesn't always have to be.

30.03.2021 / By Marcin Krykowski

Top 15 Scala Libraries for Data Science in 2021

Python and R seem to be the first choice when it comes to data engineering. However, if we look a bit deeper, Scala is gaining more and more popularity. This is because, thanks to its functional nature, it is a more common choice when it comes to building powerful applications that need to cooperate with a vast amount of data.

23.03.2021 / By Tomasz Bogus

How To Tackle Technical Debt Quadrants?

You come across the term technical debt, read a little about this matter, know everything in theory, and you’re still not sure how to correctly identify whether some kind of design flaws are actually technical debt or not.  We have a solution that can help you with this issue – Technical Debt Quadrants. This method […]

blank

Need a successful project?

Estimate project