鍍金池/ 教程/ Scala/ 柯里化和部分函數(shù)應(yīng)用
高階函數(shù)與 DRY
序列提取
譯者結(jié)語
類型 Future
類型 Option
Scala 初學(xué)指南
類型類
模式匹配與匿名函數(shù)
路徑依賴類型
提取器
類型 Either
Try 與錯誤處理
介紹
實戰(zhàn)中的 Promise 和 Future
柯里化和部分函數(shù)應(yīng)用
無處不在的模式

柯里化和部分函數(shù)應(yīng)用

上一章重點在于代碼重復(fù):提升現(xiàn)有的函數(shù)功能、或者將函數(shù)進行組合。 這一章,我們來看看另外兩種函數(shù)重用的機制:函數(shù)的部分應(yīng)用(Partial Application of Functions) 、 柯里化(Currying) 。

部分應(yīng)用的函數(shù)

和其他遵循函數(shù)式編程范式的語言一樣,Scala 允許部分應(yīng)用一個函數(shù)。 調(diào)用一個函數(shù)時,不是把函數(shù)需要的所有參數(shù)都傳遞給它,而是僅僅傳遞一部分,其他參數(shù)留空; 這樣會生成一個新的函數(shù),其參數(shù)列表由那些被留空的參數(shù)組成。(不要把這個概念和偏函數(shù)混淆)

為了具體說明這一概念,回到上一章的例子: 假想的免費郵件服務(wù),能夠讓用戶配置篩選器,以使得滿足特定條件的郵件顯示在收件箱里,其他的被過濾掉。

Email 類看起來仍然是這樣:

case class Email(
  subject: String,
  text: String,
  sender: String,
  recipient: String)
type EmailFilter = Email => Boolean

過濾郵件的條件用謂詞 Email => Boolean 表示, EmailFilter 是其別名。 調(diào)用適當(dāng)?shù)墓S方法可以生成這些謂詞。

上一章,我們創(chuàng)建了兩個這樣的工廠方法,它們檢查郵件內(nèi)容長度是否滿足給定的最大值或最小值。 這一次,我們使用部分應(yīng)用函數(shù)來實現(xiàn)這些工廠方法,做法是,修改 sizeConstraint ,固定某些參數(shù)可以創(chuàng)建更具體的限制條件:

其修改后的代碼如下:

    type IntPairPred = (Int, Int) => Boolean
    def sizeConstraint(pred: IntPairPred, n: Int, email: Email) =
      pred(email.text.size, n)

上述代碼為一個謂詞函數(shù)定義了別名 IntPairPred ,該函數(shù)接受一對整數(shù)(值 n 和郵件內(nèi)容長度),檢查郵件長度對于 n 是否 OK。

請注意,不像上一章的 sizeConstraint ,這一個并不返回新的 EmailFilter,它只是簡單的用參數(shù)做計算,返回一個布爾值。 秘訣在于,你可以部分應(yīng)用這個 sizeConstraint 來得到一個 EmailFilter

遵循 DRY 原則,我們先來定義常用的 IntPairPred 實例,這樣,在調(diào)用 sizeConstraint 時,不需要重復(fù)的寫相同的匿名函數(shù),只需傳遞下面這些:

    val gt: IntPairPred = _ > _
    val ge: IntPairPred = _ >= _
    val lt: IntPairPred = _ < _
    val le: IntPairPred = _ <= _
    val eq: IntPairPred = _ == _

最后,調(diào)用 sizeConstraint 函數(shù),用上面的 IntPairPred 傳入第一個參數(shù):

    val minimumSize: (Int, Email) => Boolean = sizeConstraint(ge, _: Int, _: Email)
    val maximumSize: (Int, Email) => Boolean = sizeConstraint(le, _: Int, _: Email)

對所有沒有傳入值的參數(shù),必須使用占位符 _ ,還需要指定這些參數(shù)的類型,這使得函數(shù)的部分應(yīng)用多少有些繁瑣。 Scala 編譯器無法推斷它們的類型,方法重載使編譯器不可能知道你想使用哪個方法。

