剖析for推導式

  1. 元素
  2. 意義
  3. 轉譯
  4. 應用

元素

Scala里碴里,一個for表達式可以包含1個或多個「生成器」(Generator)哀托,及其一個可選的yield子句缺脉;其中痪欲,每個生成器可以包含0個或多個「守衛(wèi)」(Guard),及其0個或多個「定義」(Definition)攻礼。

領域模型

也就是說业踢,for表達式可以描述為如下圖所示的模型。

for表達式的模型

基本形式

依據yield是否存在礁扮,for表達式可以歸結為兩種基本形式知举。

  • for推導式(for comprehension)
  • for循環(huán)(for loop)
for推導式

如果for表達式存在yield子句,則稱該for表達式為for推導式太伊。它最終將使用yield子句收集結果雇锡,并將其作為for表達式的值。

for循環(huán)

如果for表達式不存在yield子句僚焦,則稱該for表達式為for循環(huán)锰提。for循環(huán)用于執(zhí)行某種副作用,其返回值的類型為Unit芳悲。

意義

for表達式是一種重要的編程工具立肘,在某些場景下能夠極大地改善代碼的表達力。

需求:查詢所有產自中國名扛,且價格在10元以內的咖啡名稱赛不,及其對應的供應商。

使用高階函數

coffees filter { c =>
  c.nationality == "China" && c.price < 10 
} flatMap { c =>
  suppliers filter {
    c.supplierId == _.id 
  } map { 
    c.name -> _.name 
  }
}

使用for推導式

如果使用for推導式罢洲,可以進一步改善代碼的表達力踢故。

for {
  c <- coffees if c.nationality == "China" && c.price < 10
  s <- suppliers if s.id == c.supplierId
} yield c.name -> s.name

轉譯

事實上文黎,對于任意的for推導式,都可以使用map, flatMap, filter進行表達殿较;而對于任意的for循環(huán)耸峭,都可以使用foreach, filter進行表達。

轉譯生成器

一般地淋纲,for表達式可以包含1個或多個「生成器」(Generator)劳闹。

for (x1 <- exp1; x2 <- exp2; ...; xn <- expn) yield expr
n == 1
for (x1 <- exp1) yield expr

轉譯為

exp1 map { x => expr } 
n >= 2
for (x1 <- exp1; x2 <- exp2; ...; xn <- expn) yield expr

首先,對生成器x1 <- exp1進行轉譯:

exp1 flatMap { x1 => 
  for (x2 <- exp2; ...; xn <- expn) yield expr 
}

依次類推洽瞬,分別對x2 <- exp2; x3 <- exp3, ..., x(n_1) <- exp(n_1)進行轉譯:

exp1 flatMap { x1 =>
  exp2 flatMap { x2 =>
    ...
    exp(n-1) flatMap { x(n-1) => 
      for(xn <- expn) yield expr 
    }
  }
}

最后本涕,對于最后一個生成器xn <- expn,則使用map進行轉譯伙窃。最終for表達式轉譯為:

exp1 flatMap { x1 =>
  exp2 flatMap { x2 =>
    ...
    exp(n-1) flatMap { x(n-1) => 
      expn map { xn => expr }
    }
  }
}

轉譯守衛(wèi)

一般地菩颖,一個生成器可以附加0個或多個守衛(wèi)。

for (x <- exp if guard1 if guard2 ... if guardn) yield expr

首先为障,在轉譯x <- exp之前晦闰,首先將guard1實施于exp,將其迭代范圍縮小鳍怨。

for { 
  x <- (exp filter guard1) 
  if guard2 
  ... 
  if guardn
} yield expr

以此類推呻右,將guard2, ..., guardn依次實施于上一迭代的表達式,將其迭代范圍逐漸縮小鞋喇,最終for推導式轉譯為:

for { 
  x <- (exp filter guard1 filter guard2 ... filter guardn) 
} yield expr

合并守衛(wèi)

但是声滥,每次調用一次filter都將產生一個中間結果,效率非常低下侦香⌒汛可以使用withFilter對所有的的守衛(wèi)進行合并。

for { 
  x <- (exp withFilter guard1 withFilter guard2 ... withFilter guardn) 
} yield expr

它等價于:

for { 
  x <- exp.withFilter(guard1 && guard2 && ... && guardn) 
} yield expr

使用withFilter鄙皇,雖然它也會生成中間人。但是仰挣,其相對于filter生成昂貴的中間集合伴逸,withFilter僅僅生成WithFilter的中間實例,其成本非常低廉膘壶。

轉譯定義

一般地错蝴,一個生成器可以附加0個或多個值定義。

for (x <- exp; y1 = exp1; y2 = exp2; ...; yn = expn) yield expr

首先颓芭,對y1 = exp1進行轉譯:

for {
  (x, y1) <- for (x <- exp) yield (x, exp1)
  y2 = exp2
  ...
  yn = expn
} yield expr

依次類推顷锰,將y2 = exp2, ..., yn = expn依次實施于上一迭代的表達式,最終for推導式轉譯為:

for {
  (x, y1, y2, ..., yn) <- for (x <- exp) yield (x, exp1, exp2, ..., expn)
} yield expr

轉譯for循環(huán)

一般地亡问,當不存在yield子句時官紫,轉譯使用map, flatMap進行轉譯肛宋;但是,不存在yield子句時束世,則使用foreach進行轉譯酝陈,相對簡單。

for (x1 <- exp1; x2 <- exp2; ..., xn <- expn) expr

首先毁涉,對生成器x1 <- exp1進行轉譯:

exp1 foreach { x1 => 
  for (x2 <- exp2; ...; xn <- expn) expr 
}

依次類推沉帮,分別對x2 <- exp2; x3 <- exp3, ..., xn <- expn進行轉譯:

