与对FlatMap / Map转换的理解相混淆


87

我真的似乎不太了解Map和FlatMap。我无法理解的是理解力是对map和flatMap的一系列嵌套调用的序列。以下示例来自Scala中的Functional Programming

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的基本原理,那将是很棒的。

Answers:


197

TL; DR直接转到最后一个示例

我会尝试回顾一下。

定义

for理解是一个语法快捷方式相结合flatMap,并map以一种易于阅读和推理。

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

例子

一些常见的monad包括:

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

例如

  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表达式也发生了同样的情况:a List仍然是a List,其内容由操作中的转换yield

另一方面,中的每条装订线for只是连续的组成monads,必须对其进行“展平”以保持单个“外部形状”。

假设一会儿,每个内部绑定都转换为一个map调用,但是右手是相同的A => M[B]函数,那么M[M[B]]对于理解中的每一行都将以a结尾。
整个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吗?

如前所述,monad通过理解可以保持的形状,因此我们以Listin 开头company.branches,必须以a结尾List
内部类型改为更改,并由以下yield表达式确定:customer.value: Int

valueList 应该是 List[Int]


1
单词“与...相同”属于元语言,应从代码块中移出。
一天

3
每个FP初学者都应该阅读此内容。如何做到这一点?
mert inan 2014年

1
@melston让我们举一个例子Lists。如果您将map某个函数A => List[B](这是基本的单数运算之一)的值加倍两次,则最终会得到List [List [B]](我们认为类型匹配是理所当然的)。for comprehension内部循环将这些函数与相应的flatMap操作组合在一起,将“ List [List [B]]”的形状“展平”为简单的List [B] ...我希望这很清楚
pagoda_5b 2015年

1
阅读您的答案真是太棒了。我希望您能写一本有关scala的书,您是否有博客或其他内容?
Tomer Ben David

1
@coolbreeze可能是我没有清楚表达。我的意思是该yield子句是customer.value,其类型为Int,因此整个for comprehension计算结果为List[Int]
pagoda_5b 2015年

6

我不是斯卡拉超级头脑,所以随时可以纠正我,但这就是我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. 了解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,以类型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为我们执行此容器化操作,我们只提供从A到的映射B,它将映射到的框中,M[B]将映射到我们的框中!

您会看到您没有指定如何对containerize项目进行指定,而只是指定了如何对内部项目进行转换。而且由于我们M两者都有相同的容器M[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]

您会看到从map到flatMapflatMap 的巨大区别,我们为它提供的功能不仅是将其转换为,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,并且不将其打包),我的意思是让地图确定包装,让下一个组装线工人确定container。mkMatcher(pat2)map(g => f(s)...))//因为这是装配线中的最后一个函数,我们将使用map并将g从容器中拉出并返回包装,它map和这个包装将一路加速前进,成为我们的包装或我们的容器,是的!


4

基本原理是将单子运算链接起来,这有益地提供了适当的“快速失败”错误处理。

实际上很简单。该mkMatcher方法返回一个Option(是Monad)。mkMatcher一元运算的结果是a None或a Some(x)

mapflatMap函数应用于a None总是返回None--作为参数传递给函数,map并且flatMap不进行评估。

因此,在您的示例中,如果mkMatcher(pat)返回None,则应用于它的flatMap将返回a NonemkMatcher(pat2)将不执行第二个Monadic操作),而final map将再次返回a None。换句话说,如果for理解中的任何操作返回None,则您将具有快速失败的行为,而其余操作将不会执行。

这是错误处理的单子形式。命令式样式使用异常,这些异常基本上是跳转(到catch子句)

最后一点:patterns函数是使用以下命令将命令式样式错误处理(try... catch)转换为单子样式错误处理的典型方法:Option


您知道为什么flatMap(而不是map)“连接”第一次和第二次调用mkMatcher,但为什么map(而不是flatMap)“连接”第二次mkMatcheryields块吗?
Malte Schwerhoff 2013年

1
flatMap希望您传递一个函数,将结果“包装” /提起在Monad中,而map包装/提起本身。在中的操作的调用链接期间,for comprehension您需要flatmap使作为参数传递的函数能够返回None(您不能将值提升为None)。最后一个操作调用,其中的yield预计会运行,并且返回一个值;一个map到链中最后的操作是足够的,并且具有解除功能到单子的结果避免了。
布鲁诺·格里德

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元素循环到in map中的每个元素pat2


0

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

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

map函数应用于的结果pattern,即Option[Pattern],因此pin p => xxx只是您编译的模式。因此,在给定模式的情况下p,将构造一个新函数,该函数采用String s,并检查是否s与该模式匹配。

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

注意,该p变量绑定到已编译的模式。现在,很明显,具有签名的函数String => Boolean构造的mkMatcher

接下来,让我们检查bothMatch基于的函数mkMatcher。为了展示其bothMathch工作原理,我们首先看一下这一部分:

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

因为我们得到了有特色的功能String => BooleanmkMatcher,这是g在这种情况下,g(s)等同于Pattern.compile(pat2).macher(s).matches,它的回报,如果字符串s的匹配模式pat2。那么f(s),与相同g(s),唯一的不同是,第一次调用mkMatcheruse flatMap而不是map为什么?因为mkMatcher(pat2) map (g => ....)return Option[Boolean]Option[Option[Boolean]]如果您同时使用map这两个调用,则会得到嵌套结果,这不是您想要的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.