函數 為什麼要Currying化,currying化有什麼優點?

我自己先說一下自己的猜想

1. 為了簡化,早期的函數只能使用一個參數

2. 為了更好的實現偏函數


一個比較小的好處在於可以簡化語法設計。例如我們有這樣的語法與一個符合該語法的表達式:

IntAddExpr := Int | IntAddExpr + Int
1 + 2 + 3

不管 left recursion,1 + 2 + 3 這個表達式我們可以 parse 為 (1 + 2) + 3 的形式,此時 + 的右邊總是一個 Int,它的左邊要麼是一個 Int,要麼是一個 IntAddExpr。如果我們對它進行求值,那麼每個 + 左邊的 IntAddExpr 總會 eval 成一個 Int,這個表達式中的 + 左右的東西都一樣,整個表達式最後也會 eval 到一個 Int。

再考慮 C++ 中的 operator&<&<:

std::cout &<&< "Achieve " &<&< 96 &<&< " units of credit in " &<&< 4 &<&< " semesters." &<&< std::endl;

這個語句中每個 &<&< 左右兩邊的東西都不完全一樣,有 std::ostream,有 String Literal 和 Int Literal,還有一個 std::endl。這時 operator&<&< 的語義應該如何?

最合理的理解是,operator&<&< 是左結合,每次對它求值時,它返回的類型都是一個 std::ostream。換言之,std::cout 吞掉一個參數後,它的類型依然是一個 std::ostream,它再吞下一個參數時類型也不變。我們只要保證第一個 operator&<&< 左邊是一個 std::ostream,剩下的就全都是編譯器的事。

最後我們來看 Haskell 中的函數調用:

let f x y z = x + y + z
f 1 2 3

Haskell 中無論是函數與第一個參數之間,還是多個參數之間都是用空格分割。這個空格的語義是什麼?

這裡就是 currying 的作用。每個空格代表的都是函數調用,調用完之後返回的還是一個函數。(f 1) 是一個新函數,這個函數再調用 2 就是 (f 1) 2,然後再調用 3 變成 ((f 1) 2) 3。考慮到 Haskell 中函數和值用法很接近,沒有本質上的區別,這樣的 currying 就非常類似上文提到的 operator&<&< 的行為,同一個算符(哪怕是個空格)兩邊可以放類型不相同的成員,但語義上只有第一個和其它都不一樣。無論一個函數需要多少參數本質上都是同樣的情況。你就可以設計這樣看起來超級簡單的語法。


這樣就能很容易的把多參數函數map到單參數函數上,論文和證明寫起來會容易很多。


為了改變自己的認識。

吃2個饅頭的才能不餓的人,在吃了1個饅頭以後其實也是人,只不過還有點餓而已。


單純的Currying作用不大,它的主要作用還是方便的定義高階函數等。

比如最常見的map,map的行為看上去像是這樣:

map :: (a -&> b) -&> [a] -&> [b]

參數是兩個,一個函數(a -&> b),一個序列[a],得到另一個序列[b]。甚至在實現上很多語言都是這樣做的。然而,map的運作完全可以是這樣:

map :: (a -&> b) -&> ([a] -&> [b])

接受一個函數(a -&> b),返回一個從序列到序列的函數([a] -&> [b])。

這兩種從功能上來說沒有什麼區別,也沒有說哪種就是正確的。但如果map是Currying的,那麼這兩種其實殊途同歸。

再舉一例,Currying偶爾也能起到單獨的小作用,在靜態類型語言中協助類型推導:

// scala
val a = Array("Hello", "World")
val b = Array("Hello", "World")
a.corresponds(b)(_.equalsIgnoreCase(_))

corresponds之後的_.equalsIgnoreCase(_)即Currying參數,實際上corresponds的聲明形如:

def corresponds [B] (that: Seq[B])(p: (A, B) =&> Boolean): Boolean

這時編譯器會知道that的類型是String,並期待後面的equalsIgnoreCase的類型是(String, String) =&> Boolean,這樣,就省去了後面參數中的類型聲明,直接寫下劃線完事兒。


