【函数式】Monads模式初探——for解析式

for表达式是monad语法糖

先看一组示例:

case class Person(name: String, isMale: Boolean, children: Person*)

val lara = Person("Lara", false)
val bob = Person("Bob", true)
val julie = Person("Julie", false, lara, bob)
val persons = List(lara, bob, julie)

println(
  persons filter (p => !p.isMale) flatMap (p =>
    (p.children map (c => (p.name, c.name))))
)

println(
  for (p <- persons; if !p.isMale; c <- p.children)
    yield (p.name, c.name)
)
// output is
// List((Julie,Lara), (Julie,Bob))

Person类包含了人员名称,是否是男性,以及他的孩子的字段。代码的意义是找出列表中所有的妈妈和孩子结对的名称。
分别使用了map、flatMap、filter的方式进行查询,还使用了for表达式完成,得到相同的结果。

实际上,Scala编译器能够把所有使用yield产生结果的for表达式转移为高阶方法map、flatMap及filter的组合调用。所有的不带yield的for循环都会被转移为仅对filter和foreach的调用。

for表达式说明

for表达式形式如下:
for (seq) yield expr
这里,seq由生成器、定义及过滤器组成序列,以分号隔开。如果在for表达式中用花括号代替小括号包围表达式序列,那么分号是可选的。
比如下面的示例:

for (p <- persons; n = p.name; if (n startsWith "To"))
  yield n

for {
  p <- persons            //生成器
  n = p.name              //定义
  if (n startsWith "To")  //过滤器
} yield n

生成器的形式为patten <- expression,表达式expression典型的返回值是列表,不过它可以泛化。模式pattern一一匹配列表里的所有元素。如果匹配成功,模式中的变量将绑定元素的相应成分。但即使匹配失败也不会抛出MatchError,而只是在迭代中丢弃这个元素罢了。
所有的for表达式都以生成器开始。如果for表达式中有若干生成器,那么后面的生成器比前面的变化的更快。

for表达式的转译

对于每一个Monad来说,都支持for表达式,而每个for表达式都可以用三个高阶函数map、flatMap及filter表达。

基本的转译方式

  • 带一个生成器的for表达式
    for (x <- expr1) yield expr2转译为expr1.map(x => expr2)
  • 以生成器和过滤器开始的for表达式
    for (x <- expr1 if expr2) yield expr3
    第一个表达式可以转译成for (x <- expr1 filter (x => expr2)) yield expr3
  • 以两个生成器开始的for表达式
    for (x <- expr1; y <- expr2; seq) yield expr3
    假设seq是任意序列的生成器、定义及过滤器,也可能为空。两个生成器被转译为flatMap的应用:
    expr1.flatMap(x => for (y <- expr2; seq) yield expr3 )
    这就生成了另一个传递给flatMap的函数值形式的for表达式。

再举个例子:

// 第一步转译
for (n <- ns;
    o <- os;
    p <- ps)
    yield n*o*p
// 第二步转译
ns flatMap {n =>
          for(o <- os;
          p <- ps)
          yield n*o*p}
// 第三步转译
ns flatMap { n =>
          os flatMap { o =>
          for(p <- ps)
          yield n*o*p}}
// 第四步转译
ns flatMap {n =>
          os flatMap {o =>
          {ps map {p => n*o*p}}}}

转译for循环

for表达式也有一个命令式(imperative)的版本,用于那些你只调用一个函数,不返回任何值而仅仅执行了副作用,这个版本去掉了yield声明。
for循环的转译版本只需用到foreach,for (x <- expr1) body,转译为expr1 foreach (x => body)
更大的例子是,for (x <- expr1; if expr2; y <- expr3) body。它将被转译为:

expr1 filter (x => expr2) foreach (x =>
  expr3 foreach (y => body))

foreach依然可以使用map来实现:

class M[A] {
  def map[B](f: A => B): M[B] = ...
  def flatMap[B](f: A => M[B]): M[B] = ...
  def foreach[B](f: A => B): Unit = {
    map(f)
    ()
  }
}

foreach可以通过调用map并丢掉结果来实现。不过这么做运行效率不高,所以scala允许你用自己的方式定义foreach。

转译定义

如果for表达式中内嵌定义,如for (x <- expr1; y = expr2; seq) yield expr3
那么将转译为for ((x, y) <- for (x <- expr1) yield (x, expr2); seq) yield expr3
这里每次产生新的x值的时候,expr2都被重新计算。所以这可能会浪费计算资源,造成重复计算。
比如下面的例子和更好的写法:

for (x <- 1 to 100; y = expensiveComputationNotInvolvingX)
yield x*y

// better code
val y = expensiveComputationNotInvolvingX
for (x <- 1 to 1000) yield x*y

生成器中的模式

如果生成器的左侧是模式pat而不是简单变量,那么转译方法将变得复杂很多。
绑定变量元组
for ((x1, ..., xn) <- expr1) yield expr2
转译为:
expr1.map {case (x1, ..., xn) => expr2}

任意模式
for (pat <- expr1) yield expr2
转译为:

expr1 filter {
  case pat => true
  case _ => false
} map {
  case pat => expr2
}

即,生成的条目首先经过过滤并且仅有那些匹配与pat的才会被映射。因此,这保证了模式匹配生成器不会抛出MatchError。

小结

因为for表达式的转译仅依赖于map、flatMap和filter的搭配,所以可以吧for表达式应用于大批数据类型(这些数据类型可以用Monad来描述和概括)上。
除了列表、数组之外,Scala标准库中还有许多其他类型支持四种方法(map、flatMap、filter、foreach),从而允许for表达式存在。同样,如果你自己的数据类型定义了需要的方法也可以完美支持for表达式。如果只定义map、flatMap、filter、foreach这些方法的子集,从而部分支持for表达式或循环。
规则如下:

  • 如果定义了map,可以允许单一生成器组成的for表达式
  • 如果定义了flatMap和map,可以允许若干个生成器组成的for表达式
  • 如果定义了foreach,允许for循环
  • 如果定义了filter,for表达式中允许以if开头的过滤器表达式

for表达式的转译发生在类型检查之前。这可以保持最大的灵活性,因为接下来只需for表达式展开的结果通过类型检查即可。

在函数式编程中,Monad定制了map、flatMap和filter功能,它可以解释多种类型的计算,包括从集合、状态和I/O操纵的计算、回溯计算以及交易等,不一而足。

转载请注明作者Jason Ding及其出处
jasonding.top
Github博客主页(http://blog.jasonding.top/)
CSDN博客(http://blog.csdn.net/jasonding1354)
简书主页(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)
Google搜索jasonding1354进入我的博客主页

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容