Slide 1

Slide 1 text

C O N Q U E R I N G C O N C U R R E N C Y W I T H F U N C T I O N A L P R O G R A M M I N G J A K U B K O Z Ł O W S K I

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Concurrent world

Slide 4

Slide 4 text

Concurrent world Concurrent programs

Slide 5

Slide 5 text

Concurrent world Concurrent programs Concurrent state

Slide 6

Slide 6 text

Concurrent world Concurrent programs Concurrent state Concurrency problems

Slide 7

Slide 7 text

Concurrent state

Slide 8

Slide 8 text

Functional state?

Slide 9

Slide 9 text

State, state... cats.data.State? case class State[S, A](run: S => (S, A)) *simplified*

Slide 10

Slide 10 text

State, state... cats.data.State? case class State[S, A](run: S => (S, A)) ...does not work with concurrency *simplified*

Slide 11

Slide 11 text

Could this BE any more sequential? S => (S, A)

Slide 12

Slide 12 text

Shared state in pure FP: when a state monad won't do https://vimeo.com/294736344 "Passing something from a function to another is the most sequential thing you can think of" - Fabio Labella

Slide 13

Slide 13 text

???

Slide 14

Slide 14 text

counter += 1

Slide 15

Slide 15 text

class UserCart { private var cart = Cart.Empty def getSize(): Int = cart.size def put(item: Cart.Item): Unit = { if (!cart.contains(item)) cart = cart.appendItem(item) } }

Slide 16

Slide 16 text

class UserCart { private var cart = Cart.Empty def getSize(): Int = cart.size def put(item: Cart.Item): Unit = { if (!cart.contains(item)) cart = cart.appendItem(item) } } userCart.put(new Cart.Item(data)) //item isn't in cart yet //item added Thread 1

Slide 17

Slide 17 text

class UserCart { private var cart = Cart.Empty def getSize(): Int = cart.size def put(item: Cart.Item): Unit = { if (!cart.contains(item)) cart = cart.appendItem(item) } } userCart.put(new Cart.Item(data)) //item isn't in cart yet //item added Thread 1 userCart.put(new Cart.Item(data)) //item isn't in cart yet //item added Thread 2

Slide 18

Slide 18 text

class UserCart { private var cart = Cart.Empty def getSize(): Int = cart.size def put(item: Cart.Item): Unit = { if (!cart.contains(item)) cart = cart.appendItem(item) } } userCart.put(new Cart.Item(data)) //item isn't in cart yet //item added Thread 1 userCart.put(new Cart.Item(data)) //item isn't in cart yet //item added Thread 2 userCart.getSize() // ???

Slide 19

Slide 19 text

class UserCart { private var cart = Cart.Empty def getSize(): Int = cart.size def put(item: Cart.Item): Unit = { if (!cart.contains(item)) cart = cart.appendItem(item) } } Solution 1: locks

Slide 20

Slide 20 text

class UserCart { private var cart = Cart.Empty def getSize(): Int = cart.size def put(item: Cart.Item): Unit = { if (!cart.contains(item)) cart = cart.appendItem(item) } } Solution 1: locks

Slide 21

Slide 21 text

class UserCart { private var cart = Cart.Empty def getSize(): Int = cart.size def put(item: Cart.Item): Unit = this.synchronized { if (!cart.contains(item)) cart = cart.appendItem(item) } } Solution 1: locks Not atomic

Slide 22

Slide 22 text

class UserCart { private var cart = Cart.Empty def getSize(): Int = cart.size def put(item: Cart.Item): Unit = this.synchronized { if (!cart.contains(item)) cart = cart.appendItem(item) } } Solution 1: locks Not atomic ·Blocking ·Easy to break (e.g. deadlocks) ·Non-compositional

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

object CartActor { case object GetSize case class Put(item: Cart.Item) } class CartActor extends Actor { private var cart = Cart.Empty import CartActor._ def receive: Receive = { case GetSize => sender() ! cart.size case Put(item) => if (!cart.contains(item)) cart = cart.appendItem(item) } } Solution 2: actor model

