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

时间:2013-01-30 07:49:35

标签: scala map monads for-comprehension

我似乎真的不了解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背后的理由,那将会很棒。

5 个答案:

答案 0 :(得分:180)

TL; DR直接进入最后一个例子

我会试着回顾一下

<强>解释

for理解是一种语法快捷方式,可以将flatMapmap结合起来,使其易于理解和推理。

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

<强>实施例

一些常见的monads

  • 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]

e.g。

  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
    
  3. 现在到了

    正如您所看到的,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调用结束。< / p>

    一个人为的说明性例子
    意味着显示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]

答案 1 :(得分:5)

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

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

我们的计划

  1. 了解scala&#39; flatMap
  2. 了解scala&#39; for comprehension
  3. 了解scala&#39; map[B](f: (A) => B): M[B] 。“
  4. Scala的地图

    scala地图签名:

    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]

    类型的容器

    考虑到容器,让我们写地图的签名:

    M[B]

    请注意非常重要的关于地图的事实 - 它会在输出容器map中捆绑自动,而您无法控制它。让我们再次强调它:

    1. M[A]为我们选择输出容器,它将与我们处理的源容器相同,因此对于M容器,我们只为{{{{}}容器获取相同的B容器1}} M[B],别无其他!
    2. map为我们执行此容器化,我们只是提供从AB的映射,并将其放在M[B]的框中,将其放入框中对我们来说!
    3. 您发现您没有指定如何containerize您刚刚指定的项目如何转换内部项目。由于我们对MM[A]都有相同的容器M[B],这意味着M[B]是同一个容器,这意味着如果你有List[A]那么你就要去拥有List[B],更重要的是map正在为你做这件事!

      现在我们处理了map,让我们继续flatMap

      Scala的flatMap

      让我们看看它的签名:

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

      你看到在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转换为f <- mkMatcher(pat),则除了最后一个规则之外,规则就是这样。
      3. 由于sequenceassembly line中的第一个(想想f),我们想要的只是取map并将其传递给装配线中的下一个工作人员,我们让装配线中的下一个工人(下一个功能)能够确定我们项目的包装背面,这就是为什么最后一个功能是g <- mkMatcher(pat2)
      4. 最后map将使用map( g =>这是因为它在装配线上的最后一个!所以它可以用g进行最后的操作,是的!拉出f并使用flatMap已经从容器中取出的f,因此我们最终得到了第一个:

        mkMatcher(pat)flatMap(f //拉出f函数将项目提供给下一个装配线工人(你看到它有权访问map,并且不打包回来我的意思是让地图确定包装让下一个装配线工人确定容器。 mkMatcher(pat2)map(g =&gt; f(s)...))//因为这是装配线中的最后一个函数,我们将使用map并将g拉出容器并返回包装,它的{{1}}和这个包装会一路飙升,成为我们的包裹或我们的容器,是啊!

答案 2 :(得分:4)

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

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

mapflatMap函数应用于None始终会返回None - 作为参数传递给mapflatMap的函数}未被评估。

因此,在您的示例中,如果mkMatcher(pat)返回None,则应用于它的flatMap将返回None(第二个monadic操作mkMatcher(pat2)将不会执行)和final { {1}}将再次返回map。 换句话说,如果for comprehension中的任何操作返回None,则表示快速失败,其余操作不会执行。

这是一种错误处理的monadic风格。命令式样式使用异常,基本上是跳转(到catch子句)

最后一点:None函数是一种将命令式样式错误处理(patterns ... try)“转换”为使用{的monadic样式错误处理的典型方法{1}}

答案 3 :(得分:1)

这可以被翻译为:

def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for {
    f <- mkMatcher(pat)  // for every element from this [list, array,tuple]
    g <- mkMatcher(pat2) // iterate through every iteration of pat
} yield f(s) && g(s)

运行此命令以更好地查看其扩展

的方式
def match items(pat:List[Int] ,pat2:List[Char]):Unit = for {
        f <- pat
        g <- pat2
} println(f +"->"+g)

bothMatch( (1 to 9).toList, ('a' to 'i').toList)

结果是:

1 -> a
1 -> b
1 -> c
...
2 -> a
2 -> b
...

这类似于flatMap - 遍历pat中的每个元素,并将map元素pat2传递给{{1}}

中的每个元素

答案 4 :(得分:0)

首先,mkMatcher返回一个签名为String => Boolean的函数,这是一个只运行Pattern.compile(string)的常规java过程,如pattern函数所示。 然后,看看这一行

pattern(pat) map (p => (s:String) => p.matcher(s).matches)

map函数应用于pattern的结果,即Option[Pattern],因此p中的p => xxx只是您编译的模式。因此,给定模式p,构造一个新函数,它接受一个字符串s,并检查s是否与模式匹配。

(s: String) => p.matcher(s).matches

注意,p变量与编译模式有关。现在,很明显String => Boolean如何构建带有签名mkMatcher的函数。

接下来,让我们查看bothMatch函数,该函数基于mkMatcher。为了说明bothMathch的工作原理,我们先来看看这部分:

mkMatcher(pat2) map (g => f(s) && g(s))

由于我们从String => Boolean获得了一个带有签名mkMatcher的函数,在此上下文中为gg(s)等同于Pattern.compile(pat2).macher(s).matches,如果pat2返回String s匹配模式f(s)。那么g(s)如何与mkMatcher相同,唯一的区别是,flatMap的第一次调用使用的是map,而不是mkMatcher(pat2) map (g => ....),为什么?由于Option[Boolean]会返回Option[Option[Boolean]],如果您同时使用map,则会得到嵌套结果{{1}},这不是您想要的。