The Abstract Future 

One blog post that I occasionally see being mentioned as a poweful application of monad, especially in the context of building large application is The Abstract Future. It was originally posted to the precog.com engineering blog on November 27, 2012 by Kris Nuttycombe (@nuttycom).

At Precog, we use Futures extensively, both in a direct fashion and to allow us a composable way to interact with subsystems that are implemented atop Akka’s actor framework. Futures are arguably one of the best tools we have for reining in the complexity of asynchronous programming, and so our many of our early versions of APIs in our codebase exposed Futures directly. ….

What this means is that from the perspective of the consumer of the DatasetModule interface, the only aspect of Future that we’re relying upon is the ability to order operations in a statically checked fashion; the sequencing, rather than any particular semantics related to Future’s asynchrony, is the relevant piece of information provided by the type. So, the following generalization becomes natural.

Here I’ll use similar example as the Yoshida-san’s.

scala> import cats._, cats.instances.all._
import cats._
import cats.instances.all._

scala> :paste
// Entering paste mode (ctrl-D to finish)
case class User(id: Long, name: String)

// In actual code, probably more than 2 errors
sealed trait Error
object Error {
  final case class UserNotFound(userId: Long) extends Error
  final case class ConnectionError(message: String) extends Error
}
trait UserRepos[F[_]] {
  implicit def F: Monad[F]
  def userRepo: UserRepo
  trait UserRepo {
    def followers(userId: Long): F[List[User]]
  }
}

// Exiting paste mode, now interpreting.

defined class User
defined trait Error
defined object Error
defined trait UserRepos

UserRepos with Future 

Let’s start implementing the UserRepos module using Future.

scala> :paste
// Entering paste mode (ctrl-D to finish)
import scala.concurrent.{ Future, ExecutionContext, Await }
import scala.concurrent.duration.Duration

class UserRepos0(implicit ec: ExecutionContext) extends UserRepos[Future] {
  override val F = implicitly[Monad[Future]]
  override val userRepo: UserRepo = new UserRepo0 {}
  trait UserRepo0 extends UserRepo {
    def followers(userId: Long): Future[List[User]] = Future.successful { Nil }
  }
}

// Exiting paste mode, now interpreting.

import scala.concurrent.{Future, ExecutionContext, Await}
import scala.concurrent.duration.Duration
defined class UserRepos0

Here’s how to use it:

scala> val service = new UserRepos0()(ExecutionContext.global)
service: UserRepos0 = UserRepos0@5ecf0c1d

scala> val xs = service.userRepo.followers(1L)
xs: scala.concurrent.Future[List[User]] = scala.concurrent.impl.Promise$KeptPromise@484fc3b6

Now we have an asynchronous result. Let’s say during testing we would like it to be synchronous.

UserRepos with Id 

In a test, we probably don’t want to worry about the fact that the computation is being performed asynchronously; all that we care about is that we obtain a correct result. ….

For most cases, we’ll use the identity monad for testing. Suppose that we’re testing the piece of functionality described earlier, which has computed a result from the combination of a load, a sort, take and reduce. The test framework need never consider the monad that it’s operating in.

This is where Id datatype can be used.

scala> :paste
// Entering paste mode (ctrl-D to finish)
class TestUserRepos extends UserRepos[Id] {
  override val F = implicitly[Monad[Id]]
  override val userRepo: UserRepo = new UserRepo0 {}
  trait UserRepo0 extends UserRepo {
    def followers(userId: Long): List[User] =
      userId match {
        case 0L => List(User(1, "Michael"))
        case 1L => List(User(0, "Vito"))
        case x =>  sys.error("not found")
      }
  }
}

// Exiting paste mode, now interpreting.

defined class TestUserRepos

Here’s how to use it:

scala> val testRepo = new TestUserRepos {}
testRepo: TestUserRepos = $anon$1@49b99d49

scala> val ys = testRepo.userRepo.followers(1L)
ys: cats.Id[List[User]] = List(User(0,Vito))

