Scala Tutorial - The Functor Trait Challenge From The Cats Library
Overview
At this point, we will put together what we've learned from the Scala basic section, classes, objects, functions, and of course, traits. With this in mind, we'll examine the Functor trait from the popular cats library. In particular, our goal is to gain additional insights and knowledge on the applications of traits, so as to be able to read and understand Scala code, such as, the trait Functor from the cats library.
As a reminder, and before proceeding further, feel free to review the previous materials on traits as they are especially important:
- Learn How To Create And Extend Trait
- Learn How to Create Trait With Type Parameters
- Learn How To Extend Multiple Traits
- Learn How to Use Traits for Dependency Injection Part 1
- Learn How To Use Trait For Dependency Injection Part 2 Avoid Cake Pattern
- Traits, Companion Objects, Factory Pattern
- Type Class For Ad-hoc Polymorphism
- Trait Mixin And Linearization
- The Magnet Pattern
Steps
1. Review of the Functor trait from cats
Within the wider Scala ecosystem, you will find numerous open-sourced libraries, such as, cats, that do the heavy-lifting on general functional, and generic, programming principles. For illustrative purposes, we’ve considered the Functor trait of the cats library. Believe it or not, a Functor is perhaps an embellished name to the popular map() function, which is commonly used in the application of a general transformation on a given type.
From the materials that we’ve covered up to the Function Polymorphism section in chapter two, we were able to read and understand from left-to-right, the map() signature below. Kindly note, and as a reminder from chapter two, Scala supports Polymorphic Methods, but to keep things simple, we've referred to functions and methods interchangeably. Firstly, def map[A, B] introduces two type parameters - that is, A and B - that are used to generalize the input parameters, as well as the return type. Secondly, the function parameters are grouped in the curry style within their respective parenthesis () - that is, (fa: F[A]) and (f: A => B). Thirdly, its second parameter group - that is, (f: A => B) - is a function f that knows how to transform types of A into equivalent B types. The resulting return type - that is, F[B] - is of course the transformed A types into B types.
@typeclass trait Functor[F[_]] extends Invariant[F] { self =>
def map[A, B](fa: F[A])(f: A => B): F[B]
...
}
In this chapter, we introduced the trait concept, and in the context of the above Functor definition, it models the required constructs that should make up a general Functor, such as, providing a map() function. We’ve also introduced Self-Types earlier, but the self => notation has a more subtle intent - that of aliasing this. You would generally use such an approach when effectively tunneling this into subsequent inner types, as demonstrated in the new ComposedFunctor type from the Functor source code.
def compose[G[_]: Functor]: Functor[λ[ α => F[G[α]]]] =
new ComposedFunctor[F, G] {
val F = self
val G = Functor[G]
}
...
Chapter two also conveyed the use of the extends keyword with inheritance in mind, and therefore the extends Invariant[F] should be straightforward in that the Functor trait inherits additional constructs from the Invariant type. What’s left for us to examine further are: (1) @typeclass , and (2) F[_]. Thus, let’s provide some additional insights below.
2. @typeclass.
In this chapter, we dedicated an entire section on the use of Type Class for general ad-hoc polymorphism. By and large, it involved us with having to use a trait as the contractual feature, and lifting and wiring of implicit to any given type. This process tends to be rather manual, and produce redundant boilerplate code, just for the setting up of a Type Class. The @typeclass annotation is a convenient shortcut whenever you want to use a Type Class. It automatically generates a lot of the boilerplate code for you! This annotation is not part of the standard Scala language, but is available from the simulacrum open-source project. To use the latter, you have to add the required artifacts to the libraryDependencies property, and other compiler settings, in your build.sbt file. The home page of the simulacrum GitHub project is an invaluable resource for the general setup and usage of the @typeclass annotation.
3. F[_]
In order to better understand the meaning of F[_], let us first understand the intent of the trait Functor from the cats library. As per its documentation, you will observe that trait Functor is meant to be a generalized abstraction for any type that is mappable. For simplicity, let us consider mapping over, say, a List data structure. The latter can obviously have elements of arbitrary types, such as, an Int, String, and so on. In effect, we could abstract out the types for the elements in the List to assume a List[_]. Surely, defining a trait Functor[List[_]] would thereafter be good enough.
But, how about other data structures, or similar types? Rather than having to define unnecessary and duplicate trait Functor, we use the F[_] to abstract over First-Order Types. And, therefore, you end up with one brilliantly designed trait Functor[F[_]] in the cats library as a generalized abstraction layer for mapping over any F[_].
Summary
In this tutorial, we went over the following:
- Review of the Functor trait from cats
- @typeclass
- F[_]
Tip
- You can find a wealth of information from typelevel.org on the cats library, as well as other similar resources and libraries, that lean extensively on functional, and generic, programming paradigms.
Source Code
The source code is available on the allaboutscala GitHub repository.
What's Next
In the next tutorial, we will now start looking at the Collection data structures offered out-of-the-box within the Scala language.