高階函數(shù)與 DRY

2018-02-24 16:00 更新

前幾章介紹了 Scala 容器類型的可組合性特征。接下來,你會發(fā)現(xiàn),Scala 中的一等公民——函數(shù)也具有這一性質(zhì)。

組合性產(chǎn)生可重用性,雖然后者是經(jīng)由面向?qū)ο缶幊潭鵀槿耸熘?,但它也絕對是純函數(shù)的固有性質(zhì)。(純函數(shù)是指那些沒有副作用且是引用透明的函數(shù))

一個明顯的例子是調(diào)用已知函數(shù)實(shí)現(xiàn)一個新的函數(shù),當(dāng)然,還有其他的方式來重用已知函數(shù)。這一章會討論函數(shù)式編程的一些基本原理。你將會學(xué)到如何使用高階函數(shù),以及重用已有代碼時(shí),遵守 DRY 原則。

高階函數(shù)

和一階函數(shù)相比,高階函數(shù)可以有三種形式:

  1. 一個或多個參數(shù)是函數(shù),并返回一個值。
  2. 返回一個函數(shù),但沒有參數(shù)是函數(shù)。
  3. 上述兩者疊加:一個或多個參數(shù)是函數(shù),并返回一個函數(shù)。

看到這里的讀者應(yīng)該已經(jīng)見到過第一種使用:我們調(diào)用一個方法,像 map 、 filter 、 flatMap ,并傳遞另一個函數(shù)給它。傳遞給方法的函數(shù)通常是匿名函數(shù),有時(shí)候,還涉及一些代碼冗余。

這一章只關(guān)注另外兩種功能:一個可以根據(jù)輸入值構(gòu)建新的函數(shù),另一個可以根據(jù)現(xiàn)有的函數(shù)組合出新的函數(shù)。這兩種情況都能夠消除代碼冗余。

函數(shù)生成

你可能認(rèn)為依據(jù)輸入值創(chuàng)建新函數(shù)的能力并不是那么有用。函數(shù)組合非常重要,但在這之前,還是先來看看如何使用可以產(chǎn)生新函數(shù)的函數(shù)。

假設(shè)要實(shí)現(xiàn)一個免費(fèi)的郵件服務(wù),用戶可以設(shè)置對郵件的屏蔽。我們用一個簡單的樣例類來代表郵件:

case class Email(
  subject: String,
  text: String,
  sender: String,
  recipient: String
)

想讓用戶可以自定義過濾條件,需有一個過濾函數(shù)——類型為 Email => Boolean 的謂詞函數(shù),這個謂詞函數(shù)決定某個郵件是否該被屏蔽:如果謂詞成真,那這個郵件被接受,否則就被屏蔽掉。

type EmailFilter = Email => Boolean
def newMailsForUser(mails: Seq[Email], f: EmailFilter) = mails.filter(f)

注意,類型別名使得代碼看起來更有意義。

現(xiàn)在,為了使用戶能夠配置郵件過濾器,實(shí)現(xiàn)了一些可以產(chǎn)生 EmailFilter 的工廠方法:

  val sentByOneOf: Set[String] => EmailFilter =
    senders =>
      email => senders.contains(email.sender)
  val notSentByAnyOf: Set[String] => EmailFilter =
    senders =>
      email => !senders.contains(email.sender)
  val minimumSize: Int => EmailFilter =
    n =>
      email => email.text.size >= n
  val maximumSize: Int => EmailFilter =
    n =>
      email => email.text.size <= n

這四個 vals 都是可以返回 EmailFilter 的函數(shù),前兩個接受代表發(fā)送者的 Set[String] 作為輸入,后兩個接受代表郵件內(nèi)容長度的 Int 作為輸入。

可以使用這些函數(shù)來創(chuàng)建 EmialFilter

  val emailFilter: EmailFilter = notSentByAnyOf(Set("johndoe@example.com"))
  val mails = Email(
    subject = "It's me again, your stalker friend!",
    text = "Hello my friend! How are you?",
    sender = "johndoe@example.com",
    recipient = "me@example.com") :: Nil
  newMailsForUser(mails, emailFilter) // returns an empty list

