├── .gitignore ├── .scalafmt.conf ├── README.md ├── build.sbt ├── project ├── Dependencies.scala ├── build.properties └── plugins.sbt └── src └── main └── scala └── dev └── profunktor └── tutorials ├── t1 ├── ParTask.scala ├── tutorial_1.scala └── tutorial_1_zio.scala └── t2 ├── Sharding.scala ├── tutorial_2.scala ├── tutorial_2_cats.scala └── tutorial_2_zio.scala /.gitignore: -------------------------------------------------------------------------------- 1 | project/boot 2 | target 3 | .ensime 4 | .ensime_lucene 5 | .ensime_cache 6 | .ensime_snapshot 7 | ensime.sbt 8 | TAGS 9 | \#*# 10 | *~ 11 | .#* 12 | .lib 13 | .history 14 | .*.swp 15 | .idea 16 | .idea/* 17 | .idea_modules 18 | .DS_Store 19 | .sbtrc 20 | *.sublime-project 21 | *.sublime-workspace 22 | tests.iml 23 | # Auto-copied by sbt-microsites 24 | docs/src/main/tut/contributing.md 25 | .ignore 26 | result* 27 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "2.0.0-RC4" 2 | align.openParenCallSite = false 3 | align.tokens = ["%", "%%", {code = "=>", owner = "Case"}, {code = "=", owner = "(Enumerator.Val|Defn.(Va(l|r)|Def|Type))"}, ] 4 | align.arrowEnumeratorGenerator = true 5 | binPack.parentConstructors = false 6 | danglingParentheses = true 7 | maxColumn = 120 8 | newlines.afterImplicitKWInVerticalMultiline = true 9 | newlines.beforeImplicitKWInVerticalMultiline = true 10 | project.excludeFilters = [ .scalafmt.conf ] 11 | project.git = true 12 | rewrite.rules = [PreferCurlyFors, RedundantBraces, RedundantParens, SortImports] 13 | spaces.inImportCurlyBraces = true 14 | style = defaultWithAlign 15 | align = most 16 | unindentTopLevelOperators = true 17 | 18 | rewriteTokens { 19 | "⇒" = "=>" 20 | "→" = "->" 21 | "←" = "<-" 22 | } 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | tutorials 2 | ========= 3 | 4 | Subscribe to the [ProfunKtor YouTube channel](https://www.youtube.com/channel/UCu03VaWLqPZlpxJsieZ9FEg) for more! 5 | 6 | --- 7 | 8 | 1. [parFailFast](https://youtu.be/uuocHqdnoS0): modifying the default behavior of `parTraverse` by introducing `Deferred` (and `uncancelable`). 9 | 2. [sharding](https://youtu.be/FWYXqYQWAc0): evenly distributing a stream of values across different shards using `fs2`. 10 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import com.scalapenos.sbt.prompt.SbtPrompt.autoImport._ 2 | import com.scalapenos.sbt.prompt._ 3 | import Dependencies._ 4 | 5 | promptTheme := PromptTheme( 6 | List( 7 | text("[sbt] ", fg(105)), 8 | text(_ => "ProfunKtor", fg(15)).padRight(" λ ") 9 | ) 10 | ) 11 | 12 | lazy val tutorials = (project in file(".")).settings( 13 | inThisBuild( 14 | List( 15 | organization := "dev.profunktor", 16 | scalaVersion := "2.12.8", 17 | version := "0.1.0-SNAPSHOT" 18 | ) 19 | ), 20 | name := "tutorials", 21 | scalafmtOnCompile := true, 22 | libraryDependencies ++= Seq( 23 | compilerPlugin(Libraries.kindProjector), 24 | compilerPlugin(Libraries.betterMonadicFor), 25 | compilerPlugin(Libraries.macroParadise), 26 | Libraries.cats, 27 | Libraries.catsMeowMtl, 28 | Libraries.catsPar, 29 | Libraries.catsEffect, 30 | Libraries.fs2, 31 | Libraries.http4sDsl, 32 | Libraries.http4sServer, 33 | Libraries.http4sCirce, 34 | Libraries.http4sClient, 35 | Libraries.circeCore, 36 | Libraries.circeGeneric, 37 | Libraries.circeGenericExt, 38 | Libraries.circeParser, 39 | Libraries.pureConfig, 40 | Libraries.log4cats, 41 | Libraries.logback, 42 | Libraries.zioCore, 43 | Libraries.zioCats, 44 | Libraries.scalaTest % Test, 45 | Libraries.scalaCheck % Test, 46 | Libraries.catsEffectLaws % Test 47 | ) 48 | ) 49 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | 5 | object Versions { 6 | val cats = "1.6.0" 7 | val catsMeowMtl = "0.2.0" 8 | val catsPar = "0.2.1" 9 | val catsEffect = "1.2.0" 10 | val fs2 = "1.0.4" 11 | val http4s = "0.20.0" 12 | val circe = "0.10.0" 13 | val pureConfig = "0.10.2" 14 | val log4cats = "0.3.0" 15 | val zio = "1.0-RC4" 16 | 17 | val betterMonadicFor = "0.3.0-M4" 18 | val kindProjector = "0.9.8" 19 | val macroParadise = "2.1.0" 20 | val logback = "1.2.1" 21 | val scalaCheck = "1.14.0" 22 | val scalaTest = "3.0.5" 23 | } 24 | 25 | object Libraries { 26 | def circe(artifact: String): ModuleID = "io.circe" %% artifact % Versions.circe 27 | def http4s(artifact: String): ModuleID = "org.http4s" %% artifact % Versions.http4s 28 | def zio(artifact: String): ModuleID = "org.scalaz" %% artifact % Versions.zio 29 | 30 | lazy val cats = "org.typelevel" %% "cats-core" % Versions.cats 31 | lazy val catsMeowMtl = "com.olegpy" %% "meow-mtl" % Versions.catsMeowMtl 32 | lazy val catsPar = "io.chrisdavenport" %% "cats-par" % Versions.catsPar 33 | lazy val catsEffect = "org.typelevel" %% "cats-effect" % Versions.catsEffect 34 | lazy val fs2 = "co.fs2" %% "fs2-core" % Versions.fs2 35 | 36 | lazy val zioCore = zio("scalaz-zio") 37 | lazy val zioCats = zio("scalaz-zio-interop-cats") 38 | 39 | lazy val http4sDsl = http4s("http4s-dsl") 40 | lazy val http4sServer = http4s("http4s-blaze-server") 41 | lazy val http4sCirce = http4s("http4s-circe") 42 | lazy val http4sClient = http4s("http4s-blaze-client") 43 | lazy val circeCore = circe("circe-core") 44 | lazy val circeGeneric = circe("circe-generic") 45 | lazy val circeGenericExt = circe("circe-generic-extras") 46 | lazy val circeParser = circe("circe-parser") 47 | lazy val pureConfig = "com.github.pureconfig" %% "pureconfig" % Versions.pureConfig 48 | lazy val log4cats = "io.chrisdavenport" %% "log4cats-slf4j" % Versions.log4cats 49 | 50 | // Compiler plugins 51 | lazy val betterMonadicFor = "com.olegpy" %% "better-monadic-for" % Versions.betterMonadicFor 52 | lazy val kindProjector = "org.spire-math" %% "kind-projector" % Versions.kindProjector 53 | lazy val macroParadise = "org.scalamacros" % "paradise" % Versions.macroParadise cross CrossVersion.full 54 | 55 | // Runtime 56 | lazy val logback = "ch.qos.logback" % "logback-classic" % Versions.logback 57 | 58 | // Test 59 | lazy val scalaTest = "org.scalatest" %% "scalatest" % Versions.scalaTest 60 | lazy val scalaCheck = "org.scalacheck" %% "scalacheck" % Versions.scalaCheck 61 | lazy val catsEffectLaws = "org.typelevel" %% "cats-effect-laws" % Versions.catsEffect 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.lucidchart" % "sbt-scalafmt-coursier" % "1.15") 2 | 3 | addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.6") 4 | 5 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.0") 6 | 7 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2") 8 | 9 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.2.0") 10 | 11 | addSbtPlugin("com.scalapenos" % "sbt-prompt" % "1.0.2") 12 | -------------------------------------------------------------------------------- /src/main/scala/dev/profunktor/tutorials/t1/ParTask.scala: -------------------------------------------------------------------------------- 1 | package dev.profunktor.tutorials.t1 2 | 3 | import cats.Traverse 4 | import cats.effect._ 5 | import cats.effect.concurrent.Deferred 6 | import cats.effect.implicits._ 7 | import cats.implicits._ 8 | import cats.temp.par._ 9 | 10 | object ParTask { 11 | 12 | /** 13 | * Runs N computations concurrently and either waits for all of them to complete or just return as 14 | * soon as any of them fail while keeping the remaining computations running in the background 15 | * just for their effects. 16 | * */ 17 | def parFailFast[F[_]: Concurrent: Par, G[_]: Traverse, A](gfa: G[F[A]]): F[G[A]] = { 18 | val handler: PartialFunction[Throwable, F[A]] = { 19 | case t: Throwable => Sync[F].delay(println(s"Error: $t")) *> Sync[F].raiseError(t) 20 | } 21 | parFailFastWithHandler[F, G, A](gfa, handler) 22 | } 23 | 24 | private def parFailFastWithHandler[F[_]: Concurrent: Par, G[_]: Traverse, A]( 25 | gfa: G[F[A]], 26 | handler: PartialFunction[Throwable, F[A]] 27 | ): F[G[A]] = 28 | gfa.parTraverse { fa => 29 | Deferred[F, Either[Throwable, A]].flatMap { d => 30 | fa.recoverWith(handler).attempt.flatMap(d.complete).start *> d.get.rethrow 31 | } 32 | } 33 | 34 | def parFailFastWithHandlerAlt[F[_]: Concurrent: Par, G[_]: Traverse, A]( 35 | gfa: G[F[A]], 36 | handler: PartialFunction[Throwable, F[A]] 37 | ): F[G[A]] = 38 | gfa.parTraverse { fa => 39 | fa.recoverWith(handler).uncancelable 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/dev/profunktor/tutorials/t1/tutorial_1.scala: -------------------------------------------------------------------------------- 1 | package dev.profunktor.tutorials.t1 2 | 3 | import cats.effect._ 4 | import cats.effect.concurrent.Deferred 5 | import cats.implicits._ 6 | import scala.concurrent.duration._ 7 | import scala.util.Random 8 | import scala.util.control.NoStackTrace 9 | import java.util.concurrent.TimeUnit 10 | 11 | object tutorial_1 extends IOApp { 12 | 13 | case class Fail(value: String) extends NoStackTrace { 14 | override def toString(): String = s"Fail($value)" 15 | } 16 | 17 | def putStrLn[A](a: A): IO[Unit] = IO.sleep(150.millis) *> IO(println(a)) 18 | 19 | // Simulates expensive computations 20 | def randomPutStrLn[A](a: A): IO[Unit] = 21 | IO(Random.nextInt(10)).flatMap { 22 | case n if n % 2 == 0 => IO.sleep(Duration(n.toLong, TimeUnit.SECONDS)) *> putStrLn(a) 23 | case n => IO.sleep(Duration(n.toLong, TimeUnit.SECONDS)) *> IO.raiseError(Fail(s"n=$n, a=$a")) 24 | } 25 | 26 | val list = List.range(1, 11) 27 | 28 | def handler[A]: PartialFunction[Throwable, IO[A]] = { 29 | case e: Fail => putStrLn(e) *> IO.raiseError(e) 30 | } 31 | 32 | def parFailFast[A](gfa: List[IO[A]]): IO[List[A]] = 33 | gfa.parTraverse(_.recoverWith(handler).uncancelable) 34 | 35 | def parFailFastAlt[A](gfa: List[IO[A]]): IO[List[A]] = 36 | gfa.parTraverse { fa => 37 | Deferred[IO, Either[Throwable, A]].flatMap { d => 38 | fa.recoverWith(handler).attempt.flatMap(d.complete).start *> d.get.rethrow 39 | } 40 | } 41 | 42 | def run(args: List[String]): IO[ExitCode] = 43 | parFailFast(list.map(randomPutStrLn)) 44 | .handleErrorWith { 45 | case e: Fail => putStrLn(s"First failure: $e") 46 | } 47 | .as(ExitCode.Success) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/scala/dev/profunktor/tutorials/t1/tutorial_1_zio.scala: -------------------------------------------------------------------------------- 1 | package dev.profunktor.tutorials.t1 2 | 3 | import java.util.concurrent.TimeUnit 4 | import scalaz.zio._ 5 | import scalaz.zio.clock.Clock 6 | import scalaz.zio.duration._ 7 | import scala.util.Random 8 | 9 | object tutorial_1_zio extends App { 10 | 11 | case class Fail(value: String) 12 | 13 | def putStrLn[A](a: A): ZIO[Clock, Nothing, Unit] = 14 | ZIO.sleep(150.millis) *> ZIO.effectTotal(println(a)) 15 | 16 | // Simulates expensive computations 17 | def randomPutStrLn[A](a: A): ZIO[Clock, Fail, Unit] = 18 | ZIO.effectTotal(Random.nextInt(10)).flatMap { 19 | case n if n % 2 == 0 => ZIO.sleep(Duration(n.toLong, TimeUnit.SECONDS)) *> putStrLn(a) 20 | case n => ZIO.sleep(Duration(n.toLong, TimeUnit.SECONDS)) *> ZIO.fail(Fail(s"n=$n, a=$a")) 21 | } 22 | 23 | val list = List.range(1, 11) 24 | 25 | def handler[A]: PartialFunction[Fail, ZIO[Clock, Fail, A]] = { 26 | case e: Fail => putStrLn(e) *> IO.fail(e) 27 | } 28 | 29 | // does not behave like the cats effect equivalent 30 | def parFailFastAlt[A](gfa: List[ZIO[Clock, Fail, A]]): ZIO[Clock, Fail, List[A]] = 31 | ZIO.foreachPar(gfa)(_.catchSome(handler).uninterruptible) 32 | 33 | def parFailFast[A](gfa: List[ZIO[Clock, Fail, A]]): ZIO[Clock, Fail, List[A]] = 34 | ZIO.foreachPar(gfa) { fa => 35 | ZIO.accessM { _ => 36 | Promise.make[Nothing, Either[Fail, A]].flatMap { promise => 37 | fa.catchSome(handler).either.flatMap(promise.succeed).fork *> 38 | promise.await.flatMap(e => ZIO.accessM(_ => ZIO.fromEither(e))) 39 | } 40 | } 41 | } 42 | 43 | def run(args: List[String]): ZIO[Environment, Nothing, Int] = 44 | parFailFastAlt(list.map(randomPutStrLn)) 45 | .catchAll(e => putStrLn(s"First failure: $e")) 46 | .map(_ => 0) 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/dev/profunktor/tutorials/t2/Sharding.scala: -------------------------------------------------------------------------------- 1 | package dev.profunktor.tutorials.t2 2 | 3 | import cats.effect._ 4 | import cats.implicits._ 5 | import fs2._ 6 | import fs2.concurrent.Queue 7 | 8 | object Sharding { 9 | 10 | def putStrLn[F[_]: Sync, A](a: A): F[Unit] = Sync[F].delay(println(a)) 11 | 12 | def sharded[F[_]: Concurrent, A](shards: Int, idOf: A => Int, action: Int => A => F[Unit])( 13 | source: Stream[F, A] 14 | ): Stream[F, Unit] = 15 | Stream.eval(Queue.bounded[F, A](500).replicateA(shards)).map(qs => List.range(0, shards).zip(qs).toMap).flatMap { 16 | kvs => 17 | source.flatMap { n => 18 | val q = kvs(idOf(n) % shards) 19 | Stream.eval(q.enqueue1(n)).concurrently(q.dequeue.evalMap(action(idOf(n) % shards))) 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/dev/profunktor/tutorials/t2/tutorial_2.scala: -------------------------------------------------------------------------------- 1 | package dev.profunktor.tutorials.t2 2 | 3 | import cats.effect._ 4 | import cats.implicits._ 5 | import fs2._ 6 | import fs2.concurrent.Queue 7 | 8 | object tutorial_2 extends IOApp { 9 | 10 | def putStrLn[A](a: A): IO[Unit] = IO(println(a)) 11 | 12 | val src = Stream.range[IO](1, 11) 13 | 14 | def sharded(shards: Int, action: Int => Int => IO[Unit])(source: Stream[IO, Int]): Stream[IO, Unit] = 15 | Stream.eval(Queue.bounded[IO, Int](100).replicateA(shards).map(_.zipWithIndex.map(_.swap).toMap)).flatMap { kvs => 16 | source.flatMap { n => 17 | val q = kvs(n % shards) 18 | Stream.eval(q.enqueue1(n)).concurrently(q.dequeue.evalMap(action(n % shards))) 19 | } 20 | } 21 | 22 | val showShardAndValue: Int => Int => IO[Unit] = 23 | s => v => putStrLn(s"Shard: $s, Value: $v") 24 | 25 | def run(args: List[String]): IO[ExitCode] = 26 | sharded(3, showShardAndValue)(src).compile.drain.as(ExitCode.Success) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/dev/profunktor/tutorials/t2/tutorial_2_cats.scala: -------------------------------------------------------------------------------- 1 | package dev.profunktor.tutorials.t2 2 | 3 | import cats.effect._ 4 | import cats.implicits._ 5 | import fs2.concurrent.Queue 6 | 7 | object tutorial_2_cats extends IOApp { 8 | 9 | def putStrLn[A](a: A): IO[Unit] = IO(println(a)) 10 | 11 | def consumer[A](worker: A => IO[Unit])(workerNo: Int): IO[(Int, Queue[IO, A])] = 12 | Queue.bounded[IO, A](500).flatMap { queue => 13 | queue.dequeue1.flatMap(worker).foreverM.start.as(workerNo -> queue) 14 | } 15 | 16 | def producer[A](idOf: A => Int)(queues: Map[Int, Queue[IO, A]]): A => IO[Unit] = 17 | (a: A) => queues(idOf(a) % queues.size).enqueue1(a) 18 | 19 | def partition[A](noOfShards: Int, idOf: A => Int)(worker: Int => A => IO[Unit]): IO[A => IO[Unit]] = 20 | List.range(0, noOfShards).traverse(s => consumer(worker(s))(s)).map(_.toMap).map(producer(idOf)) 21 | 22 | val numbers: List[Int] = 23 | List.range(1, 11) 24 | 25 | def showShardAndValue: Int => Int => IO[Unit] = 26 | s => v => putStrLn(s"Shard: $s, Value: $v") 27 | 28 | def run(args: List[String]): IO[ExitCode] = 29 | partition[Int](5, _ % 5)(s => x => putStrLn(s"Shard: $s, Value: $x")) 30 | .flatMap(numbers.traverse(_)) 31 | .as(ExitCode.Success) 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/scala/dev/profunktor/tutorials/t2/tutorial_2_zio.scala: -------------------------------------------------------------------------------- 1 | package dev.profunktor.tutorials.t2 2 | 3 | import java.util.concurrent.TimeUnit 4 | import scalaz.zio._ 5 | import scalaz.zio.duration.Duration 6 | 7 | object tutorial_2_zio extends App { 8 | 9 | def consumeWork[A](worker: A => UIO[Unit])(workerNo: Int): UIO[(Int, Queue[A])] = 10 | for { 11 | queue <- Queue.bounded[A](5) 12 | _ <- queue.take.flatMap(worker).forever.fork 13 | } yield (workerNo, queue) 14 | 15 | def produceWork[A](idOf: A => Int)(queues: Map[Int, Queue[A]]): A => UIO[Boolean] = 16 | (a: A) => queues(idOf(a) % queues.size).offer(a) 17 | 18 | def partition[A](noOfShards: Int, idOf: A => Int)(worker: A => UIO[Unit]): UIO[A => UIO[Boolean]] = 19 | ZIO.foreach(0 until noOfShards)(consumeWork(worker)).map(_.toMap).map(produceWork(idOf)) 20 | 21 | val numbers: List[Int] = 22 | List.range(1, 11) 23 | 24 | def printNumberAndFiber(n: Int): UIO[Unit] = 25 | UIO.descriptorWith(desc => console.putStrLn(s"Fiber: ${desc.id}, n = $n").provide(console.Console.Live)) 26 | 27 | def run(args: List[String]): ZIO[Environment, Nothing, Int] = 28 | partition[Int](5, _ % 5)(printNumberAndFiber) 29 | .flatMap(f => UIO.foreach(numbers)(f)) 30 | .delay(Duration(500, TimeUnit.MILLISECONDS)) // Wait for workers to get a chance to run 31 | .map(_ => 0) // Exit as normal 32 | 33 | } 34 | --------------------------------------------------------------------------------