Coding in abstract 

Now that we were able to abtract the type constructor of the followers, let’s try implementing isFriends from day 10 that checks if two users follow each other.

scala> :paste
// Entering paste mode (ctrl-D to finish)
trait UserServices[F[_]] { this: UserRepos[F] =>
  def userService: UserService = new UserService
  class UserService {
    def isFriends(user1: Long, user2: Long): F[Boolean] =
      F.flatMap(userRepo.followers(user1)) { a =>
        F.map(userRepo.followers(user2)) { b =>
          a.exists(_.id == user2) && b.exists(_.id == user1)
        }
      }
  }
}

// Exiting paste mode, now interpreting.

defined trait UserServices

Here’s how to use it:

scala> val testService = new TestUserRepos with UserServices[Id] {}
testService: TestUserRepos with UserServices[cats.Id] = $anon$1@297a3171

scala> testService.userService.isFriends(0L, 1L)
res0: cats.Id[Boolean] = true

The above demonstrates that isFriends can be written without knowing anything about F[] apart from the fact that it forms a Monad. It would be nice if I could use infix flatMap and map method while keeping F abstract. I tried creating FlatMapOps(fa) manually, but that resulted in abstract method error during runtime. The actM macro that we implemented on day 6 seems to work ok:

scala> :paste
// Entering paste mode (ctrl-D to finish)
trait UserServices[F[_]] { this: UserRepos[F] =>
  def userService: UserService = new UserService
  class UserService {
    import example.MonadSyntax._
    def isFriends(user1: Long, user2: Long): F[Boolean] =
      actM[F, Boolean] {
        val a = userRepo.followers(user1).next
        val b = userRepo.followers(user2).next
        a.exists(_.id == user2) && b.exists(_.id == user1)
      }
  }
}

// Exiting paste mode, now interpreting.

defined trait UserServices

scala> val testService = new TestUserRepos with UserServices[Id] {}
testService: TestUserRepos with UserServices[cats.Id] = $anon$1@1b971bb9

scala> testService.userService.isFriends(0L, 1L)
res1: cats.Id[Boolean] = true

UserRepos with XorT 

We can also use this with the XorT (aka EitherT) with Future to carry a custom error type.

scala> :paste
// Entering paste mode (ctrl-D to finish)
import cats.data.XorT
class UserRepos1(implicit ec: ExecutionContext) extends UserRepos[XorT[Future, Error, ?]] {
  override val F = implicitly[Monad[XorT[Future, Error, ?]]]
  override val userRepo: UserRepo = new UserRepo1 {}
  trait UserRepo1 extends UserRepo {
    def followers(userId: Long): XorT[Future, Error, List[User]] =
      userId match {
        case 0L => XorT.right(Future { List(User(1, "Michael")) })
        case 1L => XorT.right(Future { List(User(0, "Vito")) })
        case x =>
          XorT.left(Future.successful { Error.UserNotFound(x) })
      }
  }
}

// Exiting paste mode, now interpreting.

import cats.data.XorT
defined class UserRepos1

Here’s how to use it:

scala> val service1 = {
  import ExecutionContext.Implicits._
  new UserRepos1 with UserServices[XorT[Future, Error, ?]] {}
}
service1: UserRepos1 with UserServices[[γ]cats.data.XorT[scala.concurrent.Future,Error,γ]] = $anon$1@4da0e55f

scala> {
  import scala.concurrent.duration._
  Await.result(service1.userService.isFriends(0L, 1L).value, 1 second)
}
res2: cats.data.Xor[Error,Boolean] = Right(true)

scala> {
  import scala.concurrent.duration._
  Await.result(service1.userService.isFriends(0L, 2L).value, 1 second)
}
res3: cats.data.Xor[Error,Boolean] = Left(UserNotFound(2))

Note that for all three versions of services, I was able to reuse the UserServices trait without any changes.

That’s it for today.