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的文章里探讨,这里不深究啦)

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

你可能感兴趣的:(map & flatMap in scala)