Bring Your Own Effect
Chris Birchall

    http://lambdale.org/
@lambd_ale
1st September 2018
The good old days
interface Cache<K, V> {
  public Optional<V> get(K key);
  public void put(K key, V value);
}
trait Cache[K, V] {
  def get(key: K): Future[Option[V]]
  def put(key: K, value: V): Future[Unit]
}trait AsyncCache[K, V] {
  def get(key: K): Future[Option[V]]
  def put(key: K, value: V): Future[Unit]
}
trait SyncCache[K, V] {
  def get(key: K): Option[V]
  def put(key: K, value: V): Unit
}
class RedisCache extends AsyncCache { ...}
class CaffeineCache extends SyncCache { ... }trait Cache[K, V] {
  def get(key: K): Future[Option[V]]
  def put(key: K, value: V): Future[Unit]
}
def getSync[K, V](cache: Cache[K, V])
                 (key: K): Option[V] =
  Await.result(cache.get(key), Duration.Inf)
def putSync[K, V](cache: Cache[K, V])
                 (key: K, value: V): Option[V] =
  Await.result(cache.put(key, value), Duration.Inf)
Source: Wikipedia

Source: pxhere.com
trait Cache[K, V] {
  def get[F[_]](key: K): F[Option[V]]
  def put[F[_]](key: K, value: V): F[Unit]
}val cache: Cache[String, Int] = ...
val future: Future[Option[Int]] = cache.get[Future]("foo")
val task: Task[Option[Int]] = cache.get[Task]("foo")val plainOldValue: Option[Int] = cache.get[???]("foo")val plainOldValue: Option[Int] = cache.get[Id]("foo")type Id[A] = Aclass MemcachedCache[K, V] extends Cache[K, V] {
  val memcachedClient = ...
  def get[F[_]](key: K): F[Option[V]] = ???
 
  def put[F[_]](key: K, value: V): F[Unit] = ???
}class MemcachedCache[K, V] extends Cache[K, V] {
  val memcachedClient = ...
  def get[F[_]](key: K)
               (implicit tc: TypeClass[F]): F[Option[V]] = {
    // TODO cache lookup!
    tc.pure(None)
  }
 
  def put[F[_]](key: K, value: V)
               (implicit tc: TypeClass[F]): F[Unit] = {
    // TODO cache write!
    tc.pure(())
  }
}What typeclass do I need?
- Depends on what your code needs to do
 - Use the least powerful tool for the job
 
Functor
Applicative
Monad
MonadError
Sync
Async
def map[A,B](fa: F[A])(f: A => B): F[B]def pure[A](a: A): F[A]def flatMap[A,B](fa: F[A])(f: A => F[B]): F[B]def raiseError[A](e: Throwable): F[A]
def handleError[A](fa: F[A])(f: Throwable => A): F[A]def delay[A](thunk: => A): F[A]def async[A](
  register: (Either[Throwable, A] => Unit) => Unit): F[A]Aside: "non-blocking"/"async" I/O
Blocking the current thread:
val x: Result = makeRemoteApiCall()val x: Future[Result] = Future { makeRemoteApiCall() }Still blocking, but on a different thread:
val x: Unit = makeRemoteApiCall(callback = {
  case Left(err) => println("Oh no!")
  case Right(result) => println("Yay!")
})Callback-based async I/O:
Aside: "non-blocking"/"async" I/O
Callback-based async I/O
Future
Blocking the current thread
(wrap in Await)
(wrap in Future)
(use callback to complete a Promise)
ScalaCache's Async typeclass
trait Async[F[_]] {
  def pure[A](a: A): F[A]
  def map[A, B](fa: F[A])(f: A => B): F[B]
  def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
  def raiseError[A](t: Throwable): F[A]
  def handleNonFatal[A](fa: => F[A])(f: Throwable => A): F[A]
  def delay[A](thunk: => A): F[A]
  def suspend[A](thunk: => F[A]): F[A]
  def async[A](register: (Either[Throwable, A] => Unit) => Unit): F[A]
}class AsyncForFuture(implicit ec: ExecutionContext) 
  extends Async[Future] {
  def pure[A](a: A): Future[A] = Future.successful(a)
  def map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f)
  ...
  def async[A](register: (Either[Throwable, A] => Unit) => Unit)
      : Future[A] = {
    val promise = Promise[A]()
    register {
      case Left(e) => promise.failure(e)
      case Right(x) => promise.success(x)
    }
    promise.future
  }
}Example instance: Future
class CaffeineCache[V](...) {
  def doGet[F[_]](key: String)
                 (implicit mode: Mode[F]): F[Option[V]] =
    mode.M.delay {
      // do the cache lookup ...
    }
}Synchronous example: Caffeine
class MemcachedCache[V](...) {
  def doGet[F[_]](key: String)
                 (implicit mode: Mode[F]): F[Option[V]] =
    mode.M.async { cb =>
      val f = client.asyncGet(key)
      f.addListener(new GetCompletionListener {
        def onComplete(g: GetFuture[_]): Unit = {
          if (g.getStatus.isSuccess) {
            // ... call the callback with the result
          } else {
            // ... call the callback with the error
          }
        }
      })
    }
}Callback example: Memcached
Why should I do this?
| In a library | - More flexibility for users | 
| In an application | - Extra dimension of abstraction - More testable  | 
Extra dimension of abstraction
Decouple:
- Composition of operations (business logic)
 - Choice of effects (error handling, context passing)
 - Concrete implementation details
 
Composition of operations
trait Persistence[F[_]] {
  def saveNewUser(details: UserDetails): F[UserId]
  def findUserById(userId: UserId): F[User]
}
trait Events[F[_]] {
  def sendCreatedUserEvent(user: User): F[Unit]
}
class UserOps[F[_]: Monad](
  persistence: Persistence[F],
  events: Events[F]) {
  import persistence._, events._
  def createUser(details: UserDetails): F[User] =
    for {
      userId <- saveNewUser(details)
      user   <- findUserById(userId)
      _      <- sendCreatedUserEvent(user)
    } yield user
}Choice of effects
sealed trait UserServiceFailure
case object NotFound extends UserServiceFailure
case class InternalError(e: Throwable) extends UserServiceFailure
type UserServiceResult[A] = Either[UserServiceFailure, A]case class Context(traceToken: String)
type UserServiceOp[A] = Kleisli[UserServiceResult, Context, A]Error handling
Context passing
Concrete impl details
class DbPersistence(db: Database) extends Persistence[UserServiceOp] {
  def saveNewUser(userDetails: UserDetails): UserServiceOp[UserId] = {
    ...
  }
  def findUserById(userId: UserId): UserServiceOp[User] = {
    ...
  }
}class KafkaEvents(config: KafkaConfig) extends Events[UserServiceOp] {
  def sendCreatedUserEvent(user: User): UserServiceOp[Unit] = {
    ...
  }
}Bring it all together
type UserService = UserOps[UserServiceOps]
val userService = new UserService(dbPersistence, kafkaEvents)
val userDetails: UserDetails = ...
val program: UserServiceOp[Unit] = userService.createUser(userDetails)
// remember, UserServiceOp[A] = Kleisli[UserServiceResult, Context, A]
program.run(Context(...)) // returns a UserServiceResult[Unit]Conclusion
Bring Your Own Effect - Scalar 2018
By Chris Birchall
Bring Your Own Effect - Scalar 2018
- 3,455