這個過濾器過濾掉列表里唯一的一個元素,因?yàn)橛脩羝帘瘟藖碜?johndoe@example.com 的郵件。可以用工廠方法創(chuàng)建任意的 EmailFilter 函數(shù),這取決于用戶的需求了。

重用已有函數(shù)

當(dāng)前的解決方案有兩個問題。第一個是工廠方法中有重復(fù)代碼。上文提到過,函數(shù)的組合特征可以很輕易的保持 DRY 原則,既然如此,那就試著使用它吧!

對于 minimumSizemaximumSize ,我們引入一個叫做 sizeConstraint 的函數(shù)。這個函數(shù)接受一個謂詞函數(shù),該謂詞函數(shù)檢查函數(shù)內(nèi)容長度是否OK,郵件長度會通過參數(shù)傳遞給它:

  type SizeChecker = Int => Boolean
  val sizeConstraint: SizeChecker => EmailFilter =
    f =>
      email => f(email.text.size)

這樣,我們就可以用 sizeConstraint 來表示 minimumSizemaximumSize 了:

  val minimumSize: Int => EmailFilter =
    n =>
      sizeConstraint(_ >= n)
  val maximumSize: Int => EmailFilter =
    n =>
      sizeConstraint(_ <= n)

函數(shù)組合

為另外兩個謂詞(sentByOneOf、 notSentByAnyOf)介紹一個通用的高階函數(shù),通過它,可以用一個函數(shù)去表達(dá)另外一個函數(shù)。

這個高階函數(shù)就是 complement ,給定一個類型為 A => Boolean 的謂詞,它返回一個新函數(shù),這個新函數(shù)總是得出和謂詞相對立的結(jié)果:

  def complement[A](predicate: A => Boolean) = (a: A) => !predicate(a)

現(xiàn)在,對于一個已有的謂詞 p ,調(diào)用 complement(p) 可以得到它的補(bǔ)。然而, sentByAnyOf 并不是一個謂詞函數(shù),它返回類型為 EmailFilter 的謂詞。

Scala 函數(shù)的可組合能力現(xiàn)在就用的上了:給定兩個函數(shù) f 、 g , f.compose(g) 返回一個新函數(shù),調(diào)用這個新函數(shù)時(shí),會首先調(diào)用 g ,然后應(yīng)用 fg 的返回結(jié)果上。類似的, f.andThen(g) 返回的新函數(shù)會應(yīng)用 gf 的返回結(jié)果上。

知道了這些,我們就可以重寫 notSentByAnyOf 了:

  val notSentByAnyOf = sentByOneOf andThen (g => complement(g))

上面的代碼創(chuàng)建了一個新的函數(shù),這個函數(shù)首先應(yīng)用 sentByOneOf 到參數(shù) Set[String] 上,產(chǎn)生一個 EmailFilter 謂詞,然后,應(yīng)用 complement 到這個謂詞上。使用 Scala 的下劃線語法,這短代碼還能更精簡:

  val notSentByAnyOf = sentByOneOf andThen (complement(_))

讀者可能已經(jīng)注意到,給定 complement 函數(shù),也可以通過 minimumSize 來實(shí)現(xiàn) maximumSize 。不過,先前的實(shí)現(xiàn)方式更加靈活,它允許檢查郵件內(nèi)容的任意長度。謂

謂詞組合

郵件過濾器的第二個問題是,當(dāng)前只能傳遞一個 EmailFilternewMailsForUser 函數(shù),而用戶必然想設(shè)置多個標(biāo)準(zhǔn)。所以需要可以一種可以創(chuàng)建組合謂詞的方法,這個組合謂詞可以在任意一個標(biāo)準(zhǔn)滿足的情況下返回 true ,或者在都不滿足時(shí)返回 false

下面的代碼是一種實(shí)現(xiàn)方式:

  def any[A](predicates: (A => Boolean)*): A => Boolean =
    a => predicates.exists(pred => pred(a))
  def none[A](predicates: (A => Boolean)*) = complement(any(predicates: _*))
  def every[A](predicates: (A => Boolean)*) = none(predicates.view.map(complement(_)): _*)

