99re热这里只有精品视频,7777色鬼xxxx欧美色妇,国产成人精品一区二三区在线观看,内射爽无广熟女亚洲,精品人妻av一区二区三区

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

2018-02-24 16:00 更新

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

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

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

為了具體說明這一概念,回到上一章的例子:假想的免費(fèi)郵件服務(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)建了兩個(gè)這樣的工廠方法,它們檢查郵件內(nèi)容長(zhǎng)度是否滿足給定的最大值或最小值。這一次,我們使用部分應(yīng)用函數(shù)來實(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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

更有趣的函數(shù)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

函數(shù)柯里化

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

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

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

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

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

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

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

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

下面這個(gè)簡(jiǎn)化的例子說明了這項(xiàng)技術(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]
    }

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

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

擴(kuò)展 MailboxService 時(shí),實(shí)現(xiàn) newMails 的方法就是應(yīng)用 getNewMails 這個(gè)方法,把依賴 EmailRepositoryFilterRepository 的具體實(shí)現(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") 無需指定要使用的存儲(chǔ)庫。在實(shí)際的應(yīng)用程序中,這個(gè)服務(wù)也可能是以依賴的方式被使用,而不是直接引用。

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

總結(jié)

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

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)