exp1 foreach { x1 =>
  exp2 foreach { x2 =>
    ...
    expn foreach { xn => 
      expr
    }
  }
}

轉譯模式

事實上,生成器是一個模式匹配的特殊應用贫堰。當對for表達式進行轉譯時穆壕,其對應的模式匹配規(guī)則將被轉譯為等價的偏函數,并應用于map, flatMapforeach方法之上其屏。

匹配元組

當生成器的模式是一個元組或者是一個變量(可以看成單元素的元組)時喇勋。

for { (x1, x2, ..., xn) <- exp } yield expr

經過轉譯,其模式匹配規(guī)則將被轉譯為等價的偏函數漫玄,它自動過濾了不匹配的元組茄蚯。

exp map { case { (x1, x2, ..., xn) => expr }
一般模式

一般地,如果生成器中的模式不是元組或者變量時睦优,它是一個一般的模式渗常。

for { pat <- exp } yield expr

首先,模式匹配規(guī)則將被轉譯為等價的偏函數汗盘,并進行一次withFilter操作皱碘,以便過濾掉不匹配的元素;然后隐孽,再將該模式匹配轉譯為等價的偏函數癌椿。

exp withFilter {
  case pat => true
  case _   => false
} map {
  case pat => expr
}

泛化

事實上,for表達式是對容器C[A]操作的語法糖表示菱阵。其中踢俄,C[A]是一個擁有foreach, map, flatMap, filter方法集合的容器。

trait C[A] {
  def map[B](f: A => B): C[B]
  def flatMap[B](f: A => C[B]): C[B]
  def filter(p: A => Boolean): C[A]
  def foreach(op: A => Unit): Unit
}

Scala的標準庫中晴及,滿足這種特征的容器有很多都办。例如,Array, Seq, Set, Map, Range, Iterator, Stream, Option等所有容器虑稼。

接下來將以Option為例琳钉,講解這幾個算子的實現方式,及其加深理解for表達式的工作機制蛛倦。

遞歸結構

sealed trait Option[+A] { self =>
  def isEmpty: Boolean
  def get: A
}

case class Some[+A](x: A) extends Option[A] {
  def isEmpty = false
  def get = x
}

case object None extends Option[Nothing] {
  def isEmpty = true
  def get = throw new NoSuchElementException("None.get")
}

實現算子

sealed trait Option[+A] {
  def isEmpty: Boolean
  def get: A
  
  def map[B](f: A => B): Option[B] =
    if (isEmpty) None else Some(f(get))
    
  def flatMap[B](f: A => Option[B]): Option[B] =
    if (isEmpty) None else f(get)
    
  def foreach[U](f: A => U): Unit = 
    if (!isEmpty) f(get)
    
  def filter(p: A => Boolean): Option[A] =
    if (isEmpty || p(get)) this else None
}

提升效率

使用filter將每次返回一個新的Option實例歌懒,成本相對昂貴。因此溯壶,可以使用withFilter替代filter及皂,這樣的設計實現如下三個效果甫男。

  • 合并for表達式中的所有過濾條件;
  • 保證map, flatMap, foreach操作發(fā)生在過濾條件之后躲庄,以便提高效率查剖;
  • 操作withFilter,可以避免生成昂貴的噪窘,類型為Option的中間實例笋庄;而以相對廉價的,類型為WithFilter的中間實例代替倔监。
sealed trait Option[+A] { self => 
  def withFilter(p: A => Boolean): WithFilter = new WithFilter(p)
  
  class WithFilter(p: A => Boolean) {
    def map[B](f: A => B): Option[B] = 
      self filter p map f
    
    def flatMap[B](f: A => Option[B]): Option[B] = 
      self filter p flatMap f
    
    def foreach[U](f: A => U): Unit = 
      self filter p foreach f
    
    def withFilter(q: A => Boolean): WithFilter = 
      new WithFilter(x => p(x) && q(x))
  }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末直砂,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子浩习,更是在濱河造成了極大的恐慌静暂,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谱秽,死亡現場離奇詭異洽蛀,居然都是意外死亡,警方通過查閱死者的電腦和手機疟赊,發(fā)現死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門郊供,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人近哟,你說我怎么就攤上這事驮审。” “怎么了吉执?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵疯淫,是天一觀的道長。 經常有香客問我戳玫,道長熙掺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任咕宿,我火速辦了婚禮币绩,結果婚禮上,老公的妹妹穿的比我還像新娘荠列。我一直安慰自己,他們只是感情好载城,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布肌似。 她就那樣靜靜地躺著,像睡著了一般诉瓦。 火紅的嫁衣襯著肌膚如雪川队。 梳的紋絲不亂的頭發(fā)上力细,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音固额,去河邊找鬼眠蚂。 笑死,一個胖子當著我的面吹牛斗躏,可吹牛的內容都是我干的逝慧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼啄糙,長吁一口氣:“原來是場噩夢啊……” “哼笛臣!你這毒婦竟也來了?” 一聲冷哼從身側響起隧饼,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤沈堡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后燕雁,有當地人在樹林里發(fā)現了一具尸體诞丽,經...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年拐格,在試婚紗的時候發(fā)現自己被綠了僧免。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡禁荒,死狀恐怖猬膨,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情呛伴,我是刑警寧澤勃痴,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站热康,受9級特大地震影響沛申,放射性物質發(fā)生泄漏。R本人自食惡果不足惜姐军,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一铁材、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧奕锌,春花似錦著觉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至辽话,卻和暖如春肄鸽,著一層夾襖步出監(jiān)牢的瞬間卫病,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工典徘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蟀苛,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓逮诲,卻偏偏與公主長得像帜平,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子汛骂,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容