不過,你可以綁定或漏掉任意個、任意位置的參數(shù)。比如,我們可以漏掉第一個值,只傳遞約束值 n

    val constr20: (IntPairPred, Email) => Boolean =
      sizeConstraint(_: IntPairPred, 20, _: Email)

    val constr30: (IntPairPred, Email) => Boolean =
      sizeConstraint(_: IntPairPred, 30, _: Email)

得到的兩個函數(shù),接受一個 IntPairPred 和一個 Email 作為參數(shù), 然后利用謂詞函數(shù) IntPairPred 把郵件長度和 20 、 30 比較, 只不過比較方法的邏輯 IntPairPred 需要另外指定。

由此可見,雖然函數(shù)部分應(yīng)用看起來比較冗長,但它要比 Clojure 的靈活,在 Clojure 里,必須從左到右的傳遞參數(shù),不能略掉中間的任何參數(shù)。

從方法到函數(shù)對象

在一個方法上做部分應(yīng)用時,可以不綁定任何的參數(shù),這樣做的效果是產(chǎn)生一個函數(shù)對象,并且其參數(shù)列表和原方法一模一樣。 通過這種方式可以將方法變成一個可賦值、可傳遞的函數(shù)!

    val sizeConstraintFn: (IntPairPred, Int, Email) => Boolean = sizeConstraint _

更有趣的函數(shù)

部分函數(shù)應(yīng)用顯得太啰嗦,用起來不夠優(yōu)雅,幸好還有其他的替代方法。

也許你已經(jīng)知道 Scala 里的方法可以有多個參數(shù)列表。 下面的代碼用多個參數(shù)列表重新定義了 sizeConstraint

    def sizeConstraint(pred: IntPairPred)(n: Int)(email: Email): Boolean =
      pred(email.text.size, n)

如果把它變成一個可賦值、可傳遞的函數(shù)對象,它的簽名看起來會像是這樣:

    val sizeConstraintFn: IntPairPred => Int => Email => Boolean = sizeConstraint _

這種單參數(shù)的鏈?zhǔn)胶瘮?shù)稱做 柯里化函數(shù) ,以發(fā)明人 Haskell Curry 命名。在 Haskell 編程語言里,所有的函數(shù)默認都是柯里化的。

sizeConstraintFn 接受一個 IntPairPred ,返回一個函數(shù),這個函數(shù)又接受 Int 類型的參數(shù),返回另一個函數(shù),最終的這個函數(shù)接受一個 Email ,返回布爾值。

現(xiàn)在,可以把要傳入的 IntPairPred 傳遞給 sizeConstraint 得到:

    val minSize: Int => Email => Boolean = sizeConstraint(ge)
    val maxSize: Int => Email => Boolean = sizeConstraint(le)

被留空的參數(shù)沒必要使用占位符,因為這不是部分函數(shù)應(yīng)用。

現(xiàn)在,可以通過這兩個柯里化函數(shù)來創(chuàng)建 EmailFilter 謂詞:

    val min20: Email => Boolean = minSize(20)
    val max20: Email => Boolean = maxSize(20)

也可以在柯里化的函數(shù)上一次性綁定多個參數(shù),直接得到上面的結(jié)果。 傳入第一個參數(shù)得到的函數(shù)會立即應(yīng)用到第二個參數(shù)上:

    val min20: Email => Boolean = sizeConstraintFn(ge)(20)
    val max20: Email => Boolean = sizeConstraintFn(le)(20)

函數(shù)柯里化

有時候,并不總是能提前知道要不要將一個函數(shù)寫成柯里化形式,畢竟,和只有單參數(shù)列表的函數(shù)相比,柯里化函數(shù)的使用并不清晰。 而且,偶爾還會想以柯里化的形式去使用第三方的函數(shù),但這些函數(shù)的參數(shù)都在一個參數(shù)列表里。

這就需要一種方法能對函數(shù)進行柯里化。 這種的柯里化行為本質(zhì)上也是一個高階函數(shù):接受現(xiàn)有的函數(shù),返回新函數(shù)。 這個高階函數(shù)就是 curriedcurried 方法存在于 Function2 、 Function3 這樣的多參數(shù)函數(shù)類型里。 如果存在一個接受兩個參數(shù)的 sum ,可以通過調(diào)用 curried 方法得到它的柯里化版本:

    val sum: (Int, Int) => Int = _ + _
    val sumCurried: Int => Int => Int = sum.curried

