前幾章介紹了 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ù)可以有三種形式:
看到這里的讀者應(yīng)該已經(jīng)見到過第一種使用:我們調(diào)用一個方法,像 map
、 filter
、 flatMap
,并傳遞另一個函數(shù)給它。傳遞給方法的函數(shù)通常是匿名函數(shù),有時(shí)候,還涉及一些代碼冗余。
這一章只關(guān)注另外兩種功能:一個可以根據(jù)輸入值構(gòu)建新的函數(shù),另一個可以根據(jù)現(xiàn)有的函數(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ù),這取決于用戶的需求了。
當(dāng)前的解決方案有兩個問題。第一個是工廠方法中有重復(fù)代碼。上文提到過,函數(shù)的組合特征可以很輕易的保持 DRY 原則,既然如此,那就試著使用它吧!
對于 minimumSize
和 maximumSize
,我們引入一個叫做 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
來表示 minimumSize
和 maximumSize
了:
val minimumSize: Int => EmailFilter =
n =>
sizeConstraint(_ >= n)
val maximumSize: Int => EmailFilter =
n =>
sizeConstraint(_ <= n)
為另外兩個謂詞(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)用 f
到 g
的返回結(jié)果上。類似的, f.andThen(g)
返回的新函數(shù)會應(yīng)用 g
到 f
的返回結(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)前只能傳遞一個 EmailFilter
給 newMailsForUser
函數(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
利用 none
和 any
來判定是否每個謂詞的補(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))
這部分不會關(guān)注細(xì)節(jié),不過,在知道了這么多通過高階函數(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ù)并不合適。仔細(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>
這一章給出了高階函數(shù)的使用,利用它可以在一個新的環(huán)境里重用已有函數(shù),并用靈活的方式去組合它們。在所舉的例子中,就代碼行數(shù)而言,可能看不出太多價(jià)值,這些例子都很簡單,只是為了說明而已,在架構(gòu)層面,組合和重用函數(shù)是有很大幫助的。
下一章,我們繼續(xù)探索函數(shù)組合的方式:函數(shù)部分應(yīng)用和柯里化(Partial Function Application and Currying)。
更多建議: