$30 off During Our Annual Pro Sale. View Details »

FP with Kotlin/Arrow: Monad Comprehensions & Parallel Processing

Matt Moore
February 11, 2020

FP with Kotlin/Arrow: Monad Comprehensions & Parallel Processing

Monads make our code safer, provide better guarantees with error handling, and clean our code up to be easier to read. In this talk we’ll take a look at Arrow’s IO monad, and how we can combine it with Either.

When composing multiple monads you’ll start to notice that you end up indenting a lot of code as you begin to nest flatMaps into flatMaps with multiple monads for each step in the happy path. We’ll take a look at another strategy for handling monadic composition in Arrow with monad comprehensions.

Monad comprehensions give us the flexibility to make our IO operations a little easier to navigate by writing each sequential operation line by line, making our code look more imperative, while still executing in a functional manner. And we still get to keep all the safety guarantees that monads provide in the process. We’ll also see how monad comprehensions were inspired by Haskell’s do notation and Scala’s for comprehensions.

Next, we’ll examine how to handle parallel processing the functional way with parallel map strategies combined with monads, allowing us to do parallel processing with far less code and safer guarantees.

Matt Moore

February 11, 2020
Tweet

More Decks by Matt Moore

Other Decks in Programming

Transcript

  1. Arrow:
    Monadic Composition
    Making Happy/Bad Path A Snap

    View Slide

  2. Monads do a lot of things.
    The most common?
    Handling happy path vs bad path.

    View Slide

  3. Option monad reifies missing values/null
    Either reifies exceptions
    State reifies mutable state
    IO reifies input/output

    View Slide

  4. For IO operations,
    IO monad is the ticket.

    View Slide

  5. The Problem
    We have data stored in APIs that we need to access:
    ● Customers
    ● Orders
    ● Addresses

    View Slide

  6. The Problem
    Given an order number, I need to get a customer's address.
    Sequence of looking up an address:
    1. Look up the order record for a given order number.
    2. Find the customer associated with that order.
    3. Get the customer's mailing address.

    View Slide

  7. The Problem
    If any of the lookups fail, we should return a message "Not found."
    ● If the order number isn't valid, no sense in running a lookup on customer.
    ● If customer can't be found, no sense searching for address.

    View Slide

  8. One Way Without Monads
    Will throw an exception if anything fails.
    We can add try/catch.
    We cannot parallelize this as easily as
    with monads (we'll get to that in a bit).

    View Slide

  9. Convert IO Functions to Arrow's IO Monad
    Wrap functions that can fail in IO.
    Before After

    View Slide

  10. Side Note: Return Type Inference
    My personal preference: include
    return type.
    The whole point of type systems
    is compiler checks for our code.
    If you omit them, you're
    bypassing the type check system.
    Kotlin, Scala and Haskell let you
    do type inference, but specifying
    it ensures compiler checks.

    View Slide

  11. Before
    After
    Monad Composition: Sequential Processing

    View Slide

  12. Monad Composition: Sequential Processing
    If any of the monads in the
    sequence fails, the entire
    composition returns nothing.
    Nothing in nothing out.
    It won't throw an exception, so
    no accidental blow-ups if we
    forget to handle an error along
    the way.
    Each monad depends on the successful
    completion of the one before it.

    View Slide

  13. Executing the monad
    Call suspended() to execute the monadic composition getAddressFromOrder()

    View Slide

  14. Executing the monad
    If the address is found, it will print. But it will print "Address lookup failed." if the
    address isn't found.

    View Slide

  15. Executing the monad
    To print "Not found." we use attempt() to convert from
    IO to Either and map over the result with Kotlin's when expression:
    Either.Left:
    Composition failed.
    Either.Right:
    Composition succeeded.
    Found the address.

    View Slide

  16. Monadic composition guarantees comprehensive error handling.
    But IO monad (sequence in the program) requires nesting with flatMap.
    This gets difficult to read over time.
    There's a better way to do monadic composition.

    View Slide

  17. Monad Comprehensions

    View Slide

  18. Monad
    Comprehension
    Remove Nesting Hell
    ● A different way to handle
    monadic composition
    ● Removes flatMap nesting
    ● Gives more flexibility to
    parallelization of monads

    View Slide

  19. Before After

    View Slide

  20. Wait, that looks suspiciously imperative!

    View Slide

  21. Yes, it looks imperative.
    But we've kept the same
    error-handling guarantees of monads.
    We've eliminated nesting.
    Monadic steps in our sequence go on separate lines.

    View Slide

  22. What's up with that "!"?

    View Slide

  23. ! is a shortcut for bind()

    View Slide

  24. bind() executes the monad
    in the context of the comprehension

    View Slide

  25. ! saves us some typing.
    It was recommended by Simon Vergauwen.

    View Slide

  26. Monad comprehensions are executed
    the same way as nested comprehensions.

    View Slide

  27. Monadic Parallel Processing

    View Slide

  28. The Problem
    We have a text document partitioned across multiple files.
    We are given a list of files in a specific order that need to be read.
    They can be read in any order.
    However, we must write the final output in the order of the files we were given.

    View Slide

  29. With what we've
    learned
    We can get all the files in order
    with monad comprehensions.
    But this happens sequentially.
    If each file takes 2 seconds,
    4 files take 8 seconds total.
    We can parallelize each file.
    Then assemble in order.

    View Slide

  30. parMapN
    Executes monads in parallel.
    Doesn't return the whole set
    until all monads are complete.

    View Slide

  31. parMapN
    But parMapN requires each
    monad to be specified explicitly.
    What if we want this
    to be dynamic?

    View Slide

  32. parTraverse
    We switch from parMapN
    to parTraverse.
    parTraverse accepts a List
    and maps the List in parallel.

    View Slide

  33. Sequential
    Parallel

    View Slide

  34. Calling Parallel Monads
    The same way we call regular monad comprehensions.

    View Slide

  35. Error messages
    Notice: Either.Left -> println(it.a.message)

    View Slide

  36. Error messages
    Notice: Either.Left -> println(it.a.message)
    If "part3.txt" is missing, we get:
    data/part3.txt (No such file or directory)
    An exception isn't thrown (program won't blow up). This is good!
    Instead, the error message returns via algebraic data type Either.Left.

    View Slide

  37. The End

    View Slide

  38. Matt Moore
    Blog:
    Lambda Show Podcast:
    (Tech Industry Interviews + Tutorials)
    GitHub:
    Twitter:
    Email:
    Practical Functional Programming:
    (Kotlinlang Slack)
    https://mattmoore.io
    https://lambda.show
    https://github.com/mattmoore
    https://twitter.com/@mattmoore_io
    [email protected]
    #practical-functional-programming

    View Slide