1. 程式人生 > >【函式式】Monads模式初探——for解析式

【函式式】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操縱的計算、回溯計算以及交易等,不一而足。