Slide 25

Slide 25 text

object CartActor { case object GetSize case class Put(item: Cart.Item) } class CartActor extends Actor { private var cart = Cart.Empty import CartActor._ def receive: Receive = { case GetSize => sender() ! cart.size case Put(item) => if (!cart.contains(item)) cart = cart.appendItem(item) } } Solution 2: actor model · Low level, advanced construct · Imposed "push" mindset · Complex in testing

Slide 26

Slide 26 text

object CartActor { case object GetSize case object Clear case class Put(item: Cart.Item) } class CartActor extends Actor { private var cart = Cart.Empty import CartActor._ def receive: Receive = { case GetSize => sender() ! cart.size case Put(item) => if (!cart.contains(item)) cart = cart.appendItem(item) case Clear => cart = Cart.Empty } } Solution 3: atomic references class UserCart { private val cartRef = new AtomicReference(Cart.Empty) def getSize(): Long = cartRef.get().size def put(item: Cart.Item): Unit = cartRef.updateAndGet { cart => if (!cart.contains(item)) cart.appendItem(item) else cart } }

Slide 27

Slide 27 text

Solution 3: atomic references class UserCart { private val cartRef = new AtomicReference(Cart.Empty) def getSize(): Long = cartRef.get().size def put(item: Cart.Item): Unit = cartRef.updateAndGet { cart => if (!cart.contains(item)) cart.appendItem(item) else cart } } ·Side-effecting ·Synchronous updates only ·Shared state is implicit

Slide 28

Slide 28 text

Solution 3: pure atomic references class UserCart { private val cartRef = new AtomicReference(Cart.Empty) def getSize(): Long = cartRef.get().size def put(item: Cart.Item): Unit = cartRef.updateAndGet { cart => if (!cart.contains(item)) cart.appendItem(item) else cart } }

Slide 29

Slide 29 text

Solution 3: pure atomic references class UserCart private(cartRef: Ref[IO, Cart]) { val getSize: IO[Long] = cartRef.get.map(_.size) def put(item: Cart.Item): IO[Unit] = cartRef.update { cart => if (!cart.contains(item)) cart.appendItem(item) else cart } } object UserCart { val create: IO[UserCart] = Ref[IO].of(Cart.Empty).map { new UserCart(_) } }

Slide 30

Slide 30 text

Solution 3: pure atomic references class UserCart private(cartRef: Ref[IO, Cart]) { val getSize: IO[Long] = cartRef.get.map(_.size) def put(item: Cart.Item): IO[Unit] = cartRef.update { cart => if (!cart.contains(item)) cart.appendItem(item) else cart } } object UserCart { val create: IO[UserCart] = Ref[IO].of(Cart.Empty).map { new UserCart(_) } } ·Still only synchronous updates (which is actually kinda cool*)

Slide 31

Slide 31 text

Solution 3: pure atomic references class UserCart private(cartRef: Ref[IO, Cart]) { val getSize: IO[Long] = cartRef.get.map(_.size) def put(item: Cart.Item): IO[Unit] = cartRef.update { cart => if (!cart.contains(item)) cart.appendItem(item) else cart } } object UserCart { val create: IO[UserCart] = Ref[IO].of(Cart.Empty).map { new UserCart(_) } } ·Still only synchronous updates (which is actually kinda cool*)

Slide 32

Slide 32 text

Solution 3: pure atomic references class UserCart private(cartRef: Ref[IO, Cart]) { val getSize: IO[Long] = cartRef.get.map(_.size) def put(item: Cart.Item): IO[Unit] = cartRef.update { cart => if (!cart.contains(item)) cart.appendItem(item) else cart } } object UserCart { val create: IO[UserCart] = Ref[IO].of(Cart.Empty).map { new UserCart(_) } } ·Still only synchronous updates (which is actually kinda cool*)

