对flatMap / Map转换的理解感到困惑

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对flatMap / Map转换的理解感到困惑相关的知识,希望对你有一定的参考价值。

我似乎真的不了解Map和FlatMap。我无法理解的是for-comprehension是如何嵌套调用map和flatMap的。以下示例来自Functional Programming in Scala

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
            f <- mkMatcher(pat)
            g <- mkMatcher(pat2)
 } yield f(s) && g(s)

翻译成

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = 
         mkMatcher(pat) flatMap (f => 
         mkMatcher(pat2) map (g => f(s) && g(s)))

mkMatcher方法定义如下:

  def mkMatcher(pat:String):Option[String => Boolean] = 
             pattern(pat) map (p => (s:String) => p.matcher(s).matches)

模式方法如下:

import java.util.regex._

def pattern(s:String):Option[Pattern] = 
  try {
        Some(Pattern.compile(s))
   }catch{
       case e: PatternSyntaxException => None
   }

如果有人能够阐明在这里使用map和flatMap背后的理由,那将是很棒的。

答案

TL; DR直接进入最后的例子

我会试着回顾一下

定义

for理解是一种语法快捷方式,以一种易于阅读和推理的方式组合flatMapmap

让我们稍微简化一下,并假设提供上述两种方法的每个class都可以称为monad,我们将使用符号M[A]来表示具有内部类型monadA

例子

一些常见的单子

  • List[String]在哪里 M[X] = List[X] A = String
  • Option[Int]在哪里 M[X] = Option[X] A = Int
  • Future[String => Boolean]在哪里 M[X] = Future[X] A = (String => Boolean)

地图和flatMap

定义在通用monad M[A]

 /* applies a transformation of the monad "content" mantaining the 
  * monad "external shape"  
  * i.e. a List remains a List and an Option remains an Option 
  * but the inner type changes
  */
  def map(f: A => B): M[B] 

 /* applies a transformation of the monad "content" by composing
  * this monad with an operation resulting in another monad instance 
  * of the same type
  */
  def flatMap(f: A => M[B]): M[B]

EG

  val list = List("neo", "smith", "trinity")

  //converts each character of the string to its corresponding code
  val f: String => List[Int] = s => s.map(_.toInt).toList 

  list map f
  >> List(List(110, 101, 111), List(115, 109, 105, 116, 104), List(116, 114, 105, 110, 105, 116, 121))

  list flatMap f
  >> List(110, 101, 111, 115, 109, 105, 116, 104, 116, 114, 105, 110, 105, 116, 121)

表达

  1. 使用<-符号的表达式中的每一行都被转换为flatMap调用,除了最后一行被转换为结束map调用,其中左侧的“绑定符号”作为参数传递给参数功能(我们以前称之为f: A => M[B]): // The following ... for { bound <- list out <- f(bound) } yield out // ... is translated by the Scala compiler as ... list.flatMap { bound => f(bound).map { out => out } } // ... which can be simplified as ... list.flatMap { bound => f(bound) } // ... which is just another way of writing: list flatMap f
  2. 只有一个<-的for-expression被转换为map调用,表达式作为参数传递: // The following ... for { bound <- list } yield f(bound) // ... is translated by the Scala compiler as ... list.map { bound => f(bound) } // ... which is just another way of writing: list map f

现在到了这一点

正如你所看到的,map操作保留了原始monad的“形状”,因此yield表达式也是如此:List仍然是List,其内容由yield中的操作转换。

另一方面,for中的每条绑定线只是连续的monads的组合,必须“展平”以保持单一的“外部形状”。

假设片刻将每个内部绑定转换为map调用,但右手是相同的A => M[B]函数,你最终会在理解中为每一行输出一个M[M[B]]。 整个for语法的目的是轻松“扁平化”连续monadic操作的连接(即“提升”“monadic形状”中的值的操作:A => M[B]),添加最终的map操作,可能执行结束转型。

我希望这解释了翻译选择背后的逻辑,它以机械方式应用,即:n flatMap嵌套调用由单个map调用结束。

一个人为的说明性例子 意味着展示for语法的表现力

case class Customer(value: Int)
case class Consultant(portfolio: List[Customer])
case class Branch(consultants: List[Consultant])
case class Company(branches: List[Branch])

def getCompanyValue(company: Company): Int = {

  val valuesList = for {
    branch     <- company.branches
    consultant <- branch.consultants
    customer   <- consultant.portfolio
  } yield (customer.value)

  valueList reduce (_ + _)
}

你能猜出valuesList的类型吗?

正如已经说过的那样,monad的形状是通过理解来维持的,所以我们从List中的company.branches开始,并且必须以List结束。 内部类型改变并由yield表达式确定:这是customer.value: Int

valueList应该是List[Int]

另一答案

我不是一个scala mega mind,所以请随意纠正我,但这就是我向自己解释flatMap/map/for-comprehension传奇的方式!

