map & flatMap in scala

用scala有段时间了,这篇文章是想总结一下mapflapMap的原理和用法

Scala Native

map

val l = List(10, 20, 30)
val res: List[Option[Int]] = l.map(v => if(v < 20) Some(v) else None)

// res = List(Some(10), None, None)

从这例子我们可以看到,对一个int型的List进行遍历,当数值小于20,就返回Some(value),否则返回None。

即我们通过这个map把List里的元素根据一定的逻辑转换成了Option类型,这也符合map的源码,把List里的元素,从类型A转换到类型B,最后返回List[B]:

final override def map[B](f: A => B): List[B]

flatMap

val l = List(10, 20, 30)
val res: List[Int] = l.flatMap(v => if(v < 20) Some(v) else None)

// res = List(10)

可以看出来flatMap比map多做的一个操作是,把List(Some(10), None, None)转换成List(10), 查看源码:

final override def flatMap[B](f: A => IterableOnce[B]): List[B]

可以看出来flatMap是把类型A转换成了IterableOnce类型, 这里能说明Option是IterableOnce的子类。

但是为什么最后返回的是Int类型?是什么操作把IterableOnce类型转换成了Int类型?

val l = List(10, 20, 30)
val res: List[Option[Int]] = l.map(v => if(v < 20) Some(v) else None)
val res1: List[Int] = res.flatten

// res = List(Some(10), None, None)
// res1 = List(10)

看起来是flatMap比map多调用了flatten方法。

对res做flatten操作之后可以看到原来res里的Option类型的元素都被“拍平”了,那么是谁帮我们做了这件事呢?显而易见是scala内置的方法,应该是Option里把这个方法implicit了,这里我们不深究

OK,到这儿基本可以总结一下了,flatMap = map + flatten

另外补充两点:

  • flatMap只能把List里的元素拍平一层。
    比如把Option[String] 解成 String,并不能直接解成 Char,根本原因是只能implicit一次。

  • flatMap并不是能把任何类型都能解开。

比如我们自己定义一个case class Person(age: Int),然后在调用flatMap的时候给了一个Int => Person的方法,那么编译器会报错,因为他没有找到一个Person => IterableOnce的方法。刚才我们说Option里一定有一个implicit的方法就是这个原因。

Cats IO

map

val io = IO(5)
val r1: IO[Boolean] = io.map(v => if(v > 3) true else false)
print(r1.unsafeRunSync())  //true

这里调用的map是cats库里的map,可以拿到IO里的元素然后根据传入的参数进行转换,源码:

final def map[B](f: A => B): IO[B]

套用到刚才给的例子里,我们传入一个Int => Boolean的方法,然后map会返回一个IO[Boolean]

flatMap

val io = IO(5)
val r1: IO[Boolean] = io.flatMap(v => if(v > 3) IO(true) else IO(false))
val r: IO[IO[Boolean]] = io.map(v => if(v > 3) IO(true) else IO(false))
print(r1.unsafeRunSync())   //true
print(r1.unsafeRunSync().unsafeRunSync())   //true

可以看出来,跟map的区别是,IO的flatMap可以把返回的IO再解开,源码:

final def flatMap[B](f: A => IO[B]): IO[B]

所以一般遇到需要用IO的flatMap的场景一般是需要raise error,比如:

val r1: IO[Boolean] = io.flatMap(v => if(v > 3) IO(true) else IO.raiseError(new RuntimeException))

因为返回的类型是一定的,所以不能前一半返回Boolean,后一半返回IO,这时如果使用flatMap就会方便许多

for

之前举的几个例子都比较简单,如果遇到了复杂的情况:

val r = List(Some(List(Some(10), Some(20), None)),
        Some(List(Some(20), Some(30), Some(40))),
        Some(List(Some(3))), None)
      
val result = r.flatMap(v => {
if(v.get.size > 1)
    v.get.flatMap(n => if(n.getOrElse(0) > 10) n else None)
else
    None
})
 
//result = List(20, 20, 30, 40)

这里模拟了一个比较复杂的场景,就是比如有一个类型为List[Option[List[Option[Int]]]]的变量,然后设置一些condition,最终我们希望得到一个类型为List[Int]的结果。

可能逻辑有点奇怪复杂并且莫名其妙,那是因为这个例子是我自己想的哈哈。其实我只是想说当出现flatMap或map层层嵌套的时候,代码看起来就很复杂,可读性断崖式下跌,反正我瞟一眼就不想仔细看里面的逻辑了,那咋办呢?

我们可以利用scala提供的一个语法糖for来让代码更简洁易懂,用for重构之后的代码:

for{
    r1 <- input
    r2 <- if(r1.getOrElse(List()).size > 1) r1.get else List()
    r3 <- if(r2.getOrElse(0) > 10) r2 else None
} yield r3

这里的input就是上面提到的那个复杂的类型,然后在for里,用<-来解一次之后,r1的类型就是Option[List[Option[Int]]],然后我们用刚才的condition来对r1进行判断,并且也解一次,得到了类型为Option[Int]的r2(r1.get的类型为List[Option[Int]],被<-解开之后得到了类型为Option[Int]的r2)。再用刚才的第二个条件来操作,得到了类型为Int的r3。

这样写就简洁很多,把两个condition这样列出来明显增加了可读性。
IO的flatMap也可以用for来重构,并且这是我们经常使用的方式。

一些补充:

  • 虽然我们yield了类型为Int的r3,但是最后得到的还是一个List[Int],原因是for其实就是map和flatMap的语法糖,所以List最后还会还是List,r3是最外层List的一个元素而已。
  • for的第一行一定要用<-运算符,因为每个使用了<-的语句是一个generator,for-comprehension一定要需要以一个generator开始(具体原因会在后续关于for-comprehension的文章里探讨,这里不深究啦)

最后一句:有问题欢迎沟通交流或批评指正~

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

推荐阅读更多精彩内容