Cats Effect

The IO Monad for Cats

About me

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?