Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Jakub Kozłowski

April 06, 2019
Tweet

More Decks by Jakub Kozłowski

Other Decks in Programming

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

    View Slide

  2. View Slide

  3. Concurrent world

    View Slide

  4. Concurrent world
    Concurrent programs

    View Slide

  5. Concurrent world
    Concurrent programs
    Concurrent state

    View Slide

  6. Concurrent world
    Concurrent programs
    Concurrent state
    Concurrency problems

    View Slide

  7. Concurrent state

    View Slide

  8. Functional state?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  13. ???

    View Slide

  14. counter += 1

    View Slide

  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)
    }
    }

    View Slide

  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

    View Slide

  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

    View Slide

  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() // ???

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  23. View Slide

  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

    View Slide

  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

    View Slide

  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
    }
    }

    View Slide

  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

    View Slide

  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
    }
    }

    View Slide

  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(_) }
    }

    View Slide

  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*)

    View Slide

  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*)

    View Slide

  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*)

    View Slide

  33. Referential transparency

    View Slide

  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)

    View Slide

  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)

    View Slide

  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)

    View Slide

  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!)

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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
    ==

    View Slide

  47. View Slide

  48. View Slide

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

    View Slide

  50. View Slide

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

    View Slide

  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

    View Slide

  53. cancelation
    Where will you be...

    View Slide

  54. cancelation
    Where will you be...

    View Slide

  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)

    View Slide

  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)

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  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]]
    }

    View Slide

  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

    View Slide

  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)
    }
    }

    View Slide

  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]
    }
    }
    }
    }
    }
    $

    View Slide

  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
    }
    }
    }
    $

    View Slide

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

    View Slide

  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)

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide