如何使嵌套的flatMap和map更易于理解

时间:2015-12-16 13:24:27

标签: swift functional-programming monads for-comprehension

假设我们有一个结构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) }
}

以及一些计算值(T)的函数,并将其作为包装值返回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)
}

现在,假设我们想要计算task1,task2和task3的值,然后将所有三个计算值作为参数传递给task4。看来,这需要使用嵌套调用flatMapmap

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

但这看起来并不易理解。有办法改善吗?例如,使用自定义运算符?

1 个答案:

答案 0 :(得分:1)

嗯,作为参考,在这里记录Haskell在这种情况下的作用会很好:

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

这对于>>=运算符来说,这是一个翻转的中缀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)

所以是的,你在这里所做的是重新发现Haskell do的动机 - 符号 - 它正是一种用于编写嵌套flatMaps的特殊平面语法!

但这是另一个可能与此示例相关的技巧。请注意,在您的计算中,task1task2task3没有任何相互依赖关系。这可以作为设计“扁平”实用程序构造以将它们合并为一个任务的基础。在Haskell中,您可以使用Applicative类和模式匹配轻松完成此操作:

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)

如果task1task2task3返回相同的类型,那么另一种展平方式是使用Traversable类(最低限度为Applicative {1}}技术如上所述):

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)

因此,一个想法就是构建一个提供类似功能的实用程序库。一些示例操作:

  1. 将异构类型的任务组合成复合词。签名看起来像(M<A1>, ..., M<An>) -> M<(A1, ..., An)>
  2. 多任务地图:将 n -place函数映射到生成相应类型的 n 任务上。
  3. 将一系列任务转换为生成结果序列的任务。
  4. 请注意,#1和#2具有相同的功率。还要注意,如果我们谈论异步任务,这些操作比平面图更有优势,那就是它们更容易并行化。