歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> 函數式中的 currying

函數式中的 currying

日期:2017/3/1 9:08:00   编辑:Linux編程

currying 是函數式語言中經常遇到的一個概念,翻譯成 柯裡化,不是庫裡化。

currying 指的是將接收多個參數的函數變換成接收一個單一參數,並且返回接收余下的參數而且返回結果的新函數的技術。

說起來比較拗口,直接看下面的代碼。

    defadd(x: Int, y: Int): Int = x + y

    //call add
    add(1, 2)
    add(3, 4)

但是如果我們使用 currying 的寫法,那麼可以將兩個參數分開,接收第一個參數(x),然後返回一個函數,這個函數以第二個參數(y)為參數,返回結果:

    defadd(x:Int) => (y: Int) = x + y

然後我們就可以這樣調用 add 了。

    add(1)(2)
    add(3)(4)

scala 對於這種寫法提供了語法糖,add 的 currying 寫法我們可以更簡單地寫為:

    defadd(x: Int)(y: Int): Int = x + y

嘗試將一個函數(這裡指f)寫成 curry 方式:

    def curry[A, B, C](f: (A, B) => C): A => (B => C) = a => b => f(a, b)

這裡的 currying 函數,它接收兩個參數,分別為 A 類型和 B 類型,然後返回一個 C 類型的結果。那麼我們可以通過得到一個偏函數將其轉化為 curry 的方式,即 A => (B => C)。具體流程是,用第一個參數 a ,得到一個函數,這個函數使用 b 作為參數,然後得到結果。這裡中間的這個函數,是一個偏函數。
同樣我們也可以寫一個 uncurry 的方法(即將 currying 的函數f轉化成非 currying 的):

    def uncurry[A, B, C](f: A => B => C): (A, B) => C = 
        (a, b) => f(a)(b)      

這裡在 currying 的過程中,使用了偏函數(partial applied function),偏函數是什麼?這個其實更好理解一些,通俗的講,可以理解為定制化函數,比如我們有一個函數需要兩個參數,那麼如果我們返回固定其中一個參數的函數,那麼我們就得到了一個偏函數:

    defadd(x: Int, y: Int): Int = x + y
    defadd1(y: Int): Int = add(1, y)

這裡我們就是固定了 add 函數中的 x 參數,然後返回了另外一個函數 add1,現在 add1 只有一個參數,我們直接可以調用 add1:

    add1(5) //6

可以看到,currying 和偏函數的主要區別就是,是否固定了參數,偏函數一旦固定參數之後,就返回了新的函數。而 currying 一般並沒有固定參數,只是類似於使用了黑魔法,讓多個參數的函數,最後變成了一個函數鏈。

其實在 C 語言中也有偏函數這種用法:

int foo(int a, int b, int c) {
    return a + b + c;
}

int foo23(int a, int c) {
  return foo(a, 23, c);
}

但是在函數式語言中,偏函數以及 currying 能發揮更大的作用,原因是,函數式中,我們的參數還可以是函數。

defsum(f: Int => Int, a: Int, b: Int): Int = 
    if(a > b) 0
    else f(a) + sum(a+1, b)

這裡有一個 sum 函數,它將 a 和 b 之間的所有的數,用 f 計算之後相加,比如:

    sum(x => x * x, 1, 10)

那麼就是計算 1 到 10 之間所有數的平方和。但是如果我們要計算所有數的立方和呢,四次方呢?

固然我們可以將 f 傳遞成不同的值,但是我們也可以更簡單地直接得到定制化之後的函數,即用不同的 f 得到不同的函數。

為了達到我們的目的,我們將函數寫成 currying 形式:

defsum(f: Int => Int)(a: Int, b: Int): Int =
    if(a >b) 0 else f(a) + sum(f)(a+1, b)

這個時候 sum 函數的類型是什麼呢?是:(Int => Int) => (( Int, Int) => Int)。第一層函數是一個參數類型為 Int => Int 的函數,其返回是一個函數,這個函數以 (Int, Int) => Int 類型為返回值。因此,如果我們傳遞不同的第一層函數的參數,即 Int => Int,那麼就將得到不同的第二層函數,即類型為 (Int, Int) => Int 的函數。

因此,我們可以傳入不同的 f,傳入 f 之後,得到的函數就是以 a 和 b 為參數的函數:

為此我們將 sumT 函數定義成如下形式,使得 sumT 函數返回一個類型為 (Int, Int) => Int 的函數:

defsumT(f: Int => Int): (Int, Int) => Int = {
    defsumF(a: Int, b: Int): Int =
        if(a > b) 0
        else f(a) + sumF(a+1, b)
    sumF
}

然後我們就可以傳遞不同的 f,獲取不同的偏函數:

def sum3 = sum(x => x * x * x)  //三次方
def sum4 = sum(x => x * x * x * x)  //四次方

這裡可以看出,偏函數和 currying 是有很大的關聯的。函數 currying 後,很容易實現某些偏函數。

在 scala 中,如果需要減少函數的參數,即固定函數的某些參數,那麼使用偏函數即可。那麼,currying的作用是什麼?

作用有二,一個是可以將函數的參數寫成{}形式,這對於參數是函數時可讀性更好。

在currying之前,可能需要這樣寫:

using (new File(name), f => {......})

在有了 currying 之後,我們可以寫成更易讀的方式:

using(new File(name)){ f => ......}

第二個作用就是用於類型推導:

一般的 currying 可以寫成

deff(arg1)(arg2)......(argn) => E
//等同於
deff(arg1)(arg2)......(argn-1) => { defg(argn) => E; g}
/.更簡單點
deff(arg1)(arg2)......(argn-1) => (argsn => E) 
//繼續最後就可以寫成
def f = args1 => args2......=>argsn => E

這樣寫之後,scala 可以對參數之間的類型進行推導:左邊參數的類型可以用於推導右邊參數的類型,從而某些參數就可以簡化寫法:

def dropWhile[A](as: List[A], f: A => Boolean): List[A] = ......
//調用
dropWhile(List(1,2,3,4), (x: Int => x < 4))

//currying之後
dropWhile[A](as:List[A])(f: A => Boolean): List[A] = ......
//調用
dropWhile(List(1,2,3,4))(x => x < 4)  //這裡不再需要指明 x 的類型

這樣就可以用來結合匿名函數的下劃線寫出更加簡潔的代碼。

currying 也可以在某些只允許使用單個參數的函數的情景下發揮作用,利用 currying 使用層層嵌套的單參數函數,可以實現語法層面的多參數函數。

Copyright © Linux教程網 All Rights Reserved