Cats Effect
The IO Monad for Cats
About me
- Functional Programmer
- Scala & Haskell
- Occasional writer at
- GitHub repo:
Cats Effect
- Started on April 2017 by Daniel Spiewak
- Current version is 0.5 (Cats 1.0.0-RC1)
- Integrated with most of the Typelevel libs:
- Fs2
- Http4s
- Doobie
- Monix
- Integrated with other projects:
- Sttp
- Fs2 Rabbit
Effects
cats.effect.IO[A]
A pure abstraction representing the intention to perform a side effect, where the result of that side effect may be obtained synchronously (via return) or asynchronously (via callback).
Effects
f(println("hi"), println("hi"))
val x = println("hi")
f(x, x)
import cats.effect.IO
import scala.io.StdIn
val program = for {
_ <- IO { println("Please enter your name:") }
name <- IO { StdIn.readLine }
_ <- IO { println(s"Hi $name!") }
} yield ()
program.unsafeRunSync()
"If replacing expression x by its value produces the same behavior, then x is referentially transparent"
def putStrLn(line: String) = IO { println(line) }
val x = putStrLn("hi")
f(x, x) == f(putStrLn("hi"), putStrLn("hi"))
Effects
Sync[F[_]]
trait Sync[F[_]] extends MonadError[F, Throwable] {
def suspend[A](thunk: => F[A]): F[A]
def delay[A](thunk: => A): F[A] = suspend(pure(thunk))
}
def delayExample[F[_] : Sync]: F[Unit] =
Sync[F].delay {
println("delay(A) === suspend(F[A])")
}
LiftIO[F[_]]
trait LiftIO[F[_]] {
def liftIO[A](ioa: IO[A]): F[A]
}
import monix.eval.Task
type MyEffect[A] = Task[Either[Throwable, A]]
implicit def myEffectLiftIO: LiftIO[MyEffect] =
new LiftIO[MyEffect] {
override def liftIO[A](ioa: IO[A]): MyEffect[A] = {
ioa.attempt.to[Task]
}
}
val ioa: IO[String] = IO("Hello World!")
val effect: MyEffect[String] = LiftIO[MyEffect].liftIO(ioa)
Async[F[_]]
trait Async[F[_]] extends Sync[F] with LiftIO[F] {
def async[A](k: (Either[Throwable, A] => Unit) => Unit): F[A]
def shift(implicit ec: ExecutionContext): F[Unit] = ??? // See next slide
override def liftIO[A](ioa: IO[A]): F[A] = {
ioa.to[F](this)
}
}
val futureOfString = Future.successful("I come from the Future!")
def async[F[_] : Async](implicit ec: ExecutionContext): F[String] =
Async[F].async { cb =>
import scala.util.{Failure, Success}
futureOfString.onComplete {
case Success(value) => cb(Right(value))
case Failure(error) => cb(Left(error))
}
}
Async[F[_]]
trait Async[F[_]] extends Sync[F] with LiftIO[F] {
def shift(implicit ec: ExecutionContext): F[Unit] = {
async { (cb: Either[Throwable, Unit] => Unit) =>
ec.execute(new Runnable {
def run() = cb(Right(()))
})
}
}
}
private val cachedThreadPool = Executors.newCachedThreadPool()
private val BlockingFileIO = ExecutionContext.fromExecutor(cachedThreadPool)
implicit val Main: ExecutionContextExecutor = ExecutionContext.global
def shiftingProgram[F[_] : Async]: F[Unit] =
for {
_ <- Sync[F].delay { println("Enter your name: ")}
_ <- Async[F].shift(BlockingFileIO)
name <- Sync[F].delay { scala.io.StdIn.readLine() }
_ <- Async[F].shift
_ <- Sync[F].delay { println(s"Welcome $name!") }
_ <- Sync[F].delay(cachedThreadPool.shutdown())
} yield ()
Effect[F[_]]
trait Effect[F[_]] extends Async[F] {
def runAsync[A](fa: F[A])(cb: Either[Throwable, A] => IO[Unit]): IO[Unit]
}
def runAsync[F[_] : Effect, A](task: F[A]): IO[Unit] =
Effect[F].runAsync(task) {
case Right(value) => IO(println(value))
case Left(error) => IO.raiseError(error)
}
import monix.eval.Task
val task = Task("Hello World!")
// same as just unsafeRunAsync (recommended in JS)
runAsync(task).unsafeRunSync
Effects
Fs2 concepts
Stream[F,O]
It represents a discrete stream of O values which may request evaluation of F effects.
Pipe[F,I,O]
= Stream[F,I] => Stream[F,O]
Sink[F,I]
= Pipe[F,I,Unit]
Just a streaming transformation!
Its sole purpose is to run effects.
Fs2
class Stream[+F[_], +O]
object Stream {
def run(implicit F: Effect[F]): F[Unit] = ???
def runSync(implicit F: Sync[F]): F[Unit] = ???
def runLast(implicit F: Effect[F]): F[Option[O]] = ???
def runLastSync(implicit F: Sync[F]): F[Option[O]] = ???
def syncInstance[F[_]]: Sync[Stream[F,?]] = new Sync[Stream[F,?]] { ??? }
}
Integration with Cats Effect
val src: Stream[IO, String] = Stream.eval(IO("Hello World!"))
val pipe: Pipe[IO, String, List[String]] = { x => x.map(_.split(" ").toList) }
val sink: Sink[IO, List[String]] = { x => x.map(println) }
val program: Stream[IO, Unit] = src through pipe to sink
val ioa: IO[Unit] = program.run
Http4s
class BaseUserHttpEndpoint extends Http4sDsl[IO] {
val usersErrorHandler: PartialFunction[Throwable, IO[Response[IO]]] = ???
val service: HttpService[IO] = HttpService[IO] {
case GET -> Root / ApiVersion / "users" =>
val users: IO[Users] = userService.findAll()
users.flatMap(u => Ok(u.asJson)).recoverWith(usersErrorHandler)
}
}
Integration with Cats Effect
type HttpService[F[_]] = Kleisli[OptionT[F, ?], Request[F], Response[F]]
trait Http4sClientDsl[F[_]]
object HttpService {
def apply[F[_]](pf: PartialFunction[Request[F], F[Response[F]]])(
implicit F: Applicative[F]): HttpService[F] = ???
}
object BlazeBuilder {
def apply[F[_]](implicit F: Effect[F]): BlazeBuilder[F] = ???
}
Doobie
object fromDriverManager {
private def create[M[_]: Async](driver: String,
conn: => Connection): Transactor.Aux[M, Unit] =
Transactor((), u => Sync[M].delay { Class.forName(driver); conn }
, KleisliInterpreter[M].ConnectionInterpreter, Strategy.default)
def apply[M[_]: Async](driver: String, url: String): Transactor.Aux[M, Unit] =
create(driver, DriverManager.getConnection(url))
}
Integration with Cats Effect
val xa = Transactor.fromDriverManager[IO]("org.postgresql.Driver", "jdbc:postgresql:world")
case class Country(code: String, name: String, population: Long)
def find(n: String): ConnectionIO[Option[Country]] =
sql"select code, name, population from country where name = $n".query[Country].option
val ioa: IO[Option[Country]] = find("France").transact(xa)
Monix
private[eval] object TaskConversions {
def toIO[A](source: Task[A])(implicit s: Scheduler): IO[A] = ???
def fromIO[A](io: IO[A]): Task[A] = io.to[Task]
def fromEffect[F[_], A](fa: F[A])(implicit F: Effect[F]): Task[A] = ???
}
Integration with Cats Effect
import monix.eval.Task
import monix.eval.instances._
class BaseUserHttpEndpoint extends Http4sDsl[Task] {
val usersErrorHandler: PartialFunction[Throwable, Task[Response[Task]]] = ???
val service = HttpService[Task] {
case GET -> Root / ApiVersion / "users" =>
val users: Task[Users] = userService.findAll()
users.flatMap(u => Ok(u.asJson)).recoverWith(usersErrorHandler)
}
}
Sttp
import cats.effect.IO
import com.softwaremill.sttp._
import com.softwaremill.sttp.asynchttpclient.cats.AsyncHttpClientCatsBackend
implicit val backend: SttpBackend[IO, Nothing] = AsyncHttpClientCatsBackend[IO]()
val ioa: IO[Response[String]] = sttp.get(uri"http://httpbin.org/ip").send()
Integration with Cats Effect
object AsyncHttpClientCatsBackend {
private def apply[F[_]: Async](asyncHttpClient: AsyncHttpClient,
closeClient: Boolean): SttpBackend[F, Nothing] =
new FollowRedirectsBackend[F, Nothing](
new AsyncHttpClientCatsBackend(asyncHttpClient, closeClient))
}
Fs2 Rabbit
protected def acquireConnection[F[_] : Sync]: F[(Connection, Channel)] =
Sync[F].delay {
val conn = factory.newConnection
val channel = conn.createChannel
(conn, channel)
}
/**
* Creates a connection and a channel in a safe way using Stream.bracket.
* In case of failure, the resources will be cleaned up properly.
*
* @return An effectful [[fs2.Stream]] of type [[Channel]]
* */
def createConnectionChannel[F[_] : Sync](): Stream[F, Channel] =
Stream.bracket(acquireConnection)(
cc => asyncF[F, Channel](cc._2),
cc => Sync[F].delay {
val (conn, channel) = cc
log.info(s"Releasing connection: $conn previously acquired.")
if (channel.isOpen) channel.close()
if (conn.isOpen) conn.close()
}
)
Integration with Cats Effect
Community's on fire!
- Cats v.1.0.0 final release
- IO instance for cats.Parallel
- sequential: F[_] ~> G[_]
- parallel: G[_] ~> F[_]
- Performance comparison and design choices discussion with new Scalaz IO
- PR submitted 16th of November
- Scalaz 8 expected for June 2018
Resources
Questions?
Cats Effect: The IO Monad for Cats
By Gabriel Volpe
Cats Effect: The IO Monad for Cats
- 4,954