前幾章介紹了 Scala 容器類型的可組合性特征。接下來,你會發現,Scala 中的一等公民——函數也具有這一性質。
組合性產生可重用性,雖然后者是經由面向對象編程而為人熟知,但它也絕對是純函數的固有性質。(純函數是指那些沒有副作用且是引用透明的函數)
一個明顯的例子是調用已知函數實現一個新的函數,當然,還有其他的方式來重用已知函數。這一章會討論函數式編程的一些基本原理。你將會學到如何使用高階函數,以及重用已有代碼時,遵守 [DRY](http://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 原則。
### 高階函數
和一階函數相比,高階函數可以有三種形式:
1. 一個或多個參數是函數,并返回一個值。
1. 返回一個函數,但沒有參數是函數。
1. 上述兩者疊加:一個或多個參數是函數,并返回一個函數。
看到這里的讀者應該已經見到過第一種使用:我們調用一個方法,像 `map` 、 `filter` 、 `flatMap` ,并傳遞另一個函數給它。傳遞給方法的函數通常是匿名函數,有時候,還涉及一些代碼冗余。
這一章只關注另外兩種功能:一個可以根據輸入值構建新的函數,另一個可以根據現有的函數組合出新的函數。這兩種情況都能夠消除代碼冗余。
### 函數生成
你可能認為依據輸入值創建新函數的能力并不是那么有用。函數組合非常重要,但在這之前,還是先來看看如何使用可以產生新函數的函數。
假設要實現一個免費的郵件服務,用戶可以設置對郵件的屏蔽。我們用一個簡單的樣例類來代表郵件:
~~~
case class Email(
subject: String,
text: String,
sender: String,
recipient: String
)
~~~
想讓用戶可以自定義過濾條件,需有一個過濾函數——類型為 `Email => Boolean` 的謂詞函數,這個謂詞函數決定某個郵件是否該被屏蔽:如果謂詞成真,那這個郵件被接受,否則就被屏蔽掉。
~~~
type EmailFilter = Email => Boolean
def newMailsForUser(mails: Seq[Email], f: EmailFilter) = mails.filter(f)
~~~
注意,類型別名使得代碼看起來更有意義。
現在,為了使用戶能夠配置郵件過濾器,實現了一些可以產生 `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` 的函數,前兩個接受代表發送者的 `Set[String]` 作為輸入,后兩個接受代表郵件內容長度的 `Int` 作為輸入。
可以使用這些函數來創建 `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
~~~
這個過濾器過濾掉列表里唯一的一個元素,因為用戶屏蔽了來自 `johndoe@example.com` 的郵件。可以用工廠方法創建任意的 `EmailFilter` 函數,這取決于用戶的需求了。
### 重用已有函數
當前的解決方案有兩個問題。第一個是工廠方法中有重復代碼。上文提到過,函數的組合特征可以很輕易的保持 DRY 原則,既然如此,那就試著使用它吧!
對于 `minimumSize` 和 `maximumSize` ,我們引入一個叫做 `sizeConstraint` 的函數。這個函數接受一個謂詞函數,該謂詞函數檢查函數內容長度是否OK,郵件長度會通過參數傳遞給它:
~~~
type SizeChecker = Int => Boolean
val sizeConstraint: SizeChecker => EmailFilter =
f =>
email => f(email.text.size)
~~~
這樣,我們就可以用 `sizeConstraint` 來表示 `minimumSize` 和 `maximumSize` 了:
~~~
val minimumSize: Int => EmailFilter =
n =>
sizeConstraint(_ >= n)
val maximumSize: Int => EmailFilter =
n =>
sizeConstraint(_ <= n)
~~~
### 函數組合
為另外兩個謂詞(`sentByOneOf`、 `notSentByAnyOf`)介紹一個通用的高階函數,通過它,可以用一個函數去表達另外一個函數。
這個高階函數就是 `complement` ,給定一個類型為 `A => Boolean` 的謂詞,它返回一個新函數,這個新函數總是得出和謂詞相對立的結果:
~~~
def complement[A](predicate: A => Boolean) = (a: A) => !predicate(a)
~~~
現在,對于一個已有的謂詞 `p` ,調用 `complement(p)` 可以得到它的補。然而, `sentByAnyOf` 并不是一個謂詞函數,它返回類型為 `EmailFilter` 的謂詞。
Scala 函數的可組合能力現在就用的上了:給定兩個函數 `f` 、 `g` , `f.compose(g)` 返回一個新函數,調用這個新函數時,會首先調用 `g` ,然后應用 `f` 到 `g` 的返回結果上。類似的, `f.andThen(g)` 返回的新函數會應用 `g` 到 `f` 的返回結果上。
知道了這些,我們就可以重寫 `notSentByAnyOf` 了:
~~~
val notSentByAnyOf = sentByOneOf andThen (g => complement(g))
~~~
上面的代碼創建了一個新的函數,這個函數首先應用 `sentByOneOf` 到參數 `Set[String]` 上,產生一個 `EmailFilter` 謂詞,然后,應用 `complement` 到這個謂詞上。使用 Scala 的下劃線語法,這短代碼還能更精簡:
~~~
val notSentByAnyOf = sentByOneOf andThen (complement(_))
~~~
讀者可能已經注意到,給定 `complement` 函數,也可以通過 `minimumSize` 來實現 `maximumSize` 。不過,先前的實現方式更加靈活,它允許檢查郵件內容的任意長度。謂
#### 謂詞組合
郵件過濾器的第二個問題是,當前只能傳遞一個 `EmailFilter` 給 `newMailsForUser` 函數,而用戶必然想設置多個標準。所以需要可以一種可以創建組合謂詞的方法,這個組合謂詞可以在任意一個標準滿足的情況下返回 `true` ,或者在都不滿足時返回 `false` 。
下面的代碼是一種實現方式:
~~~
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` 函數返回的新函數會檢查是否有一個謂詞對于輸入 `a` 成真。`none` 返回的是 `any` 返回函數的補,只要存在一個成真的謂詞, `none` 的條件就無法滿足。最后, `every` 利用 `none` 和 `any` 來判定是否每個謂詞的補對于輸入 `a` 都不成真。
可以使用它們來創建代表用戶設置的組合 `EmialFilter` :
~~~
val filter: EmailFilter = every(
notSentByAnyOf(Set("johndoe@example.com")),
minimumSize(100),
maximumSize(10000)
)
~~~
#### 流水線組合
再舉一個函數組合的例子。回顧下上面的場景,郵件提供者不僅想讓用戶可以配置郵件過濾器,還想對用戶發送的郵件做一些處理。這是一些簡單的 `Emial => Email` 函數,一些可能的處理函數是:
~~~
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")
~~~
現在,根據老板的心情,可以按需配置郵件處理的流水線。通過 `andThen` 調用實現,或者使用 Function 伴生對象上的 `chain` 方法:
~~~
val pipeline = Function.chain(Seq(
addMissingSubject,
checkSpelling,
removeInappropriateLanguage,
addAdvertismentToFooter))
~~~
### 高階函數與偏函數
這部分不會關注細節,不過,在知道了這么多通過高階函數來組合和重用函數的方法之后,你可能想再重新看看偏函數。
#### 鏈接偏函數
匿名函數那一章提到過,偏函數可以被用來創建責任鏈:`PartialFunction` 上的 `orElse` 方法允許鏈接任意個偏函數,從而組合出一個新的偏函數。不過,只有在一個偏函數沒有為給定輸入定義的時候,才會把責任傳遞給下一個偏函數。從而可以做下面這樣的事情:
~~~
val handler = fooHandler orElse barHandler orElse bazHandler
~~~
#### 再看偏函數
有時候,偏函數并不合適。仔細想想,一個函數沒有為所有的輸入值定義操作,這樣的事實還可以用一個返回 `Option[A]` 的標準函數代替:如果函數為一個輸入定義了操作,那就返回 `Some[A]` ,否則返回 `None` 。
要這么做的話,可以在給定的偏函數 `pf` 上調用 `lift` 方法得到一個普通的函數,這個函數返回 `Option` 。反過來,如果有一個返回 `Option` 的普通函數 `f` ,也可以調用 `Function.unlift(f)` 來得到一個偏函數。總
### 總結
這一章給出了高階函數的使用,利用它可以在一個新的環境里重用已有函數,并用靈活的方式去組合它們。在所舉的例子中,就代碼行數而言,可能看不出太多價值,這些例子都很簡單,只是為了說明而已,在架構層面,組合和重用函數是有很大幫助的。
下一章,我們繼續探索函數組合的方式:_函數部分應用和柯里化(Partial Function Application and Currying)_。