Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make nested flatMap and map more comprehensible

Suppose, we have a struct M:

public struct M<T> {
    let value: T
    public init(_ value: T) { self.value = value }

    public func map<U>(f: T -> U) -> M<U> { return M<U>(f(value)) }
    public func flatMap<U>(f: T -> M<U>) -> M<U> { return f(value) }
}

and a few functions which compute a value (T) and return it as a wrapped value with M:

func task1() -> M<Int> {
    return M(1)
}

func task2(value: Int = 2) -> M<Int> {
    return M(value)
}

func task3(value: Int = 3) -> M<Int> {
    return M(value)
}

func task4(arg1: Int, arg2: Int, arg3: Int) -> M<Int> {
    return M(arg1 + arg2 + arg2)
}

Now, suppose we want to compute the values of task1, task2 and task3 and then pass all three computed values as arguments to task4. It seems, this requires to use nested calls to flatMap and map:

let f1 = task1()
let f2 = task2()
let f3 = task3()

f1.flatMap { arg1 in
    return f2.flatMap { arg2 in
        return f3.flatMap { arg3 in
            return task4(arg1, arg2:arg2, arg3:arg3).map { value in
                print("Result: \(value)")
            }
        }
    }
}

But that doesn't look quite comprehensible. Is there a way to improve that? For example, using custom operators?

like image 403
CouchDeveloper Avatar asked Nov 08 '22 23:11

CouchDeveloper


1 Answers

Well, for reference, it would do well to document here what Haskell does in this situation:

example1 = do
  arg1 <- task1
  arg2 <- task2
  arg3 <- task3
  value <- task4 arg1 arg2 arg3
  putStrLn ("Result: " ++ show value)

This desugars to the >>= operator, which is a flipped infix flatMap:

-- (>>=) :: Monad m => m a -> (a -> m b) -> m b
-- 
-- It's a right-associative operator

example2 = task1 >>= \arg1 -> 
             task2 >>= \arg2 -> 
               task3 >>= \arg3 -> 
                 task4 arg1 arg2 arg3 >>= \value ->
                   putStrLn ("Result: " ++ show value)

So yeah, what you've done here is rediscover the motivation for Haskell's do-notation—it's precisely a special flat syntax for writing nested flatMaps!

But here's another trick that might be relevant to this example. Note that in your computation, task1, task2 and task3 don't have any interdependencies. This can be the basis for designing a "flat" utility construct for merging them into one task. In Haskell, you can do this easily with the Applicative class and pattern matching:

import Control.Applicative (liftA3, (<$>), (<*>))

-- `liftA3` is the generic "three-argument map" function, 
-- from `Control.Applicative`.
example3 = do
  -- `liftA3 (,,)` is a task that puts the results of its subtasks
  -- into a triple.  We then flatMap over this task and pattern match
  -- on its result. 
  (arg1, arg2, arg3) <- liftA3 (,,) task1 task2 task3
  value <- task4 arg1 arg2 arg3
  putStrLn ("Result: " ++ show value)

-- Same thing, but with `<$>` and `<*>` instead of `liftA3`
example4 = do
  (arg1, arg2, arg3) <- (,,) <$> task1 <*> task2 <*> task3
  value <- task4 arg1 arg2 arg3
  putStrLn ("Result: " ++ show value)

If task1, task2 and task3 return the same type, then another way of flattening it is to use the Traversable class (which bottoms out to the same Applicative technique as above):

import Data.Traversable (sequenceA)

example5 = do
  -- In this use, sequenceA turns a list of tasks into a
  -- task that produces a list of the originals results.
  [arg1, arg2, arg3] <- sequenceA [task1, task2, task3]
  value <- task4 arg1 arg2 arg3
  putStrLn ("Result: " ++ show value)

So one idea then is to build an utility library that provides similar functionality. Some example operations:

  1. Combine heterogeneously typed tasks into a composite. Signatures look like (M<A1>, ..., M<An>) -> M<(A1, ..., An)>
  2. Multi-task map: map an n-place function over n tasks that produce the appropriate types.
  3. Turn a sequence of tasks into a task that produces a sequence of their results.

Note that #1 and #2 have equivalent power. And also note that if we're talking about asynchronous tasks, these operations have an advantage over flatmaps, which is that they're much easier to parallelize.

like image 115
Luis Casillas Avatar answered Dec 26 '22 14:12

Luis Casillas