--- class: center, middle # HTTP applications are just a Kleisli function from a streaming request to a polymorphic effect of a streaming response. ## So what's the problem? Ross A. Baker • [`@rossabaker`](https://twitter.com/rossabaker) • [Formation](http://formation.ai/careers) ??? - notes here - see https://remarkjs.com/#1 --- ## Who am I? - 2009: began writing Scala - 2010: helped start [IndyScala](https://github.com/indyscala/) meetup - 2011: lost source code to Scala 2.8 app, still running today - 2012: began writing Scala professionally - 2013: started [http4s](http://http4s.org/) project - 2014: got more functional - 2015: got more functional - 2016: got more functional - 2017: got more functional - 2018: started writing Scala and Haskell at [Formation](http://formation.ai) --- ## What is http4s? - Inspired by: - Java's [servlets](https://docs.oracle.com/javaee/6/tutorial/doc/bnafd.html) - Clojure's [Ring](https://github.com/ring-clojure/ring/blob/master/SPEC) - Ruby's [Rack](https://rack.github.io/) - Python's [WSGI](http://wsgi.readthedocs.io/en/latest/) - Haskell's [WAI](https://hackage.haskell.org/package/wai) - Multiple frontends: - http4s-dsl - [rho](https://github.com/http4s/rho) - pure functions - Backends - Server - [blaze](https://github.com/http4s/blaze) - Jetty - Tomcat - Client - blaze - async-http-client --- ## Version 0.0.1 An HTTP application is just a function from a request to a response. ```scala case class Request( method: Method, uri: Uri, // headers, // httpVersion, body: String = "", ) case class Response( status: Status, // headers, // httpVersion, body: String = "", ) type HttpApp = Request => Response ``` --- ## Hello World ```scala val helloWorld: HttpApp = { case Request(POST, Uri("/hello"), name) => Response(OK, s"Hello, $name!") case _ => Response(NotFound) } ``` --- ## Hello World ```scala val helloWorld: HttpApp = { case Request(POST, Uri("/hello"), name) => Response(OK, s"Hello, $name!") case _ => Response(NotFound) } ``` ### Unit test ```scala val req = Request(POST, Uri("/hello"), "Boston") // req: Request = Request(POST,Uri(/hello),Boston) assert("Hello, Boston!", helloWorld(req).body) // ✔ Hello, Boston! ``` --- ## Hello World ```scala val helloWorld: HttpApp = { case Request(POST, Uri("/hello"), name) => Response(OK, s"Hello, $name!") case _ => Response(NotFound) } ``` ### Load test ```scala loadTest(helloWorld) // 😎😎😎😎😎😎😎 ``` --- ## Async ```scala val app: HttpApp = { case Request(POST, Uri("/translate"), text) => val resp = Translator.future(text).map(Response(OK, _)) Await.result(resp, 3.seconds) case _ => Response(NotFound) } ``` --- ## Async ```scala val app: HttpApp = { case Request(POST, Uri("/translate"), text) => val resp = Translator.future(text).map(Response(OK, _)) Await.result(resp, 3.seconds) case _ => Response(NotFound) } ``` ### Unit test ```scala val req = Request(POST, Uri("/translate"), "one") // req: Request = Request(POST,Uri(/translate),one) assert("uno", app(req).body) // ✔ uno ``` --- ## Async ```scala val app: HttpApp = { case Request(POST, Uri("/translate"), text) => val resp = Translator.future(text).map(Response(OK, _)) Await.result(resp, 3.seconds) case _ => Response(NotFound) } ``` ### Load test ```scala loadTest(app) // res5: String = Await is bad and you should feel bad. ``` --- ## Version 0.0.2 An HTTP application is just a function from a request to a future response. ```scala type HttpApp = Request => Future[Response] ``` --- ## Async ```scala val app: HttpApp = { case Request(POST, Uri("/translate"), text) => Translator.future(text).map(Response(OK, _)) case _ => Future.successful(Response(NotFound)) } ``` --- ## Async ```scala val app: HttpApp = { case Request(POST, Uri("/translate"), text) => Translator.future(text).map(Response(OK, _)) case _ => Future.successful(Response(NotFound)) } ``` ### Unit test ```scala val req = Request(POST, Uri("/translate"), "one") // req: Request = Request(POST,Uri(/translate),one) asyncAssert("uno", app(req).map(_.body)) // ✔ uno ``` --- ## Async ```scala val app: HttpApp = { case Request(POST, Uri("/translate"), text) => Translator.future(text).map(Response(OK, _)) case _ => Future.successful(Response(NotFound)) } ``` ### Load test ```scala loadTest(app) // res7: String = ✨✨✨ IT'S REACTIVE ✨✨✨ ``` --- ## Combination ```scala def combine(x: HttpApp, y: HttpApp): HttpApp = { req => try x(req) catch { case e: MatchError => y(req) } } val helloWorld: HttpApp = { case Request(POST, Uri("/hello"), name) => Future.successful(Response(OK, s"Hello, $name!")) } val holaMundo: HttpApp = { case Request(POST, Uri("/hola"), name) => Future.successful(Response(OK, s"¡Hola, $name!")) } val app: HttpApp = combine(helloWorld, holaMundo) ``` --- ## Combination ```scala def combine(x: HttpApp, y: HttpApp): HttpApp = { req => try x(req) catch { case e: MatchError => y(req) } } val helloWorld: HttpApp = { case Request(POST, Uri("/hello"), name) => Future.successful(Response(OK, s"Hello, $name!")) } val holaMundo: HttpApp = { case Request(POST, Uri("/hola"), name) => Future.successful(Response(OK, s"¡Hola, $name!")) } val app: HttpApp = combine(helloWorld, holaMundo) ``` ### Unit test ```scala val req = Request(POST, Uri("/hello"), "Boston") // req: Request = Request(POST,Uri(/hello),Boston) asyncAssert("Hello, Boston!", app(req).map(_.body)) // ✔ Hello, Boston! ``` --- ## Combination ```scala def combine(x: HttpApp, y: HttpApp): HttpApp = { req => try x(req) catch { case e: MatchError => y(req) } } val helloWorld: HttpApp = { case Request(POST, Uri("/hello"), name) => Future.successful(Response(OK, s"Hello, $name!")) } val holaMundo: HttpApp = { case Request(POST, Uri("/hola"), name) => Future.successful(Response(OK, s"¡Hola, $name!")) } val app: HttpApp = combine(helloWorld, holaMundo) ``` ### Unit test ```scala val req = Request(POST, Uri("/hola"), "Boston") // req: Request = Request(POST,Uri(/hola),Boston) asyncAssert("¡Hola, Boston!", app(req).map(_.body)) // ✔ ¡Hola, Boston! ``` --- ## Version 0.0.3 An HTTP application is just a function from a request to a future response. ```scala type HttpApp = Request => Future[Response] ``` HTTP routes are just a partial function from a request to a future response. ```scala type HttpRoutes = PartialFunction[Request, Future[Response]] ``` --- ## Combination ```scala def combine(x: HttpRoutes, y: HttpRoutes): HttpRoutes = x orElse y val helloWorld: HttpRoutes = { case Request(POST, Uri("/hello"), name) => Future.successful(Response(OK, s"Hello, $name!")) } val holaMundo: HttpRoutes = { case Request(POST, Uri("/hola"), name) => Future.successful(Response(OK, s"¡Hola, $name!")) } val app: HttpApp = combine(helloWorld, holaMundo) ``` --- ## Combination ```scala def combine(x: HttpRoutes, y: HttpRoutes): HttpRoutes = x orElse y val helloWorld: HttpRoutes = { case Request(POST, Uri("/hello"), name) => Future.successful(Response(OK, s"Hello, $name!")) } val holaMundo: HttpRoutes = { case Request(POST, Uri("/hola"), name) => Future.successful(Response(OK, s"¡Hola, $name!")) } val app: HttpApp = combine(helloWorld, holaMundo) ``` ### Unit test ```scala val req = Request(POST, Uri("/guten-tag"), "Boston") // req: Request = Request(POST,Uri(/guten-tag),Boston) ``` ```scala asyncAssert("Guten Tag, Boston!", app(req).map(_.body)) // scala.MatchError: Request(POST,Uri(/guten-tag),Boston) (of class Request) // at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:254) // at scala.PartialFunction$$anon$1.apply(PartialFunction.scala:252) // at $anonfun$1.applyOrElse(
:40) // at $anonfun$1.applyOrElse(
:40) // at scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:34) // at $anonfun$1.applyOrElse(
:41) // at $anonfun$1.applyOrElse(
:41) // at scala.PartialFunction$OrElse.apply(PartialFunction.scala:168) // ... 43 elided ``` --- ## Version 0.0.4 An HTTP application is just a function from a request to a future response. ```scala type HttpApp = Request => Future[Response] ``` HTTP routes are just a function from a request to an optional future response. ```scala type HttpRoutes = Request => Option[Future[Response]] object HttpRoutes { def of(pf: PartialFunction[Request, Future[Response]]): HttpRoutes = pf.lift } ``` --- ## Combination ```scala def combine(x: HttpRoutes, y: HttpRoutes): HttpRoutes = req => x(req).orElse(y(req)) val helloWorld: HttpRoutes = HttpRoutes.of { case Request(POST, Uri("/hello"), name) => Future.successful(Response(OK, s"Hello, $name!")) } val holaMundo: HttpRoutes = HttpRoutes.of { case Request(POST, Uri("/hola"), name) => Future.successful(Response(OK, s"¡Hola, $name!")) } ``` ```scala val app: HttpApp = combine(helloWorld, holaMundo) //
:39: error: type mismatch; // found : HttpRoutes // (which expands to) Request => Option[scala.concurrent.Future[Response]] // required: HttpApp // (which expands to) Request => scala.concurrent.Future[Response] // val app: HttpApp = combine(helloWorld, holaMundo) // ^ ``` --- ## Combination ```scala val helloWorld: HttpRoutes = HttpRoutes.of { case Request(POST, Uri("/hello"), name) => Future.successful(Response(OK, s"Hello, $name!")) } val holaMundo: HttpRoutes = HttpRoutes.of { case Request(POST, Uri("/hola"), name) => Future.successful(Response(OK, s"¡Hola, $name!")) } def combine(x: HttpRoutes, y: HttpRoutes): HttpRoutes = req => x(req).orElse(y(req)) def seal(routes: HttpRoutes): HttpApp = routes.andThen(_.getOrElse( Future.successful(Response(NotFound)))) val app: HttpApp = seal(combine(helloWorld, holaMundo)) ``` ### Unit test ```scala val req = Request(POST, Uri("/guten-tag"), "Boston") // req: Request = Request(POST,Uri(/guten-tag),Boston) asyncAssert(NotFound, app(req).map(_.status)) // ✔ NotFound ``` --- ## Composition ```scala def translate(app: HttpApp): HttpApp = { app.andThen(future => for { resp <- future tx <- Translator.future(resp.body) } yield resp.copy(body = tx))} def hello(theUri: Uri): HttpRoutes = HttpRoutes.of { case Request(POST, uri, name) if uri == theUri => Future.successful(Response(OK, s"Hello, $name!")) } val app: HttpApp = translate(seal(hello(Uri("/hola")))) ``` --- ## Composition ```scala def translate(app: HttpApp): HttpApp = { app.andThen(future => for { resp <- future tx <- Translator.future(resp.body) } yield resp.copy(body = tx))} def hello(theUri: Uri): HttpRoutes = HttpRoutes.of { case Request(POST, uri, name) if uri == theUri => Future.successful(Response(OK, s"Hello, $name!")) } val app: HttpApp = translate(seal(hello(Uri("/hola")))) ``` ### Unit test ```scala val req = Request(POST, Uri("/hola"), "Boston") // req: Request = Request(POST,Uri(/hola),Boston) asyncAssert("¡Hola, Boston!", app(req).map(_.body)) // ✔ ¡Hola, Boston! ``` --- ## Composition ```scala def translate(app: HttpApp): HttpApp = { app.andThen(future => for { resp <- future tx <- Translator.future(resp.body) } yield resp.copy(body = tx))} def hello(theUri: Uri): HttpRoutes = HttpRoutes.of { case Request(POST, uri, name) if uri == theUri => Future.successful(Response(OK, s"Hello, $name!")) } val en: HttpRoutes = hello(Uri("/hello")) ``` ```scala val es: HttpRoutes = translate(hello(Uri("/hola"))) //
:38: error: type mismatch; // found : HttpRoutes // (which expands to) Request => Option[scala.concurrent.Future[Response]] // required: HttpApp // (which expands to) Request => scala.concurrent.Future[Response] // val es: HttpRoutes = translate(hello(Uri("/hola"))) // ^ ``` --- ## Kleisli Quoth the [Cats docs](https://typelevel.org/cats/datatypes/kleisli.html): > Kleisli enables composition of functions that return a monadic > value ```scala case class Kleisli[F[_], A, B])(run: A => F[B]) ``` > We may also have several functions which depend on some environment > and want a nice way to compose these functions to ensure they all > receive the same environment. --- ## Version 0.0.5 HTTP is just a Kleisli function from a request to a polymorphic effect of a response. ```scala type Http[F[_]] = Kleisli[F, Request, Response] ``` An HTTP application is just HTTP in a future effect. ```scala type HttpApp = Http[Future] object HttpApp { def apply(f: Request => Future[Response]): HttpApp = Kleisli(f) } ``` HTTP routes are just HTTP in an optional future. ```scala case class OptionFuture[A](value: Option[Future[A]]) type HttpRoutes = Http[OptionFuture] object HttpRoutes { def of(pf: PartialFunction[Request, Future[Response]]): HttpRoutes = Kleisli(pf.lift.andThen(OptionFuture(_))) } ``` --- ## Polymorphic Composition ```scala def translate[F[_]: Monad](app: Http[F]): Http[F] = for { resp <- app tx <- Kleisli.liftF(Translator[F](resp.body)) } yield resp.copy(body = tx) def hello(theUri: Uri): HttpRoutes = HttpRoutes.of { case Request(POST, uri, name) if uri == theUri => Future.successful(Response(OK, s"Hello, $name!")) } ``` --- ## Polymorphic Composition ```scala def translate[F[_]: Monad](app: Http[F]): Http[F] = for { resp <- app tx <- Kleisli.liftF(Translator[F](resp.body)) } yield resp.copy(body = tx) def hello(theUri: Uri): HttpRoutes = HttpRoutes.of { case Request(POST, uri, name) if uri == theUri => Future.successful(Response(OK, s"Hello, $name!")) } ``` ```scala val es = translate(hello(Uri("/hola"))) //
:37: error: could not find implicit value for evidence parameter of type cats.Monad[OptionFuture] // val es = translate(hello(Uri("/hola"))) // ^ ``` --- ## Version 0.0.6 HTTP is just a Kleisli function from a request to a polymorphic effect of a response. ```scala type Http[F[_]] = Kleisli[F, Request, Response] ``` An HTTP application is just HTTP in a future effect. ```scala type HttpApp = Http[Future] ``` HTTP routes are just HTTP in an future option. ```scala case class FutureOption[A](value: Future[Option[A]]) type HttpRoutes = Http[FutureOption] object HttpRoutes { def of(pf: PartialFunction[Request, Future[Response]]): HttpRoutes = Kleisli(req => FutureOption(pf.lift(req).sequence)) } implicit val monadForFutureOption: Monad[FutureOption] = new Monad[FutureOption] with StackSafeMonad[FutureOption] { def pure[A](x: A) = FutureOption(Future.successful(Some(x))) def flatMap[A, B](fa: FutureOption[A])(f: A => FutureOption[B]): FutureOption[B] = { FutureOption(fa.value.flatMap { case Some(a) => f(a).value case None => Future.successful(None) })}} ``` --- ## Polymorphic Composition ```scala def translate[F[_]: Monad](app: Http[F]): Http[F] = for { resp <- app tx <- Kleisli.liftF(Translator[F](resp.body)) } yield resp.copy(body = tx) def hello(theUri: Uri): HttpRoutes = HttpRoutes.of { case Request(POST, uri, name) if uri == theUri => Future.successful(Response(OK, s"Hello, $name!")) } ``` ```scala seal(translate(hello(Uri("/hola")))): HttpApp // res37: HttpApp = Kleisli(scala.Function1$$Lambda$3220/1659487287@3208201a) translate(seal(hello(Uri("/hola")))): HttpApp // res38: HttpApp = Kleisli(cats.data.Kleisli$$Lambda$10742/1012291153@1d8e07eb) ``` --- ## SemigroupK Hey, what's this funny thing? ```scala private[data] sealed trait KleisliSemigroupK[F[_], A] extends SemigroupK[Kleisli[F, A, ?]] { implicit def F: SemigroupK[F] override def combineK[B](x: Kleisli[F, A, B], y: Kleisli[F, A, B]): Kleisli[F, A, B] = Kleisli(a => F.combineK(x.run(a), y.run(a))) } ``` Quoth the [Cats docs](https://typelevel.org/cats/typeclasses/semigroupk.html) > `SemigroupK[Option]` operates on `Option` where the inner type is not fully specified and can be anything (i.e. is "universally quantified")... in the case of `Option` the `SemigroupK[Option].combineK` method has no choice but to use the `orElse` method of Option --- ## Combination with SemigroupK ```scala implicit val SemigroupKForFutureOption: SemigroupK[FutureOption] = new SemigroupK[FutureOption] { def combineK[A](x: FutureOption[A], y: FutureOption[A]): FutureOption[A] = FutureOption(x.value.flatMap { case Some(a) => Future.successful(Some(a)) case None => y.value }) } val en: HttpRoutes = hello(Uri("/hello")) val es: HttpRoutes = translate(hello(Uri("/hola"))) val app: HttpApp = seal(en.combineK(es)) ``` --- ## Combination with SemigroupK ```scala implicit val SemigroupKForFutureOption: SemigroupK[FutureOption] = new SemigroupK[FutureOption] { def combineK[A](x: FutureOption[A], y: FutureOption[A]): FutureOption[A] = FutureOption(x.value.flatMap { case Some(a) => Future.successful(Some(a)) case None => y.value }) } val en: HttpRoutes = hello(Uri("/hello")) val es: HttpRoutes = translate(hello(Uri("/hola"))) val app: HttpApp = seal(en.combineK(es)) ``` ### Unit test ```scala val req = Request(POST, Uri("/hola"), "Boston") // req: Request = Request(POST,Uri(/hola),Boston) asyncAssert("¡Hola, Boston!", app(req).map(_.body)) // ✔ ¡Hola, Boston! ``` --- ## Combination with SemigroupK ```scala def log[F[_]: Monad](app: Http[F]): Http[F] = app.mapF(Monad[F].pure(println("TRANSLATING")) *> _) val en: HttpRoutes = hello(Uri("/hello")) val es: HttpRoutes = log(translate(hello(Uri("/hola")))) val app: HttpApp = seal(en.combineK(es)) ``` --- ## Combination with SemigroupK ```scala def log[F[_]: Monad](app: Http[F]): Http[F] = app.mapF(Monad[F].pure(println("TRANSLATING")) *> _) val en: HttpRoutes = hello(Uri("/hello")) val es: HttpRoutes = log(translate(hello(Uri("/hola")))) val app: HttpApp = seal(en.combineK(es)) ``` ### Unit test ```scala val req = Request(POST, Uri("/hello"), "Boston") // req: Request = Request(POST,Uri(/hello),Boston) asyncAssert("Hello, Boston!", app(req).map(_.body)) // TRANSLATING // ✔ Hello, Boston! ``` --- ## Sync Quoth the [cats-effect docs](https://github.com/typelevel/cats-effect/blob/master/site/src/main/tut/typeclasses/sync.md): > A `Monad` that can suspend the execution of side effects in the `F[_]` context. ## IO Quoth the [cats-effect docs](https://github.com/typelevel/cats-effect/blob/master/site/src/main/tut/typeclasses/io.md): > Effects contained within this abstraction are not evaluated until the "end of the world", which is to say, when one of the "unsafe" methods are used. --- ## Version 0.0.7 HTTP is just a Kleisli function from a request to a polymorphic effect of a response. ```scala type Http[F[_]] = Kleisli[F, Request, Response] ``` An HTTP application is just HTTP in an IO effect. ```scala type HttpApp = Http[IO] ``` HTTP routes are just HTTP in IO of an Option. ```scala case class IOOption[A](value: IO[Option[A]]) type HttpRoutes = Http[IOOption] object HttpRoutes { def of(pf: PartialFunction[Request, IO[Response]]): HttpRoutes = Kleisli(req => IOOption(pf.lift(req).sequence)) } ``` --- ## Another stack, another artisanally handcrafted instance ```scala implicit val syncForIOOption: Sync[IOOption] with SemigroupK[IOOption] = new Sync[IOOption] with StackSafeMonad[IOOption] with SemigroupK[IOOption] { def pure[A](a: A): IOOption[A] = IOOption(IO.pure(Some(a))) def handleErrorWith[A](fa: IOOption[A])(f: Throwable => IOOption[A]): IOOption[A] = IOOption(fa.value.handleErrorWith(f(_).value)) def raiseError[A](e: Throwable): IOOption[A] = IOOption(IO.raiseError(e).map(Some(_))) def suspend[A](thunk: => IOOption[A]): IOOption[A] = IOOption(IO.suspend(thunk.value)) def flatMap[A, B](fa: IOOption[A])(f: A => IOOption[B]): IOOption[B] = IOOption(fa.value.flatMap { case Some(a) => f(a).value case None => IO.pure(None) }) def combineK[A](x: IOOption[A], y: IOOption[A]): IOOption[A] = IOOption(x.value.flatMap { case Some(a) => IO.pure(Some(a)) case None => y.value }) } ``` --- ## IO ```scala def log[F[_]: Sync](app: Http[F]): Http[F] = app.mapF(Sync[F].delay(println("TRANSLATE")) *> _) def hello(theUri: Uri): HttpRoutes = HttpRoutes.of { case Request(POST, uri, name) if uri == theUri => IO(Response(OK, s"Hello, $name!")) } val en: HttpRoutes = hello(Uri("/hello")) val es: HttpRoutes = log(translate(hello(Uri("/hola")))) val app: HttpApp = seal(en.combineK(es)) ``` --- ## IO ```scala def log[F[_]: Sync](app: Http[F]): Http[F] = app.mapF(Sync[F].delay(println("TRANSLATE")) *> _) def hello(theUri: Uri): HttpRoutes = HttpRoutes.of { case Request(POST, uri, name) if uri == theUri => IO(Response(OK, s"Hello, $name!")) } val en: HttpRoutes = hello(Uri("/hello")) val es: HttpRoutes = log(translate(hello(Uri("/hola")))) val app: HttpApp = seal(en.combineK(es)) ``` ### Unit test ```scala val req = Request(POST, Uri("/hello"), "Boston") // req: Request = Request(POST,Uri(/hello),Boston) asyncAssert("Hello, Boston!", app(req).map(_.body).unsafeToFuture) // ✔ Hello, Boston! ``` --- ## OptionT Quoth the [Cats docs](https://typelevel.org/cats/datatypes/optiont.html) > `OptionT[F[_], A]` is a light wrapper on an `F[Option[A]]`. Speaking technically, it is a monad transformer for `Option`. --- ## Version 0.0.8 HTTP is just a Kleisli function from a request to a polymorphic effect of a response. ```scala type Http[F[_]] = Kleisli[F, Request, Response] ``` An HTTP application is just HTTP in IO effect. ```scala type HttpApp[F[_]] = Http[F] ``` HTTP routes are just HTTP in an Option monad transformer over a polymorphic context. ```scala type HttpRoutes[F[_]] = Http[OptionT[F, ?]] object HttpRoutes { def of[F[_]: Sync](pf: PartialFunction[Request, F[Response]]): HttpRoutes[F] = Kleisli(req => OptionT(pf.lift(req).sequence)) } ``` --- ## OptionT[IO, ?] ```scala def hello[F[_]: Sync](theUri: Uri): HttpRoutes[F] = HttpRoutes.of { case Request(POST, uri, name) if uri == theUri => Response(OK, s"Hello, $name!").pure[F] } val en: HttpRoutes[IO] = hello(Uri("/hello")) val es: HttpRoutes[IO] = translate(hello(Uri("/hola"))) val app: HttpApp[IO] = seal(en.combineK(es)) ``` --- ## OptionT[IO, ?] ```scala def hello[F[_]: Sync](theUri: Uri): HttpRoutes[F] = HttpRoutes.of { case Request(POST, uri, name) if uri == theUri => Applicative[F].pure(Response(OK, s"Hello, $name!")) } val en: HttpRoutes[IO] = hello(Uri("/hello")) val es: HttpRoutes[IO] = translate(hello(Uri("/hola"))) val app: HttpApp[IO] = seal(en.combineK(es)) ``` ### Unit test ```scala val req = Request(POST, Uri("/hello"), "Boston") // req: Request = Request(POST,Uri(/hello),Boston) asyncAssert("Hello, Boston!", app(req).map(_.body).unsafeToFuture) // ✔ Hello, Boston! ``` --- ## OptionT[Task, ?] ```scala def hello[F[_]: Sync](theUri: Uri): HttpRoutes[F] = HttpRoutes.of { case Request(POST, uri, name) if uri == theUri => Response(OK, s"Hello, $name!").pure[F] } val en: HttpRoutes[Task] = hello(Uri("/hello")) val es: HttpRoutes[Task] = translate(hello(Uri("/hola"))) val app: HttpApp[Task] = seal(en.combineK(es)) ``` ### Unit test ```scala val req = Request(POST, Uri("/hello"), "Boston") // req: Request = Request(POST,Uri(/hello),Boston) asyncAssert("Hello, Boston!", app(req).map(_.body).runAsync) // ✔ Hello, Boston! ``` --- ## Translation ```scala def app[F[_]: Sync]: HttpApp[F] = seal(HttpRoutes.of { case Request(POST, Uri("/translate"), text) => Translator[F](text).map(Response(OK, _)) }) ``` ```scala app[IO].run(Request(POST, Uri("/translate"), PaulReveresRide)).unsafeRunSync // res55: Response = // Response(OK, // Escuchen, hijos míos, y oirán // Del paseo de medianoche de Paul Revere, // El dieciocho de abril, en Setenta y cinco: // Apenas un hombre está vivo // ¿Quién recuerda ese famoso día y año? // // Le dijo a su amigo: "Si la marcha británica // Por tierra o mar desde el pueblo esta noche, // Cuelgue una linterna en el arco campanario // De la Torre de la Iglesia del Norte, como una luz de señal, - // Uno si por tierra, y dos si por mar; // Y yo en la orilla opuesta, // Listo para montar y difundir la alarma // A través de cada aldea y granja de Middlesex, // Para que la gente del campo se levante y se arme ". // // Luego dijo "¡Buenas noches!" Y con el remo amortiguado // En silencio remado a la orilla de Charlestown, // Justo cuando la luna se elevaba sobre la bahía, // Donde se balanc... ``` --- ## Streaming bodies ```scala import fs2.Stream case class Request[F[_]]( method: Method, uri: Uri, // headers, // httpVersion, body: Stream[F, Byte], ) case class Response[F[_]]( status: Status, // headers, // httpVersion, body: Stream[F, Byte], ) ``` --- ## I'm probably almost out of time. Final version. HTTP is just a Kleisli function from a streaming request to a polymorphic effect of a streaming response. ```scala type Http[F[_], G[_]] = Kleisli[F, Request[G], Response[G]] ``` An HTTP application is just HTTP where the response effect is the stream effect. ```scala type HttpApp[F[_]] = Http[F, F] ``` HTTP routes are just HTTP where the response context is an Option monad transformer applied to the stream effect. ```scala type HttpRoutes[F[_]] = Http[OptionT[F, ?], F] object HttpRoutes { def of[F[_]: Sync](pf: PartialFunction[Request[F], F[Response[F]]]): HttpRoutes[F] = Kleisli(req => OptionT(Sync[F].suspend(pf.lift(req).sequence))) } ``` --- ## Streaming translation ```scala def seal[F[_]: Functor](routes: HttpRoutes[F]): HttpApp[F] = routes.mapF(_.getOrElse(Response(NotFound, Stream.empty))) ``` ```scala def app[F[_]: Sync]: HttpApp[F] = seal(HttpRoutes.of { case Request(POST, Uri("/translate"), body) => Response(OK, Translator.streaming[F]( body.through(fs2.text.utf8Decode) ).through(fs2.text.utf8Encode)).pure[F] }) ``` --- ## Unit test ```scala val ioResp = app[IO].run(Request(POST, Uri("/translate"), PaulReveresStream)) // ioResp: cats.effect.IO[Response[cats.effect.IO]] =
(Stream.eval(ioResp) // => Stream[F, Response[F]] .flatMap(_.body) // => Stream[F, Byte] .through(fs2.text.utf8Decode) // => Stream[F, String] .take(5) // => Stream[F, String] .compile.toList // => F[List[String]] .map(_.mkString("\n")) // => F[String] .unsafeRunSync) // => String // res58: String = // Escuchen, hijos míos, y oirán // Del paseo de medianoche de Paul Revere, // El dieciocho de abril, en Setenta y cinco: // Apenas un hombre está vivo // ¿Quién recuerda ese famoso día y año? ``` --- ## PureConfig ``` demo-conf { url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", username = "sa", password = "" } ``` ```scala import pureconfig._, pureconfig.module.catseffect._ // import pureconfig._ // import pureconfig.module.catseffect._ case class DbConf(url: String, username: String, password: String) // defined class DbConf val config = loadConfigF[IO, DbConf] // config: cats.effect.IO[DbConf] = IO$612004103 ``` --- ## Doobie ```scala import doobie._, doobie.implicits._, doobie.h2._ // import doobie._ // import doobie.implicits._ // import doobie.h2._ def transactor(conf: DbConf): IO[Transactor[IO]] = H2Transactor.newH2Transactor[IO](conf.url, conf.username, conf.password) // transactor: (conf: DbConf)cats.effect.IO[doobie.Transactor[cats.effect.IO]] def numbers(transactor: Transactor[IO]): Stream[IO, (Int, String)] = sql"select id, en from numbers order by id".query[(Int, String)].stream.transact(transactor) // numbers: (transactor: doobie.Transactor[cats.effect.IO])fs2.Stream[cats.effect.IO,(Int, String)] ``` --- ## jawn-fs2 ```scala import fs2.Chunk import _root_.io.circe.Json import _root_.io.circe.jawn.CirceSupportParser.facade import jawnfs2._ ``` ```scala Stream.emit("""[{"one":1},{"two":2},{"three":3}]"""). covary[IO]. unwrapJsonArray. evalMap(s => IO(println(s))). compile.drain.unsafeRunSync // { // "one" : 1 // } // { // "two" : 2 // } // { // "three" : 3 // } ``` --- ## A real http4s API ```scala import io.circe.literal._ import org.http4s._, import org.http4s.circe._ def app(xa: Transactor[IO]) = HttpService[IO] { case req => val nums = numbers(xa).map { case (i, n) => json"""{"int": $i, "name": $n}""" } Response(Ok).withBody(nums) } ``` --- ## All together now ```scala def parseResponse(resp: Response[IO]): IO[List[Json]] = { resp.body.chunks.parseJsonStream .take(3) .compile.toList } val prog = for { c <- config xa <- H2Transactor.newH2Transactor[IO](c.url, c.username, c.password) _ <- DoobieExample.setUp(xa) server <- BlazeBuilder[IO].mountService(app(xa)).bindHttp(8080).start client <- Http1Client[IO]() req <- GET(uri("http://localhost:8080/")) nums <- client.fetch(req)(resp => parseResponse(resp)) _ <- client.shutdown _ <- server.shutdown } yield nums ``` --- ## All together now ```scala prog.unsafeRunSync() // res62: List[io.circe.Json] = // List({ // "int" : 1, // "name" : "one" // }, { // "int" : 2, // "name" : "two" // }, { // "int" : 3, // "name" : "three" // }) ``` --- # Bonus talking point in case I talked too fast Thanks to [@iravid](https://gist.github.com/iravid/7c4b3d0bbd5a9de058bd7a5534073b4d) for this observation: ```scala abstract class ContT[F[_], R, A] { def run(cont: A => F[R]): F[R] def runPure(cont: A => R)(implicit F: Applicative[F]): F[R] = run((a: A) => cont(a).pure[F]) } pathPrefix("hello") run { _ => get runPure { _ => complete("OK") } } ``` --- # Acknowledgements Acknowledgements Thanks to | GitHub | Twitter --------------- | ------ | ------------- ![](https://avatars3.githubusercontent.com/u/538571?s=24&v=4) Arya Irani | [@aryairani](https://github.com/aryairani) | [@aryairani](https://twitter.com/aryairani) ![](https://avatars3.githubusercontent.com/u/1432894?s=24&v=4) Bjørn Madsen | [@aeons](https://github.com/aeons) | [@bjoernmadsen](https://twitter.com/bjoernmadsen) ![](https://avatars3.githubusercontent.com/u/3615303?s=24&v=4) Carlos Quiroz | [@cquiroz](https://github.com/cquiroz) | [@cquiroz](https://twitter.com/cquiroz) ![](https://avatars3.githubusercontent.com/u/10272700?s=24&v=4)Chris Davenport | [@ChristopherDavenport](https://github.com/ChristopherDavenport) | [@davenpcm](https://twitter.com/davenpcm) ![](https://avatars3.githubusercontent.com/u/7769582?s=24&v=4) Fabio Labella | [@SystemFw](https://github.com/SystemFw) | too smart for that ![](https://avatars3.githubusercontent.com/u/15273652?s=24&v=4) Jose Cardona | [@jmcardon](https://github.com/jmcardon) | [@JM0x5C](https://twitter.com/@JM0x5C) --- # Formation is hiring
If you'd like to do some combination of the following, talk to me: - Scala - Haskell - Machine learning - Big data - Remote friendly [Open positions](http://formation.ai/careers) --- class: center, middle # Thanks! Code and slides at `rossabaker/boston-http4s` on GitHub ## Questions?