90

我似乎真的不了解 Map 和 FlatMap。我不明白的是,理解如何是对 map 和 flatMap 的一系列嵌套调用。以下示例来自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 背后的基本原理,那就太好了。

4

5 回答 5

217

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

我会试着回顾一下。

定义

for理解是一种语法快捷方式,可以以一种易于阅读和推理的方式flatMap组合起来。map

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

例子

一些常见的单子包括:

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

地图和平面地图

在通用单子中定义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]

例如

  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 表达式<-被转换为一个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表达式也是如此: aList仍然是 a List,其内容由yield.

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

假设每个内部绑定都被转换为一个map调用,但右手是同一个A => M[B]函数,你最终会在推导中得到一个M[M[B]]for each line。
整个for语法的目的是轻松地“扁平化”连续单子操作的串联(即“提升”“单子形状”中的值的操作:) ,并添加可能执行结论转换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)

  valuesList reduce (_ + _)
}

你能猜出它的类型valuesList吗?

如前所述,the 的形状monad是通过推导来维持的,所以我们从 a Listin开始company.branches,并且必须以 a 结束List
相反,内部类型会发生变化并由yield表达式确定:customer.value: Int

valueList应该是一个List[Int]

于 2013-01-30T10:48:57.480 回答
7

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

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

我们的计划

  1. 了解 scala 的map.
  2. 了解 scala 的flatMap.
  3. 了解 scala 的for comprehension.`

斯卡拉的地图

scala地图签名:

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

但是当我们看到这个签名时,有一个很大的部分丢失了,它是——它是A从哪里来的?我们的容器是类型的A,所以在容器的上下文中查看这个函数很重要 - M[A]。我们的容器可以是一个List类型的项目,A我们的map函数采用一个函数将每个类型的项目转换A为 type 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],您无法控制它。让我们再次强调一下:

  1. map为我们选择输出容器,它将与我们处理的源相同,因此对于M[A]容器,我们只得到相同的M容器B M[B],没有别的!
  2. map为我们做这个容器化,我们只给一个映射 from AtoB它将把它放在盒子里,M[B]然后把它放在盒子里!

您看到您没有指定如何对containerize项目进行转换,您只是指定了如何转换内部项目。因为我们有相同的容器M,这意味着是相同的容器M[A],这意味着如果你有,那么你将拥有一个,更重要的是为你做!M[B]M[B]List[A]List[B]map

既然我们已经处理了,map让我们继续flatMap

Scala 的平面地图

让我们看看它的签名:

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]还是 map 本身为我们容器化呢?

您会看到正在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,不要将它打包回来我的意思是让地图确定包装让下一个流水线工人确定container.mkMatcher(pat2) map (g => f(s) ...)) // 因为这是装配线中的最后一个函数,我们将使用 map 并将 g 从容器中拉出并返回包装,它map和这个包装将一路加速,成为我们的包裹或容器,是的!

于 2017-05-25T13:53:59.687 回答
5

基本原理是链接一元操作,这提供了一个好处,适当的“快速失败”错误处理。

其实很简单。该mkMatcher方法返回一个Option(它是一个 Monad)。的结果mkMatcher,一元操作,要么是 a 要么是Nonea Some(x)

maporflatMap函数应用于 aNone总是返回 a None- 作为参数传递给的函数map并且flatMap不被评估。

因此,在您的示例中,如果mkMatcher(pat)返回 None ,则应用于它的 flatMap 将返回 a (不会执行None第二个单子操作)并且 final将再次返回 a 。换句话说,如果 for comprehension 中的任何操作返回 None,则您有一个快速失败的行为,并且不会执行其余操作。mkMatcher(pat2)mapNone

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

最后一点:该patterns函数是一种将命令式错误处理(try... catch)“转换”为单子式错误处理的典型方式,使用Option

于 2013-01-30T08:22:22.283 回答
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- 遍历 in 中的每个元素pat并 foreach 元素map到 in 中的每个元素pat2

于 2013-01-30T08:14:28.343 回答
0

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

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

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

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

请注意,p变量绑定到编译模式。现在,很清楚带有签名的函数是如何String => BooleanmkMatcher.

接下来,让我们检查一下bothMatch基于mkMatcher. 为了展示如何bothMathch工作,我们首先看这部分:

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

因为我们得到了一个带有 from 签名的函数,String => BooleanmkMatcher这个g上下文中,g(s)它等价于Pattern.compile(pat2).macher(s).matches,如果 String s 匹配 pattern 则返回pat2。那么怎么样f(s),和 一样g(s),唯一不同的是,第一次调用mkMatcheruses flatMap,而不是map,为什么?因为return ,如果你同时使用这两个调用,你会得到一个嵌套的结果mkMatcher(pat2) map (g => ....),这不是你想要的。Option[Boolean]Option[Option[Boolean]]map

于 2013-01-30T08:14:11.987 回答