Bring Your Own Effect
Chris Birchall
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] = A
class 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:
val x: Future[Result] = {
val p = Promise[Result]()
makeRemoteApiCall(callback = {
case Left(err) => p.failure(err)
case Right(result) => p.success(result)
})
p.future
}
Aside: "non-blocking"/"async" I/O
Callback-based async I/O
Future
Blocking the current thread
val x = Await.result(future, Duration.Inf)
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 flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] = fa.flatMap(f)
def raiseError[A](t: Throwable): Future[A] = Future.failed(t)
def handleNonFatal[A](fa: => Future[A])(f: Throwable => A): Future[A] = {
fa.recover {
case NonFatal(e) => f(e)
}
}
def delay[A](thunk: => A): Future[A] = Future(thunk)
def suspend[A](thunk: => Future[A]): Future[A] = thunk
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
}
}
class CaffeineCache[V](...) {
def doGet[F[_]](key: String)(implicit mode: Mode[F]): F[Option[V]] = {
mode.M.delay {
val baseValue = underlying.getIfPresent(key)
val result = {
if (baseValue != null) {
val entry = baseValue.asInstanceOf[Entry[V]]
if (entry.isExpired) None else Some(entry.value)
} else None
}
logCacheHitOrMiss(key, result)
result
}
}
...
}
class MemcachedCache[V](...) {
def doGet[F[_]](key: String)(implicit mode: Mode[F]): F[Option[V]] = {
mode.M.async { cb =>
val f = client.asyncGet(keySanitizer.toValidMemcachedKey(key))
f.addListener(new GetCompletionListener {
def onComplete(g: GetFuture[_]): Unit = {
if (g.getStatus.isSuccess) {
try {
val bytes = g.get()
val value = codec.decode(bytes.asInstanceOf[Array[Byte]]).map(Some(_))
cb(value)
} catch {
case NonFatal(e) => cb(Left(e))
}
} else {
g.getStatus.getStatusCode match {
case StatusCode.ERR_NOT_FOUND => cb(Right(None))
case _ => cb(Left(new MemcachedException(g.getStatus.getMessage)))
}
}
}
})
}
}
...
}
Why should I do this?
In a library:
- More flexibility - allow users to choose the F[_] that is most appropriate to their application
In an application:
- Extra dimension of abstraction
- Simplify testing
Extra dimension of abstraction
Decouple:
- Composition of operations (business logic)
- Choice of monad (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]
}
trait UserOps[F[_]] extends Persistence[F] with Events[F] {
implicit def M: Monad[F]
final def createUser(details: UserDetails): F[User] =
for {
userId <- saveNewUser(details)
user <- findUserById(userId)
_ <- sendCreatedUserEvent(user)
} yield user
}
Choice of monad
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]
trait UserService extends UserOps[UserServiceOp]
Concrete impl details
class MyLovelyUserService(db: Database, eventPublisher: EventPublisher)
(implicit def M: Monad[UserUserviceOp])
extends UserService {
def saveNewUser(userDetails: UserDetails): UserServiceOp[UserId] = {
...
}
def findUserById(userId: UserId): UserServiceOp[User] = {
...
}
def sendCreatedUserEvent(user: User): UserServiceOp[Unit] = {
...
}
}
Conclusion
Bring Your Own Effect
By Chris Birchall
Bring Your Own Effect
- 3,194