Conquering concurrency with functional programming

Conquering concurrency with functional programming

Some people claim functional programming is useless because in the end there will be side effects - or else your program won't do anything useful. That's not true, and as it turns out purely functional programming is really good at solving some real-world problems, like concurrency.
I'll talk about how shared mutable state, queues, and streams can be used in purely functional programs, and why such a solution might be preferred over over the classical ways of managing concurrent state.

08f642741fba006656cb86fb61c160b3?s=128

Jakub Kozłowski

April 06, 2019
Tweet

Transcript

  1. 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
  2. None
  3. Concurrent world

  4. Concurrent world Concurrent programs

  5. Concurrent world Concurrent programs Concurrent state

  6. Concurrent world Concurrent programs Concurrent state Concurrency problems

  7. Concurrent state

  8. Functional state?

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

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

    A)) ...does not work with concurrency *simplified*
  11. Could this BE any more sequential? S => (S, A)

  12. 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
  13. ???

  14. counter += 1

  15. 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) } }
  16. 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
  17. 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
  18. 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() // ???
  19. 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
  20. 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
  21. 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
  22. 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
  23. None
  24. 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
  25. 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
  26. 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 } }
  27. 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
  28. 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 } }
  29. 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(_) } }
  30. 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*)
  31. 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*)
  32. 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*)
  33. Referential transparency

  34. 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)
  35. 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)
  36. 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)
  37. 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!)
  38. ...but it works with IO/Task/ZIO val msg = IO(StdIn.readLine()) (msg,

    msg) == (IO(StdIn.readLine()), IO(StdIn.readLine())
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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 ==
  47. None
  48. None
  49. Why is referential transparency useful? - Fearless refactoring - Compositionality

    - Explicit, controlable dependencies - Explicit effects
  50. None
  51. cats-effect IO, Fiber, effect type classes, concurrency primitives (Ref, Deferred,

    Semaphore, MVar)
  52. 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
  53. cancelation Where will you be...

  54. cancelation Where will you be...

  55. 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)
  56. 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)
  57. 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 } }
  58. 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 } }
  59. 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 } }
  60. 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]] }
  61. 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
  62. 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) } }
  63. 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] } } } } } $
  64. 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 } } } $
  65. Build your own concurrent algebras - Circuit breakers - Caches

    - Job queues - Your domain-specific in-memory state - More
  66. 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)
  67. 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
  68. 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
  69. 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