用scala有段時間了,這篇文章是想總結一下map和flapMap的原理和用法
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進行遍歷,當數(shù)值小于20,就返回Some(value),否則返回None。
即我們通過這個map把List里的元素根據(jù)一定的邏輯轉換成了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里的元素然后根據(jù)傳入的參數(shù)進行轉換,源碼:
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的區(qū)別是,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]的結果。
可能邏輯有點奇怪復雜并且莫名其妙,那是因為這個例子是我自己想的哈哈。其實我只是想說當出現(xiàn)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來重構,并且這是我們經(jīng)常使用的方式。
一些補充:
- 雖然我們yield了類型為Int的r3,但是最后得到的還是一個
List[Int],原因是for其實就是map和flatMap的語法糖,所以List最后還會還是List,r3是最外層List的一個元素而已。 - for的第一行一定要用
<-運算符,因為每個使用了<-的語句是一個generator,for-comprehension一定要需要以一個generator開始(具體原因會在后續(xù)關于for-comprehension的文章里探討,這里不深究啦)
最后一句:有問題歡迎溝通交流或批評指正~