要了解for comprehension及其对scala's map / flatMap的翻译,我们必须采取一些小步骤,并了解作曲部分 - mapflatMap。但不是scala's flatMap只是mapflatten你问自己!如果是这样,为什么这么多开发人员发现很难掌握它或for-comprehension / flatMap / map。好吧,如果你只看scala的mapflatMap签名,你会发现它们返回相同的返回类型M[B]并且它们在相同的输入参数A(至少是它们所采用的函数的第一部分)上工作,如果那是什么产生影响呢?

我们的计划

  1. 了解斯卡拉的map
  2. 了解斯卡拉的flatMap
  3. 了解斯卡拉的for comprehension

早上的地图

scala地图签名:

map[B](f: (A) => B): M[B]

但是当我们看到这个签名时,有很大一部分缺失,而且 - 这就是A来自哪里?我们的容器是A类型,因此在容器的上下文中查看此函数非常重要 - M[A]。我们的容器可能是List类型的A,我们的map函数采用将A类型的每个项目转换为B类型的函数,然后返回B类型的容器(或M[B]

让我们在考虑容器的情况下编写地图的签名:

M[A]: // We are in M[A] context.
    map[B](f: (A) => B): M[B] // map takes a function which knows to transform A to B and then it bundles them in M[B]

请注意关于map的极其重要的事实 - 它在输出容器M[B]中自动捆绑,你无法控制它。让我们再次强调它:

  1. map为我们选择输出容器,它将与我们工作的源相同,所以对于M[A]容器,我们只为M B获得相同的M[B]容器,没有别的!
  2. map为我们做了这个容器化我们只是给出了从AB的映射,它会把它放在M[B]的盒子里,将它放在我们的盒子里!

您看到您没有指定如何containerize您刚刚指定的项目如何转换内部项目。因为MM[A]都有相同的容器M[B]这意味着M[B]是同一个容器,这意味着如果你有List[A]那么你将有一个List[B],更重要的是map正在为你做这个!

现在我们已经处理了map让我们继续前进到flatMap

早上的平面图

让我们看看它的签名:

flatMap[B](f: (A) => M[B]): M[B] // we need to show it how to containerize the A into M[B]

你看到从map到flatMap在flatMap中的巨大差异我们提供的功能不仅仅是从A to B转换而且还将它集成到M[B]中。

为什么我们关心谁进行集装箱化?

那么为什么我们如此关注map / flatMap的输入函数是否容器化为M[B]或地图本身是否为我们进行了容器化?

你在for comprehension的上下文中看到正在发生的是for中提供的项目的多次转换,因此我们为装配线中的下一个工人提供了确定包装的能力。想象一下,我们有一条装配线,每个工人都对产品做了一些事情,只有最后一个工人将它包装在容器中!欢迎来到flatMap这是它的目的,在map,每个工人在完成该项目的工作时也包装它,这样你就可以将容器放在容器上。

强大的理解力

现在让我们考虑一下我们上面所说的内容:

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
    f <- mkMatcher(pat)   
    g <- mkMatcher(pat2)
} yield f(s) && g(s)

我们在这里得到了什么:

  1. mkMatcher返回一个container容器包含一个函数:String => Boolean
  2. 规则是如果我们有多个<-他们转换为flatMap除了最后一个。
  3. 由于f <- mkMatcher(pat)sequence(想想assembly line)的第一个,我们想要的就是拿出f并将其传递给装配线上的下一个工人,我们让下一个工人在我们的装配线(下一个功能)中确定能力什么是我们项目的包装背面这就是为什么最后一个功能是map
  4. 最后一个g <- mkMatcher(pat2)将使用map这是因为它的最后一个装配线!所以它可以用map( g =>做最后的操作,是的!拉出g并使用已经从f从容器中拉出的flatMap因此我们最终得到了第一个: mkMatcher(pat)flatMap(f //拉出f函数给下一个装配线工人提供项目(你看它有权访问f,不要打包回来我的意思是让地图确定包装让下一个装配线工人确定容器.mkMatcher(pat2)map(g => f(s)...))//因为这是装配线中的最后一个函数,我们将使用map并将g拉出容器并打包到包装中回来,它的map和这个包装将一路飙升,成为我们的包装或我们的容器,是啊!
另一答案

理由是将monadic操作链接起来,提供正确的“快速失败”错误处理。

它实际上非常简单。 mkMatcher方法返回一个Option(这是一个Monad)。 mkMatcher,monadic操作的结果是NoneSome(x)

mapflatMap函数应用于None始终返回None - 作为参数传递给map的函数,并且不评估flatMap

因此在你的例子中,如果mkMatcher(pat)返回None,则应用于它的flatMap将返回None(第二个monadic操作mkMatcher(pat2)将不会执行),最后的map将再次返回None。换句话说,如果for c

以上是关于对flatMap / Map转换的理解感到困惑的主要内容,如果未能解决你的问题,请参考以下文章

理解Swift中map 和 flatMap对集合的作用

对代码中的类结构感到困惑[关闭]

对“Scala in Depth”选项示例感到困惑

对减少和箭头功能感到困惑[重复]

对 let 到 lambda 转换感到困惑

如何将Perl Map函数转换为Python? [关闭]