使用 Funtion.uncurried 進行反向操作,可以將一個柯里化函數(shù)轉(zhuǎn)換成非柯里化版本。

函數(shù)化的依賴注入

在這一章的最后,我們來看看柯里化函數(shù)如何發(fā)揮其更大的作用。 來自 Java 或者 .NET 世界的人,或多或少都用過依賴注入容器,這些容器為使用者管理對象,以及對象之間的依賴關(guān)系。 在 Scala 里,你并不真的需要這樣的外部工具,語言已經(jīng)提供了許多功能,這些功能簡化了依賴注入的實現(xiàn)。

函數(shù)式編程仍然需要注入依賴:應(yīng)用程序中上層函數(shù)需要調(diào)用其他函數(shù)。 把要調(diào)用的函數(shù)硬編碼在上層函數(shù)里,不利于它們的獨立測試。 從而需要把被依賴的函數(shù)以參數(shù)的形式傳遞給上層函數(shù)。

但是,每次調(diào)用都傳遞相同的依賴,是不符合 DRY 原則的,這時候,柯里化函數(shù)就有用了! 柯里化和部分函數(shù)應(yīng)用是函數(shù)式編程里依賴注入的幾種方式之一。

下面這個簡化的例子說明了這項技術(shù):

    case class User(name: String)
    trait EmailRepository {
      def getMails(user: User, unread: Boolean): Seq[Email]
    }
    trait FilterRepository {
      def getEmailFilter(user: User): EmailFilter
    }
    trait MailboxService {
      def getNewMails(emailRepo: EmailRepository)(filterRepo: FilterRepository)(user: User) =
        emailRepo.getMails(user, true).filter(filterRepo.getEmailFilter(user))
      val newMails: User => Seq[Email]
    }

這個例子有一個依賴兩個不同存儲庫的服務(wù),這些依賴被聲明為 getNewMails 方法的參數(shù),并且每個依賴都在一個單獨的參數(shù)列表里。

MailboxService 實現(xiàn)了這個方法,留空了字段 newMails,這個字段的類型是一個函數(shù): User => Seq[Email],依賴于 MailboxService 的組件會調(diào)用這個函數(shù)。

擴展 MailboxService 時,實現(xiàn) newMails 的方法就是應(yīng)用 getNewMails 這個方法,把依賴 EmailRepository 、 FilterRepository 的具體實現(xiàn)傳遞給它:

    object MockEmailRepository extends EmailRepository {
      def getMails(user: User, unread: Boolean): Seq[Email] = Nil
    }
    object MockFilterRepository extends FilterRepository {
      def getEmailFilter(user: User): EmailFilter = _ => true
    }
    object MailboxServiceWithMockDeps extends MailboxService {
      val newMails: (User) => Seq[Email] =
        getNewMails(MockEmailRepository)(MockFilterRepository) _
    }

調(diào)用 MailboxServiceWithMockDeps.newMails(User("daniel") 無需指定要使用的存儲庫。 在實際的應(yīng)用程序中,這個服務(wù)也可能是以依賴的方式被使用,而不是直接引用。

這可能不是最強大、可擴展的依賴注入實現(xiàn)方式,但依舊是一個非常不錯的選擇,對展示部分函數(shù)應(yīng)用和柯里化更廣泛的功用來說,這也是一個不錯的例子。 如果你想知道更多關(guān)于這一點的知識,推薦看 Debasish Ghosh 的幻燈片 “Dependency Injection in Scala”。

總結(jié)

這一章討論了兩個附加的可以避免代碼重復(fù)的函數(shù)式編程技術(shù), 并且在這個基礎(chǔ)上,得到了很大的靈活性,可以用多種不同的形式重用函數(shù)。 部分函數(shù)應(yīng)用和柯里化,這兩者或多或少都可以實現(xiàn)同樣的效果,只是有時候,其中的某一個會更為優(yōu)雅。 下一章會繼續(xù)探討保持靈活性的方法:類型類(type class)。

上一篇:類型 Either下一篇:序列提取