any 函數(shù)返回的新函數(shù)會檢查是否有一個謂詞對于輸入 a 成真。none 返回的是 any 返回函數(shù)的補(bǔ),只要存在一個成真的謂詞, none 的條件就無法滿足。最后, every 利用 noneany 來判定是否每個謂詞的補(bǔ)對于輸入 a 都不成真。

可以使用它們來創(chuàng)建代表用戶設(shè)置的組合 EmialFilter

  val filter: EmailFilter = every(
      notSentByAnyOf(Set("johndoe@example.com")),
      minimumSize(100),
      maximumSize(10000)
    )

流水線組合

再舉一個函數(shù)組合的例子?;仡櫹律厦娴膱鼍埃]件提供者不僅想讓用戶可以配置郵件過濾器,還想對用戶發(fā)送的郵件做一些處理。這是一些簡單的 Emial => Email 函數(shù),一些可能的處理函數(shù)是:

  val addMissingSubject = (email: Email) =>
    if (email.subject.isEmpty) email.copy(subject = "No subject")
    else email
  val checkSpelling = (email: Email) =>
    email.copy(text = email.text.replaceAll("your", "you're"))
  val removeInappropriateLanguage = (email: Email) =>
    email.copy(text = email.text.replaceAll("dynamic typing", "**CENSORED**"))
  val addAdvertismentToFooter = (email: Email) =>
    email.copy(text = email.text + "\nThis mail sent via Super Awesome Free Mail")

現(xiàn)在,根據(jù)老板的心情,可以按需配置郵件處理的流水線。通過 andThen 調(diào)用實(shí)現(xiàn),或者使用 Function 伴生對象上的 chain 方法:

  val pipeline = Function.chain(Seq(
    addMissingSubject,
    checkSpelling,
    removeInappropriateLanguage,
    addAdvertismentToFooter))

高階函數(shù)與偏函數(shù)

這部分不會關(guān)注細(xì)節(jié),不過,在知道了這么多通過高階函數(shù)來組合和重用函數(shù)的方法之后,你可能想再重新看看偏函數(shù)。

鏈接偏函數(shù)

匿名函數(shù)那一章提到過,偏函數(shù)可以被用來創(chuàng)建責(zé)任鏈:PartialFunction 上的 orElse 方法允許鏈接任意個偏函數(shù),從而組合出一個新的偏函數(shù)。不過,只有在一個偏函數(shù)沒有為給定輸入定義的時(shí)候,才會把責(zé)任傳遞給下一個偏函數(shù)。從而可以做下面這樣的事情:

  val handler = fooHandler orElse barHandler orElse bazHandler

再看偏函數(shù)

有時(shí)候,偏函數(shù)并不合適。仔細(xì)想想,一個函數(shù)沒有為所有的輸入值定義操作,這樣的事實(shí)還可以用一個返回 Option[A] 的標(biāo)準(zhǔn)函數(shù)代替:如果函數(shù)為一個輸入定義了操作,那就返回 Some[A] ,否則返回 None

要這么做的話,可以在給定的偏函數(shù) pf 上調(diào)用 lift 方法得到一個普通的函數(shù),這個函數(shù)返回 Option 。反過來,如果有一個返回 Option 的普通函數(shù) f ,也可以調(diào)用 Function.unlift(f) 來得到一個偏函數(shù)???/p>

總結(jié)

這一章給出了高階函數(shù)的使用,利用它可以在一個新的環(huán)境里重用已有函數(shù),并用靈活的方式去組合它們。在所舉的例子中,就代碼行數(shù)而言,可能看不出太多價(jià)值,這些例子都很簡單,只是為了說明而已,在架構(gòu)層面,組合和重用函數(shù)是有很大幫助的。

下一章,我們繼續(xù)探索函數(shù)組合的方式:函數(shù)部分應(yīng)用和柯里化(Partial Function Application and Currying)

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號