Currying 是為了簡化討論,把多元函數簡化成一元。


之前我自己試圖實現一個小小的Scheme的類型推導工具,發現Currying除了理論形式上的簡約,在Haskell中語法表達上的簡約,在實現上還有一些有趣的地方:

1. Currying的情形下,解釋一個abstraction結構時,由於parameter只有一個,所以解釋abstraction body時只需要向environment中添加一個Binding,使得environment可以用Stack&類型來實現。假如不使用Currying,對於多個參數,我們必須向棧中添加一個Mapping of Bindings,而不僅僅是一個Binding,即environment需要Stack&&>類型來實現。

另一方面與Currying相應的lookup函數也更簡單。

2. Currying的情形下,所有的abstraction/application都是二元表達式結構,使得類型推導的約束方程可以完全以 T3 == T1 -&> T2 的形式呈現出來,有點類似編譯原理中提到的喬姆斯基標準型(CNF),非常漂亮,有種一生二二生三三生萬物的趕腳(誤

*. 以下是簡單的Python偽代碼,用於解釋Currying下的Lambda Term結構並構造類型約束方程組。

# Pseudo interpreter in Python
def interp(term, env):
if is_var(term):
binding = lookup(term.symbol, env)
if binding:
return binding
else:
return Var(term.name)
elif is_abst(term):
local = Var(term.par.symbol)
env1 = cons(pair(term.par.symbol, local), env) # local_binding入棧
bdy = interp(term.body, env1)
abst = Abstraction(local, bdy)
add_type_equation(abst.id, [local.id, -&>, bdy.id]) # 形如T3 = T1 -&> T2
return abst
elif is_appl(term):
fun = interp(term.fun, env)
arg = interp(term.arg, env)
app = Application(fun, arg)
add_type_equation(fun.id, [arg.id, -&>, app.id]) # 形如T2 = T1 -&> T3
return app
else:
raise ValueError()

剩下的就是用Unification來求解約束方程組了,類型推導不再神秘~

(相關方法可以參考Implementation of Functional Languages一書


(重複問題的重複回答預警:這篇文章我已經在知乎上貼第三遍了,請讀過的童鞋自動忽略。。

為什麼我們需要(or不需要)科里化

我們見過的多參函數

多參函數似乎是個很無聊的話題,定義或者調用一個多參函數這事兒是每個學習編程的人最先學會的幾件事之一。大家使用起多參函數來,似乎是那麼的自然,那麼的理所應當,甚至大家還在這上面玩出了各種花樣,比如什麼不定長參數啦,什麼具名參數啦,總之各個語言都有自己的一套,大家都各自玩得很high。

好吧,不廢話,先揪幾個小嘍啰出來讓大家指認一下:

這是一個典型的C語言里的多參函數

int plus(int a, int b) {
return a + b;
}

這是以上函數的Python版本

def plus(a, b):
return a + b

python君覺得僅僅是這樣當然還不夠帥氣,所以又加了不定長參數的功能

def sum(*arr):
return foldl(plus, 0, arr)

於是你就可以這樣調用這個sum函數了:

print sum(1, 2)
print sum(1, 2, 3)

(台下有同學一臉迷茫:"foldl"是神馬?好吧,你可以先無視它,我僅僅是想偷個懶少寫個for而已。。

當然了,僅僅這樣是不足以體現python君的帥(zi)氣(lian)的,於是又有了這樣的東西

def sum(*arr, **opts):
plus = (lambda a, b: (a + b) % opts.get(mod, 1024))
return foldl(plus , opts.get(init, 0), arr)

然後用的時候是這樣子

print sum(1, 2)
print sum(1, 2, init = 100, mod = 3)

當然了,除此之外,python君還允許參數有默認值,以及各種傳參方式可以混合使用等等,我就不一一舉例了。

多參帶來的不便

乍一看,Python君用起來好方便,參數想怎麼傳就怎麼傳,多happy。但是事實是,這種做法,在你需要做一些高階抽象的時候,就會帶來無盡的麻煩了。

比如,我想定義一個叫作echo_param的函數,它接受一個函數f作為參數,返回一個稍加修飾後的f2,f2跟f做一樣的事情,區別僅僅是f2會將其接收的參數先列印出來。

如果僅僅考慮f是單參函數的情況,我們可以這樣實現echo_param:

def echo_param(f):
def f2(x):
print "param:", x
return f(x)
return f2

然後你可以這樣用這個echo_param來幫助你調試代碼:

inc = (lambda x: x + 1)
inc = echo_param inc
print returns:, inc(1)
# param: 1
# returns: 2

然而,當你希望你的echo_param函數可以用在任何函數上的時候,你就會發現麻煩來了,為了適應各種傳參方式,你的代碼得寫成這樣:

def echo_param(f):
def f2(*a, **d):
print "param:", a, d
return f(*a, **d)
return f2

於是,你在寫很多高階函數的時候,你都得考慮你的 參數函數f 在面臨各種傳參方式的情況下要怎麼處理,這事兒是痛苦並且容易忘記的,因為你本不應該關心f的參數是些啥的! 類似情況的例子還有memoize等等。 同樣的,如果你想把一個函數和函數的參數(這些信息描述了一個調用過程)記錄下來,然後在合適的時間再去調用(如threading.Thread),那麼你也會遇到「函數的參數難以表達」這個問題。

盲目地引入傳遞多參的機制,其實是沒有把問題想明白的表現。

元組和列表

元組和列表在我看來是最自然和最容易理解的兩種組合基本元素的方式。 元組表達的是固定數量的不同類別(這裡指「不一定同類別「)的值的組合,而列表則表達了不定數量的多個同類的值的羅列。比如("張三", 123)是一個元組,而[123, 456, 789]是一個列表。

有了元組和列表,我們能很容易地表達任何一種複雜數據。現在讓我們想像一下這樣一門語言,我們暫時稱之為QP(」奇葩「)語言,這門語言的設計者忘記了有「多參函數」這種需求,不過他恰好實現了元組和列表,可現在我們需要在QP語言里定義plus這個「二元函數」和sum這個「?元函數」,我們該怎麼辦呢?

好吧,為了防止台下的觀眾因為搶答太過激烈引發安全事故,我就直接貼出答案了:

plus (a, b) = a + b
sum xs = foldl (+) 0 xs

然後你可以在QP語言里這樣調用它們

u = plus (1, 2)
v = sum [1, 2, 3]

於是聰明的你瞬間驚奇地發現:之前的貓貓狗狗語言里定義的各種傳參方式完全是多餘的! 事實上,我們需要的僅僅是表達複合數據類型的能力,以及單參函數,這些就已經完全足夠了,再複雜的參數也能通過兩種組合方式來構造:要麼就是不同類型的事物的有限組合,要麼就是同一類型的多個事物的羅列。

至於怎麼實現「具名參數」和「帶默認值的參數」,就留作思考題吧。

函數作為一等公民,以及「科里化」

不同與貓貓狗狗語言的設計者們(他們在多年後才意識到函數是大大的良民不該被歧視),QP語言在一開始就十分歡迎函數這種外星生物的到來,並且把它們當做一等公民,享有和Int,String,元組,列表等原住民一樣的進出各類公共場所的權利。

函數這種外星生物在QP語言里長這樣(下面是四個這類生物的標本):

x -&> x + 1
(x, y) -&> x + y
f -&> f 1
f -&> (x -&> f x)

因為Int,String,元組,列表們都可以以「函數參數」的身份進入某個函數,或者以「函數返回值」的身份走出某個函數,所以函數作為一等公民也就自然的擁有這些權利,比如以上生物標本中的第三個,就是一個以另一個函數f為參數的函數;而標本中的第四個,不僅參數是個函數,且返回值也是一個函數。嗯,不要覺得奇怪,函數們只是膚色暗一點而已,它們被允許做這些是理所應當的。

當函數被允許返回另一個函數後,QP語言里就會發生一些更有意思的事情了。比如,你可以這樣來重新定義plus了:

plus = x -&> (y -&> x + y)

然後你可以這樣用這個plus函數:

r = (plus 1) 2

嗯,這很好理解,既然(plus 1)的值是個函數(它是(y -&> 1 + y)),那麼(plus 1) 2就相當於(y -&> 1 + y) 2,也就相當於1 + 2,即是3了。

我們通過這樣輾轉的方式,同樣定義了一個「二元函數」。這種利用函數的一等公民身份委婉地定義多元函數的方式,通常被稱作「科里化」(curry)

嗯,先不說這麼定義多元函數好不好。既然你接納「函數是一等公民」這個事情了,那麼它們做出這樣違背倫常的事情也是不可避免的了。

為什麼我們需要科里化

科里化這種現象是隨著函數被當做一等公民自然而然地產生的,原本與我們需要或不需要無關。但既然這樣的自然現象存在了,那麼我們當然可以考慮一下這現象有什麼可以利用的地方。

先來舉個栗子,比如我們現在需要一個all函數,這個函數用來判斷一個列表裡的所有元素,是否都滿足某一給定條件。它應該是接受一個judge函數和一個列表,然後返回一個bool值。我們當然可以這樣來定義它:

all = (judge, xs) -&> if xs == [] then true else ((judge (head xs)) and all (judge, (tail xs)))
flag = all ((x -&> x &> 0), [1, 2, 3])

但是,如果現在的情況是,我需要判斷一系列的列表分別是否「合法」(即列表的每個元素都使judge返回True),那麼我得這樣寫:

flag1 = all ((x -&> x &> 0), [1, 2, 3])
flag2 = all ((x -&> x &> 0), [4, 5])
flag3 = all ((x -&> x &> 0), [6, 7, 8, 9])

當然,把judge函數寫這麼多遍太二了,所以大家一般會寫成這樣:

judge = (x -&> x &> 0)
flag1 = all (judge, [1, 2, 3])
flag2 = all (judge, [4, 5])
flag3 = all (judge, [6, 7, 8, 9])

事實上,你會發現以上的代碼仍然有all (judge, XXX)這樣的公共模式,於是聰明的你當然會這樣寫:

judge = (x -&> x &> 0)
judge_list = (xs -&> all (judge, xs))
flag1 = judge_list [1, 2, 3]
flag2 = judge_list [4, 5]
flag3 = judge_list [6, 7, 8, 9]

甚至,聰明的你連judge_list這個名字也不想重複這麼多遍

judge = (x -&> x &> 0)
judge_list = (xs -&> all (judge, xs))
[flag1, flag2, flag3] = (map judge_list) [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

這裡,從發現all (judge, XXX)這樣的公共模式,到judge_list這個函數,我們所做的事情是「抽象」,即把一個以XXX為模板變數的公共模式,抽象成一個以x為自變數的函數。我們發現,通過合適的抽象,我們總是能得到我們想要的東西。不過,有沒有更方便一點點的方式呢?

如果我們考慮通過「科里化」的方式來處理all函數的兩個參數,那麼all的定義就是這樣:

all = judge -&> (xs -&> ...)
flag = (all (x -&> x &> 0)) [1, 2, 3]

這樣一來,當我們需要judge_list函數的時候,我們可以通過「函數應用」來得到,而不再需要做「抽象」了:

judge_list = all (x -&> x &> 0)
[flag1, flag2, flag3] = (map judge_list) [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

嗯,這樣當然會讓事情簡單一些,並且由於可以方便地「部分應用」函數(畢竟「抽象」還是相對麻煩一點點),我們可以不用取很多臨時的名字啦(畢竟你看,通過「部分應用」得到的all (x -&> x &> 0)並不比它的名字judge_list長很多對吧):

[flag1, flag2, flag3] = (map (all (x -&> x &> 0))) [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

這樣一來,我們的代碼似乎帥氣了很多對吧

為什麼我們不需要科里化

其實,構造這個judge_list函數,是基於一種叫做Point-free的想法。Point-free就是說,你定義的東西(如函數),應該可以獨立的存在,並且具有完整的語義,而與應用到的具體目標無關。在以上的演示中,我們已經看到,Point-free的想法,是避免重複的有效手段。

當然了,只要做到了Point-free,不管用不用「科里化」這種方式,我們都可以有效地避免重複,科里化的意義,僅僅是某些時候可以讓事情方便那麼一點點。

然而,在「方便一點點」之餘,科里化的風格的大量應用還會帶來一些別的副作用,讓我們對比一下用元組參數風格和科里化參數風格定義的兩個all函數在實際使用的時候會讓你的代碼變成什麼樣子:

judge = (x -&> x &> 0)
judge_list = (xs -&> all (judge, xs))
[flag1, flag2, flag3] = (map judge_list) [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

[flag1, flag2, flag3] = (map (all (x -&> x &> 0))) [[1, 2, 3], [4, 5], [6, 7, 8, 9]]

前者是元組風格的多參,後者是科里化風格的多參,我們很容易發現,當我們只有元組形式的多參函數可以用的時候,我們更傾向於定義一些中間步驟來做適當的抽象,然後再將這些中間步驟組合起來。而當我們大量使用科里化風格的時候,我們則傾向於省略這些中間步驟,畢竟給這些中間步驟取個名字的話,名字的長度不會比組合這些函數來得簡短。 請注意,我在這裡討論的是「傾向」,不是必然。很多人會說,即使用科里化,也可以給中間步驟取些名字,這不假,但大量使用科里化,會讓人傾向於寫出例子中的代碼,卻是事實。

值得注意的是,這種不同,會給代碼的可讀性帶來非常微妙的影響,我們發現,由於缺少必要的中間步驟,科里化風格常常傾向於讓人寫出包含過多細節的代碼,而不太願意適時地給出一些中間步驟。

事實上,如何在構建程序的過程中,保持一個合適的抽象粒度,這本身就是一個很有意思的話題。我會在一篇單獨的博文里詳細地聊一聊這件事。

三種傳遞多參數的方式比較

語法層面支持的多參函數定義是一種扁平、簡單、有效的方式,但是,當程序需要做高階函數抽象的時候,這種方式會帶來麻煩。

通過組合型數據來傳遞複雜參數,是一種很自然的方式,這種情況下並不需要刻意考慮「多參函數」這種問題,並且這種方式對於做高階函數抽象非常友好。

科里化的方式定義多參函數,同樣是一種自然產生的方式,只要把函數當做一等公民,就會存在科里化的方式。科里化的方式使得我們的函數可以更細粒度地方便地組合。

這麼看來,如果要設計一門函數式語言,僅支持後兩者是自然且明智的選擇。然而,大多數流行的語言(包括流行的lisp家族語言)卻都選擇了包含第一種方式的多參——即支持多參語法,從我目前的分析來看,這真的不是一個好的選擇。事實上,在同時存在三種傳遞多參的方式的時候,定義一個多參函數時,選擇傳參的方式都是一件令人頭疼的事情。

如何選擇

當你需要定義一個多參函數的時候,你會把它定義成什麼樣子呢?

在支持多參函數語法的語言里,把它定義成「接受一個組合型參數的函數」或是定義成「多參函數」通常僅僅是出於美觀和習慣上的考慮。雖然我個人是更傾向於用組合型參數的方式的,但是很多時候這麼做並不符合該語言的用戶習慣,而有些語言甚至就沒有提供方便的組合數據的方式。

但是,是否要使用科里化的方式,卻是一件需要細細揣摩的事情。比如,我要定義一個rotate函數,它接受一個二維向量的坐標,返回一個逆時針旋轉九十度後的向量坐標,那麼你可能會把它定義成

rotate = (x -&> (y -&> (y, -x)))

或者

rotate = ((x, y) -&> (y, -x))

你覺得哪種定義方式比較好呢? 在這個例子里,顯然是第二種定義方式比較好,一來,二維向量的兩個坐標通常是成對出現的,很少會有「部分應用」的需要,所以也就自然用不到科里化的任何好處;二來,當你需要把一個向量rotate兩次的時候,科里化版本就會體現出異常的麻煩了。

所以,我的結論是:

  1. 如果某些參數在大部分情況下,總是需要同時給出,才具有真實的意義,那麼應該把這些參數組合成一個組合型參數來處理,而不應該科里化。
  2. 如果要定義的多參函數是一個閉合函數,那麼它是很可能需要被多次應用的,這種情況下,應該用組合型參數的方式來處理。
  3. 如果先指定某一些參數就有明確的意義,那麼就應該用科里化的方式來處理。

以上內容copy自我自己的blog:為什麼我們需要(or不需要)科里化——關於三種定義多參函數的風格的討論

所以,結論是,並非「科里化」對函數式編程有意義。而是,函數式編程在把函數當作一等公民的同時,就不可避免的會產生「科里化」這種用法。所以它並不是因為「有什麼意義」才出現的。當然既然存在了,我們自然可以探討一下怎麼利用這種現象。


Curry化是lambda表達式帶來的必然結果啊。

函數可以作為另一個函數的body,為了簡便所以可以Curry化。


我能想到的一種好玩的用法就是

let fx f p1 = f p1 p2 ... px

其中 f : a -&> b-&> ... z -&> c

p1 : a

p2 : b

...

px : z

這樣對於任何簽名為(a -&> b-&> ... z -&> c)的函數fa和固定參數p2...px,你都可以用fx fa p1直接調用


如果你的函數要返回另一個函數怎麼辦?

def foo(a):
def bar(b):
...
return bar

語法整體來說還是比較繁瑣的,有了currying,你可以直接定義

def foo(a)(b):
...

凡是能用currying完成的,也是能用函數嵌套完成的,說到底還是一個語法糖,方便寫程序


就是語義學裡面討論lambda的時候,只需要對一元函數的情況成立,然後用currying推廣的任意n元函數都成立。


我來從兩個不同的角度說說我的看法。

科里化的第一個作用是,解決懶得寫或者不好寫閉包的問題。這個也是其他答案著重描述的理解。其實就是配合其他函數的調用場景。

另一個我覺得比較有意思的理解角度,就是科里化把函數的層層調用和封裝,變成了狀態的轉換。函數每調用一次,生成一個新的函數,即相當於遷移至了一個新的狀態上。於是程序代碼更加接近狀態機,也會有助於程序結構的梳理。


科里化的過程,就是函數逐步求值的過程。


本末倒置了,應該問為什麼有多參數的函數。很多語言都是以lambda演算為基礎,lambda演算只承認一元函數,curry是lambda演算中天然存在的,多元函數只是curry的語法糖。


我覺得currying不一定要那種語法, 像

(a-&>b) -&> [a] -&> [b]
map

變成

[a] -&> [b]
map f

哪怕是手動用個lambda也能叫做currying,還不受參數順序限制

(a -&> b) -&> [b]
f -&> map f somelist

對著整數我們能加減乘除,對著函數我們能幹嘛呢?當然是currying了。


newPeople nationality age gender address mother father weight height = xxxxx

newSpanish = newPeople spanish


2. 為了更好的實現偏函數

#partially applied function和partial function還是不同的, currying和偏函數有關係么?

我覺得在函數為一類對象的語言中,由一個函數生成另外一個函數就如同過程式編程中對變數進行變更一樣基本。

currying的地位就如同變數賦值一樣基本


currying化使函數具有部分求值的能力。


curry更多的還是理論價值吧,因為pure lambda calculas裡面是沒有多元函數的。


推薦閱讀:

學習編程有什麼前景?
Sublime Text 2的Haskell開發環境設置
為什麼 WhatsApp 後台使用 Erlang 而不是 C?
Python 中 open() 方法既能直接返回也能通過with語句當作上下文管理器使用是怎麼做到的?
C++派生類怎樣進行文件讀寫?

TAG:編程語言 | 函數式編程 | Scheme |