Higher Kinded Play Framework
Higher order functions are functions that accept other functions as parameters. Higher kinded types are types that can accept other types as parameters. In Scala, the syntax
F[_] is used to express that
F is a higher-kinded type that can accept any type.
Consider the following example; using a higher-kinded type in a service interface to retrieve Users by userId:
When using Play the
F[_] will almost definitely be
Future when we run the code. Why not just use
Future directly if we already know we’re going to use it at runtime? Well,
Future is a very specific class; it is an asynchronous success or an asychronous exception. That influences how to write and test code using it, in ways that can be detrimental.
Firstly, does your code need to know that things are asynchronous, or does it only need to sequence operations together?
We don’t care if we’re using Futures in the code above. If we use
F[_]: Monad instead of
Future we can still
map the two function calls and yield the same result.
F[_]: Monad says to the compiler, “choose anything you like, as long as it obeys the Monad laws”. This means our code becomes more generic; Scala has lots of monads, including
Either, etc. Using a higher kind allows us to explicitly state what is required, but also the ability to delay choosing until runtime.
The cats library has the identity (Id) monad, which has the type:
This type simply disappears once evaluated. The classic example of the power of the
Id monad is for use in testing. Your production code can implement
F[_]: Monad as a
Future, but your tests can use
Id. This means that your tests become synchronous, and a whole class of problems to do with async-testing disappear!
The second problem with
Future is that it forces the code to use exceptions. Exceptions can be tricky. They break type-signatures, as you can no longer tell what a method returns by looking at it. It might call a method that throws an exception. You would only discover that by reading the code, or running some tests.
The solution to that particular problem is to use
F[_], and pass back a type that has errors if they can happen, such as
The previous example of a UserService would become this:
Using higher-kinded types instead of
Future everywhere has a teaching and communication cost. That cost depends on who you’re working with, but I’ve seen people successfully get up to speed with this style (plus using the
EitherT monad-transformer) in only a few days.
The most tangible disadvantage I’ve experienced is IntelliJ being slow after importing all of the cats-library implicitly. A partial solution to this is to only import the implicits that you’re using:
IntelliJ also sometimes requires extra convincing that code compiles. SBT can compile things that IntelliJ’s error-highlighting complains about. Adding in type definitions and splitting into smaller methods usually works, but it is sometimes annoying.
You may also encounter strange Guice exceptions, and wiring issues, such as:
[info] com.google.inject.CreationException: Unable to create injector, see the following errors: [info] [info] 1) No implementation for cats.Monad<scala.concurrent.Future> was bound. [info] while locating cats.Monad<scala.concurrent.Future> [info] for the 1st parameter of ...
These errors can be solved by providing Guice with a
Monad[Future]. Fortunately, the cats-library can create one from an
In conclusion, despite some tooling-related annoyances and a small learning curve, I would encourage exploring this style of Scala when using the Play Framework. Code becomes more explicit, and easier to reason about as there are no exceptions, unless an exceptional scenario occurs, like an OutOfMemoryException.