Slide 33

Slide 33 text

Referential transparency

Slide 34

Slide 34 text

Replacing any or all occurrences of an expression x in a program p with the value of x doesn't change the program. Example val pi = 3 //precise approximation (pi + 1, pi)

Slide 35

Slide 35 text

Replacing any or all occurrences of an expression x in a program p with the value of x doesn't change the program. Example val pi = 3 //precise approximation (pi + 1, pi)

Slide 36

Slide 36 text

Replacing any or all occurrences of an expression x in a program p with the value of x doesn't change the program. Example val pi = 3 //precise approximation (pi + 1, pi) == (3 + 1, 3)

Slide 37

Slide 37 text

val msg = StdIn.readLine() (msg, msg) == (StdIn.readLine(), StdIn.readLine()) Referential transparency is broken with side effects! val msg = Future(StdIn.readLine()) (msg, msg) == (Future(StdIn.readLine()), Future(StdIn.readLine())) (yes, Future too!)

Slide 38

Slide 38 text

...but it works with IO/Task/ZIO val msg = IO(StdIn.readLine()) (msg, msg) == (IO(StdIn.readLine()), IO(StdIn.readLine())

Slide 39

Slide 39 text

Ref creation needs to be suspended val ref = Ref.unsafe[IO, Int](0) val prog = for { _ <- ref.update(_ + 1) v <- ref.get } yield v prog .unsafeRunSync //1

Slide 40

Slide 40 text

Ref creation needs to be suspended val ref = Ref.unsafe[IO, Int](0) val prog = for { _ <- ref.update(_ + 1) v <- ref.get } yield v prog .unsafeRunSync //1

Slide 41

Slide 41 text

Ref creation needs to be suspended val prog = for { _ <- Ref.unsafe[IO, Int](0).update(_ + 1) v <- Ref.unsafe[IO, Int](0).get } yield v prog .unsafeRunSync //0

Slide 42

Slide 42 text

Ref creation needs to be suspended val prog = for { _ <- Ref.unsafe[IO, Int](0).update(_ + 1) v <- Ref.unsafe[IO, Int](0).get } yield v prog .unsafeRunSync //0

Slide 43

Slide 43 text

A Ref can be shared inside IO val refIO = Ref[IO].of(0) def prog(ref: Ref[IO, Int]) = for { _ <- ref.update(_ + 1) v <- ref.get } yield v refIO .flatMap(prog) .unsafeRunSync //1

Slide 44

Slide 44 text

A Ref can be shared inside IO def prog(ref: Ref[IO, Int]) = for { _ <- ref.update(_ + 1) v <- ref.get } yield v Ref[IO].of(0) .flatMap(prog) .unsafeRunSync //1

Slide 45

Slide 45 text

A Ref can be shared inside IO def prog(ref: Ref[IO, Int]) = for { _ <- ref.update(_ + 1) v <- ref.get } yield v Ref[IO].of(0) .flatMap(prog) .unsafeRunSync //1

Slide 46

Slide 46 text

A Ref can be shared inside IO def prog(ref: Ref[IO, Int]) = for { _ <- ref.update(_ + 1) v <- ref.get } yield v Ref[IO].of(0) .flatMap(prog) .unsafeRunSync //1 Ref[IO].of(0) .flatMap { ref => for { _ <- ref.update(_ + 1) v <- ref.get } yield v } .unsafeRunSync //1 ==

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

Why is referential transparency useful? - Fearless refactoring - Compositionality - Explicit, controlable dependencies - Explicit effects

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

cats-effect IO, Fiber, effect type classes, concurrency primitives (Ref, Deferred, Semaphore, MVar)

Slide 52

Slide 52 text

def racePairKeepLeft[A, B](left: IO[A], right: IO[B]): IO[A] Task: build a combinator 1. Left completes first - cancel right 2. Right completes first - keep left running 3. The result must maintain cancelability in all cases

Slide 53

Slide 53 text

cancelation Where will you be...

Slide 54

Slide 54 text

cancelation Where will you be...

Slide 55

Slide 55 text

Cancelation val a = IO.sleep(5.seconds) >> veryExpensiveJob val b = IO.sleep(1.second) >> IO.raiseError(new Throwable("Oh no!")) (a, b).parTupled (1 to 100).toList.parTraverse(veryExpensive)

Slide 56

Slide 56 text

Cancelation val a = IO.sleep(5.seconds) >> veryExpensiveJob val b = IO.sleep(1.second) >> IO.raiseError(new Throwable("Oh no!")) (a, b).parTupled (1 to 100).toList.parTraverse(veryExpensive)

Slide 57

Slide 57 text

Direct implementation with racePair def racePairKeepLeft[A, B](left: IO[A], right: IO[B]): IO[A] = { left .racePair(right) .bracketCase { case Left((left, rightFiber)) => rightFiber.cancel.as(left).uncancelable case Right((leftFiber, _)) => leftFiber.join.guaranteeCase { case ExitCase.Canceled => leftFiber.cancel case _ => IO.unit } } { case (Left((_, rightFiber)), ExitCase.Canceled) => rightFiber.cancel case (Right((leftFiber, _)), ExitCase.Canceled) => leftFiber.cancel case _ => IO.unit } }

Slide 58

Slide 58 text

Direct implementation with racePair def racePairKeepLeft[A, B](left: IO[A], right: IO[B]): IO[A] = { left .racePair(right) .bracketCase { case Left((left, rightFiber)) => rightFiber.cancel.as(left).uncancelable case Right((leftFiber, _)) => leftFiber.join.guaranteeCase { case ExitCase.Canceled => leftFiber.cancel case _ => IO.unit } } { case (Left((_, rightFiber)), ExitCase.Canceled) => rightFiber.cancel case (Right((leftFiber, _)), ExitCase.Canceled) => leftFiber.cancel case _ => IO.unit } }

Slide 59

Slide 59 text

Direct implementation with racePair def racePairKeepLeft[A, B](left: IO[A], right: IO[B]): IO[A] = { left .racePair(right) .bracketCase { case Left((left, rightFiber)) => rightFiber.cancel.as(left).uncancelable case Right((leftFiber, _)) => leftFiber.join.guaranteeCase { case ExitCase.Canceled => leftFiber.cancel case _ => IO.unit } } { case (Left((_, rightFiber)), ExitCase.Canceled) => rightFiber.cancel case (Right((leftFiber, _)), ExitCase.Canceled) => leftFiber.cancel case _ => IO.unit } }

Slide 60

Slide 60 text

Deferred - purely functional promise abstract class Deferred[F[_], A] { def get: F[A] def complete(a: A): F[Unit] } object Deferred { def apply[F[_], A]( implicit F: Concurrent[F] ): F[Deferred[F, A]] }

Slide 61

Slide 61 text

Implementation with Deferred /** * Left completes first - cancel right * Right completes first - keep left running * The result must maintain cancelability in all cases **/ def racePairKeepLeft[A, B](left: IO[A], right: IO[B]): IO[A] = { Deferred[IO, Unit].flatMap { leftCompleted => (left <* leftCompleted.complete(())) <& (right race leftCompleted.get) } } a <* b - run a, then b, keep the result of a a <& b - run a and b in parallel, keep result of a (if one fails the other one is canceled) race - run both sides in parallel, when one succeeds cancel the other

Slide 62

Slide 62 text

Implementation with Concurrent.memoize object Concurrent { def memoize[F[_], A](f: F[A])(implicit F: Concurrent[F]): F[F[A]] = Ref.of[F, Option[Deferred[F, Either[Throwable, A]]]](None).map { ref => Deferred[F, Either[Throwable, A]].flatMap { d => ref .modify { case None => Some(d) -> f.attempt.flatTap(d.complete) case s @ Some(other) => s -> other.get } .flatten .rethrow } } } def racePairKeepLeft[A, B](left: IO[A], right: IO[B]): IO[A] = { Concurrent.memoize(left).flatMap { leftM => leftM <& (right race leftM) } }

Slide 63

Slide 63 text

Referential transparency makes concurrency bearable def unbounded[F[_]: Concurrent]: Resource[F, Manager[F]] = { val id: F[Unique] = Sync[F].delay(new Unique) Resource { Ref[F].of(Map.empty[Unique, Fiber[F, Unit]]).map { tasks => new Manager[F] { override def safeStart[A](fa: F[A]): F[Unit] = Deferred[F, Unit].flatMap { isManaged => id.flatMap { taskId => (isManaged.get >> fa.attempt >> tasks.update(_ - taskId)).start.flatMap { fiber => tasks.update(_ + (taskId -> fiber.void)) >> isManaged.complete(()) }.uncancelable } } } -> tasks.get.flatMap { _.toList.traverse_ { case (_, task) => task.cancel: F[Unit] } } } } } $

Slide 64

Slide 64 text

Referential transparency makes concurrency bearable def unbounded[F[_]: Concurrent]: Resource[F, Manager[F]] = { val id: F[Unique] = Sync[F].delay(new Unique) Resource { Ref[F].of(Map.empty[Unique, Fiber[F, Unit]]).map { tasks => val cancelAllTasks = tasks.get.flatMap { _.toList.traverse_ { case (_, task) => task.cancel: F[Unit] } } new Manager[F] { override def safeStart[A](fa: F[A]): F[Unit] = Deferred[F, Unit].flatMap { isManaged => val markManaged = isManaged.complete(()) id.flatMap { taskId => val unregister = tasks.update(_ - taskId) val runJob = isManaged.get >> fa.attempt >> unregister runJob.start.flatMap { fiber => val register = tasks.update(_ + (taskId -> fiber.void)) register >> markManaged }.uncancelable } } } -> cancelAllTasks } } } $

Slide 65

Slide 65 text

Build your own concurrent algebras - Circuit breakers - Caches - Job queues - Your domain-specific in-memory state - More

Slide 66

Slide 66 text

Functional concurrency is cool Try it at home: - https://typelevel.org/cats-effect/datatypes/io.html - https://typelevel.org/cats-effect/concurrency/ Use IO in production! (we do) Don't get discouraged (it takes a while to get comfortable with)

Slide 67

Slide 67 text

What next? Exercises: - https://typelevel.org/cats-effect/tutorial/tutorial.html - https://olegpy.com/cats-effect-exercises/ - http://degoes.net/articles/zio-challenge More: - https://fs2.io/ - https://typelevel.org/cats-effect/#libraries - A bunch of links on the next slide - Ask on gitter! https://gitter.im/typelevel/cats-effect

Slide 68

Slide 68 text

https://fs2.io/concurrency-primitives.html https://typelevel.org/blog/2018/06/07/shared-state-in-fp.html https://github.com/kubukoz/brick-store https://github.com/ChristopherDavenport/cats-par https://github.com/SystemFw/upperbound https://www.youtube.com/watch?v=x3GLwl1FxcA https://twitter.com/jdegoes/status/936301872066977792 https://twitter.com/impurepics/status/983407934574153728 https://github.com/pauljamescleary/scala-pet-store https://www.youtube.com/watch?v=oFk8-a1FSP0 https://www.youtube.com/watch?v=sxudIMiOo68 https://www.youtube.com/watch?v=EL3xy9DKhno https://www.youtube.com/watch?v=X-cEGEJMx_4 https://www.youtube.com/watch?v=po3wmq4S15A For those who actually view the slides online

Slide 69

Slide 69 text

T H A N K Y O U Slides: bit.ly/2YdpmxE Some code: github.com/kubukoz/concurrency-fun My twitter: @kubukoz My blog: blog.kubukoz.com