如何让嵌套的 flatMap 和 map 更容易理解

Cou*_*per 5 monads functional-programming for-comprehension swift

假设,我们有一个 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) }
}
Run Code Online (Sandbox Code Playgroud)

以及一些计算值 ( 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)
}
Run Code Online (Sandbox Code Playgroud)

现在,假设我们要计算 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)")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但这看起来不太好理解。有没有办法改善它?例如,使用自定义运算符?

Lui*_*las 1

好吧,作为参考,最好在这里记录 Haskell 在这种情况下所做的事情:

\n\n
example1 = do\n  arg1 <- task1\n  arg2 <- task2\n  arg3 <- task3\n  value <- task4 arg1 arg2 arg3\n  putStrLn ("Result: " ++ show value)\n
Run Code Online (Sandbox Code Playgroud)\n\n

这对运算符进行了脱糖>>=处理,它是一个翻转的中缀 flatMap:

\n\n
-- (>>=) :: Monad m => m a -> (a -> m b) -> m b\n-- \n-- It\'s a right-associative operator\n\nexample2 = task1 >>= \\arg1 -> \n             task2 >>= \\arg2 -> \n               task3 >>= \\arg3 -> \n                 task4 arg1 arg2 arg3 >>= \\value ->\n                   putStrLn ("Result: " ++ show value)\n
Run Code Online (Sandbox Code Playgroud)\n\n

所以是的,你在这里所做的就是重新发现 Haskell 的do-notation\xe2\x80\x94 的动机,它正是一种用于编写嵌套平面映射的特殊平面语法!

\n\n

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

\n\n
import Control.Applicative (liftA3, (<$>), (<*>))\n\n-- `liftA3` is the generic "three-argument map" function, \n-- from `Control.Applicative`.\nexample3 = do\n  -- `liftA3 (,,)` is a task that puts the results of its subtasks\n  -- into a triple.  We then flatMap over this task and pattern match\n  -- on its result. \n  (arg1, arg2, arg3) <- liftA3 (,,) task1 task2 task3\n  value <- task4 arg1 arg2 arg3\n  putStrLn ("Result: " ++ show value)\n\n-- Same thing, but with `<$>` and `<*>` instead of `liftA3`\nexample4 = do\n  (arg1, arg2, arg3) <- (,,) <$> task1 <*> task2 <*> task3\n  value <- task4 arg1 arg2 arg3\n  putStrLn ("Result: " ++ show value)\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果task1task2task3返回相同的类型,则另一种扁平化方法是使用该类Traversable(其底部采用与Applicative上面相同的技术):

\n\n
import Data.Traversable (sequenceA)\n\nexample5 = do\n  -- In this use, sequenceA turns a list of tasks into a\n  -- task that produces a list of the originals results.\n  [arg1, arg2, arg3] <- sequenceA [task1, task2, task3]\n  value <- task4 arg1 arg2 arg3\n  putStrLn ("Result: " ++ show value)\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
    \n
  1. 将异构类型的任务组合成一个组合。签名看起来像(M<A1>, ..., M<An>) -> M<(A1, ..., An)>
  2. \n
  3. 多任务映射:将n处函数映射到生成适当类型的n 个任务上。
  4. \n
  5. 将一系列任务转换为产生一系列结果的任务。
  6. \n
\n\n

请注意#1 和#2 具有相同的功率。另请注意,如果我们谈论异步任务,这些操作比平面映射有一个优势,即它们更容易并行化。

\n