Skip to content

Colin Webb

Functional Programming with the Play Framework

Three ways to do functional programming when working with the Play Framework.

Use functions

This one seems obvious, but Play Framework has a lot of influence from Java so tends to favour object composition. To do functional programming, pass a function as a dependency instead of passing an object.

In the example below, we're implementing a UserService class to aggregate everything needed to display the user's profile on a social media site. The UserService currently depends on two traits, and uses a method from each.

trait UserDB {
   def get(id: UserId): User
}

trait TimelineDB {
   def get(id: TimelineId): Timeline
}

class UserService(userDB: UserDB,
                  timelineDB: TimelineDB) {

   def getProfile(id: UserId): UserProfile = {
      val user = userDB.getUser(id)
      val timeline = timelineDB.get(user.timelineId)
      UserProfile(user, timeline)
   }
}

We can change our implementation of the UserService to rely on functions instead of the traits.

class UserService(
   getUser: UserId => User,
   getTimeline: TimelineId => Timeline) {

   def getProfile(id: UserId): UserProfile = {
      val user = getUser(id)
      val timeline = getTimeline(user.timelineId)
      UserProfile(user, timeline)
   }
}

Testing this class becomes easier. Switching out implementations does too, as the class no longer relies on another. It is just functions. Your tests can use a function like this:

val getUser = (id: UserId) => User(id, "Alice")

Or even a function like this, to test error handling.

val getUser = (u: UserId) =>
   throw new DBError("Cannot connect to DB")

One of the great things about using functions instead of classes is that mocking libraries become less essential.

Guice works on type signatures, and happily supports binding and injecting functions into classes. Use the @Named annotation in order to distinguish between different functions with the same type signature. Alternatively, don't use Guice, now that Play 2.6 has made it an optional dependency!

Consider higher-kinded types

In our previous example, the UserDB gave back a User. This type signature implies a synchronous call to get a User.

In practice, this means there is a thread blocking, or the code would return a Future[User] instead. Perhaps we're using a non-blocking database driver, or perhaps the DB is actually a microservice and an HTTP request/response. Perhaps we actually want a thread to block until we have our result. Or perhaps we haven't decided yet, and want to delay making decisions about the execution model of our application...

Using a higher-kinded type allows us to express this, and also explicitly write how we sequence interactions. Future is a monad, but we don't necessarily need Future to write our code. If we know it is a Monad we can still sequence functions together:

import cats.Monad
import cats.syntax.functor._
import cats.syntax.flatMap._

class UserService[F[_]: Monad](
   getUser: UserId => F[User],
   getTimeline: TimelineId => F[Timeline]){

   def getProfile(id: UserId) = for {
     user <- getUser(id)
     timeline <- getTimeline(user.timelineId)
   } yield UserProfile(user, timeline)
}

The benefit of higher-kinded types is that F doesn't need to be a Future. If you set F as Id, essentially the identity higher-kinded type, then the everything becomes synchronous again.

If the test below was fleshed out, it would pass. The Id monad disappears without having to implement polling or waiting for a Future:

import cats.Id

...

val service = new UserService[Id](getUser, getTimeline)
val profile = service.getProfile(id)
profile.id shouldBe id

...

A whole problem space disappears! You can concentrate on business logic without worrying about flakey tests.

Higher-kinded types are slightly tricky to implement in Play Framework when using Guice, but you can make an @Provider to make sure F is set to your chosen Monad:

@Singleton
@Provides
def usersService(
   getUser: UserId => Future[User],
   getTimeline: TimelineId => Future[TimelineId])
   (implicit ec: ExecutionContext){
   
   import cats.instances.future._
   new UsersService[Future](getUser, getTimeline)
}

Do not throw exceptions

A function that can throw an exception is not a pure function. The great thing about pure functions is that they tend to be easier to reason about. Not using exceptions can increase code clarity.

One way of doing this is to pass back errors via an Either instead of throwing an exception. If the error is an algebraic data type implemented with a sealed trait, that's even better.

sealed trait UserError
case object UserNotFound extends UserError
case class UserBanned(reason: BanReason) extends UserError

def getUser[F](id: UserId): F[Either[UserError, User]] = ...

Combining higher-kinded types with Either works well. If your interfaces return Future[Either[A,B]] you need to be careful whenever the Future may fail and throw an exception. With higher-kinded types, you're just dealing with an F.

Using explicit types instead of exceptions will also eventually lead you into the wonderful world of monad transformers!