与flatMap / Map转换的理解混淆
我似乎并不了解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的基本原理,那将是非常棒的。
TL; DR直接进入最后的例子
我会尽力回顾
定义
理解是一种语法快捷方式,以便于阅读和理解的方式将flatMap
和map
结合起来。
让我们稍微简化一下,并假定提供上述两种方法的每个class
都可以称为monad
,我们将使用符号M[A]
表示具有内部typesA
的monad
。
例子
一些常见的单子
-
List[String]
在哪里-
M[_]: List[_]
-
A: String
-
-
Option[Int]
其中-
M[_]: Option[_]
-
A: Int
-
-
Future[String => Boolean]
其中-
M[_]: Future[_]
-
A: String => Boolean
-
地图和flatMap
定义在通用monad M[A]
/* applies a transformation of the monad "content" mantaining the * monad "external shape" * ie 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)
expression
-
expression式中使用
<-
symbol的每一行都被转换为一个flatMap
调用,除了最后一行被转换为一个结束map
调用,其中左侧的“bound symbol”作为parameter passing给参数函数(我们之前称之为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
-
只有一个
<-
的expression式被转换为map
调用,expression式作为parameter passing:// 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
expression式也是如此: List
保持一个List
,其内容通过yield
的操作转换
另一方面, for
中的每个装订线只是连续monads
的组合,为了保持单个“外部形状”,必须将其“拼合”
假设每一个内部绑定都被转换成一个map
调用,但是右边的是相同的A => M[B]
函数,那么最终会得到一个M[M[B]]
理解。
整个语法的目的是容易“扁平化”连续的一元运算的连接(即“提升”一个单值forms的值的操作: A => M[B]
),并添加一个final map
操作可能会进行一个结论性的转换
我希望这解释了翻译select背后的逻辑,这是以机械方式应用的,即: n
flatMap
通过一个map
调用结束的嵌套调用。
一个人为的例子
意思是显示for
语法的performance力
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
的types吗?
正如已经说过的那样, monad
的形状是通过理解来保持的,所以我们从company.branches
的List
开始,并且必须以List
结束。
内部types改变,并由yield
expression式确定:which is customer.value: Int
valueList
应该是一个List[Int]
其基本原理是链接作为一个好处的monadic操作,适当的“快速失败”的error handling。
这其实很简单。 mkMatcher
方法返回一个Option
(这是一个Monad)。 mkMatcher
操作mkMatcher
的结果是None
或Some(x)
。
将map
或flatMap
函数应用于None
始终返回None
– 作为parameter passing给map
和flatMap
函数不会被评估。
因此,在你的例子中,如果mkMatcher(pat)
返回None,那么应用到它的flatMap将返回一个None
(第二个mkMatcher(pat2)
操作mkMatcher(pat2)
将不会被执行),最终的map
将再次返回一个None
。 换句话说,如果理解中的任何一个操作返回一个无,你就有一个快速失败的行为,其余的操作不会被执行。
这是error handling的一元风格。 命令式使用exception,基本上是跳转(到catch子句)
最后一点: patterns
函数是一种将命令式error handling( try
… catch
)“翻译”为使用Option
的一元式error handling的典型方法
这可以被转换为:
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
每个元素循环,并且foreach元素map
其map
到pat2
每个元素
我不是一个斯卡拉mega的头脑,所以请随时纠正我,但这是我如何解释flatMap/map/for-comprehension
传奇给我自己!
为了理解for comprehension
和对scala's map / flatMap
的翻译,我们必须采取小步骤,理解构成部分 – map
和flatMap
。 但是,不是scala's flatMap
只是map
flatten
你问自己! 如果是这样的话,为什么很多开发者会觉得很难掌握它或者for-comprehension / flatMap / map
。 那么,如果你只看scala的map
和flatMap
签名,你会发现它们返回相同的返回typesM[B]
并且它们使用相同的input参数A
(至less是它们所采用函数的第一部分)有什么区别?
我们的计划
- 了解斯卡拉的
map
。 - 理解scala的
flatMap
。 - 理解斯卡拉的
for comprehension
斯卡拉的地图
斯卡拉地图签名:
map[B](f: (A) => B): M[B]
但是当我们看这个签名的时候,有一个很大的缺失,就是 – 这个A
来自哪里? 我们的容器是A
typesA
所以在容器的上下文中看这个函数很重要 – M[A]
。 我们的容器可以是一个A
types的项目List
,我们的map
函数使用一个函数将A
types的每个项目转换成B
types,然后返回一个B
types的容器(或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]
您无法控制它。 让我们再次强调一下:
-
map
为我们select了输出容器,并且它将和我们的源代码一样是容器,所以对于M[A]
容器,我们只为B
M[B]
获得相同的M
容器,没有别的! -
map
为我们做这种容器化,我们只是给出一个从A
到B
的映射,它会把它放在M[B]
的框中,将它放在我们的盒子里!
你看到你没有指定如何containerize
你刚刚指定的项目如何转换内部项目。 因为我们对于M[A]
和M[B]
都有相同的容器M
,这意味着M[B]
是同一个容器,这意味着如果你有List[A]
那么你将有一个List[B]
和更重要的是map
是为你做的!
现在我们已经处理了map
让我们继续前往flatMap
。
斯卡拉的flatMap
让我们看看它的签名:
flatMap[B](f: (A) => M[B]): M[B] // we need to show it how to containerize the A into M[B]
在flatMap中,我们看到了从map到flatMap
的巨大差异,我们提供的function不是从A to B
转换A to B
而是将其转换为M[B]
。
为什么我们关心集装箱化的人呢?
那么为什么我们如此关心map / flatMap的input函数将容器集成到M[B]
还是映射本身为我们做了容器化呢?
您在for comprehension
的背景下看到发生了什么事情,是for
让我们的assembly线中的下一个工人能够确定包装,而在工件中提供的项目上进行了多次转换。 想象一下,我们有一条assembly线,每个工人对产品做了一些事情,只有最后一名工人将它包装在一个容器中! 欢迎来到flatMap
这是它的目的,在map
每个工作人员完成该项目的工作也打包它,让你的容器上的容器。
强大的理解力
现在让我们看看你的理解,考虑到我们上面所说的:
def bothMatch(pat:String,pat2:String,s:String):Option[Boolean] = for { f <- mkMatcher(pat) g <- mkMatcher(pat2) } yield f(s) && g(s)
我们在这里得到了什么:
-
mkMatcher
返回一个container
,容器包含一个函数:String => Boolean
- 规则是如果我们有多个
<-
他们翻译成flatMap
除了最后一个。 - 由于
f <- mkMatcher(pat)
是第一个sequence
(想象assembly line
)所有我们想要的就是把它传递给stream水线中的下一个工人,我们让下一个工人在我们的stream水线下一个function)能够确定什么是我们的项目包装后面这就是为什么最后一个function是map
。 -
最后的
g <- mkMatcher(pat2)
将使用map
这是因为它的最后一个在stream水线! 所以可以用map( g =>
来做最后的操作map( g =>
是的!拉出g
并使用已经从flatMap
从容器中拉出来的f
,因此我们以第一个结束:mkMatcher(pat)flatMap(f //把f函数给出给下一个stream水线工作者(你看它有权访问
f
,不打包回来,我的意思是让映射确定包装让下一个stream水线工人确定mkMatcher(pat2)map(g => f(s)…))//因为这是assembly线中的最后一个函数,我们将使用map和pull g从容器和包装回来,它的map
和这个包装会一路飙升,成为我们的包装或我们的容器,呀!
首先, mkMatcher
返回一个函数,其签名是String => Boolean
,这是一个正常运行Pattern.compile(string)
java过程,如pattern
函数中所示。 然后,看看这一行
pattern(pat) map (p => (s:String) => p.matcher(s).matches)
map
函数应用于pattern
的结果,即Option[Pattern]
,所以p => xxx
中的p => xxx
就是你编译的模式。 因此,给定一个模式p
,构造一个新的函数,它接受一个strings
,并检查s
与模式匹配。
(s: String) => p.matcher(s).matches
请注意, p
variables是绑定到编译模式。 现在,清楚的是,具有签名String => Boolean
的函数是如何由mkMatcher
构造的。
接下来,我们来bothMatch
基于mkMatcher
的bothMatch
函数。 为了展示bothMathch
如何工作,我们首先看看这个部分:
mkMatcher(pat2) map (g => f(s) && g(s))
由于我们从mkMatcher
获得了一个具有签名String => Boolean
的mkMatcher
,在这个上下文中, g(s)
等同于Pattern.compile(pat2).macher(s).matches
,如果String匹配模式pat2
。 那么f(s)
怎么样,和g(s)
是一样g(s)
,唯一不同的是, mkMatcher
的第一个调用是使用flatMap
而不是map
,为什么? 因为mkMatcher(pat2) map (g => ....)
返回Option[Boolean]
,所以如果两个调用都使用map
,那么将得到一个嵌套的结果Option[Option[Boolean]]
,这不是你想要的。