第二周
這一周的內(nèi)容主要圍繞著“函數(shù)”來進行璧尸。本來想順著書的內(nèi)容往下講咒林,不過那樣就沒有自己的東西了,所以爷光,就想到哪里寫到哪里吧~
普通函數(shù)
我們從一個最普通的函數(shù)開始垫竞。它的作用跟函數(shù)名一樣,你給它輸入什么蛀序,它給你返回什么欢瞪。
def echo(x : Int) : Int = {
x
}
在語法上:
- 函數(shù)定義以def開頭。
- 函數(shù)的返回值類型在函數(shù)名之后徐裸。
- 注意那個等號遣鼓。
- 不需要顯式的寫return。(請問是為什么呢)
泛型函數(shù)
當然倦逐,這個例子只是為了引出接下來的例子譬正,因為普通的版本,只能接受Int作為輸入和輸入檬姥,如果要支持任意類型曾我,就需要讓函數(shù)支持泛型。
def echo[T](x : T) : T = {
x
}
通過在函數(shù)名之后的中括號和T健民,可以支持單個參數(shù)的泛型抒巢,如果要支持多個,可以寫成類似[T, V]的形式秉犹。
帶默認值的函數(shù)
好了蛉谜,水完跟Java類似的部分,下面講講Scala做得好的部分崇堵。
相信你一定見過這樣的代碼型诚,把一個字符串類型的日期,轉換為Date鸳劳,通常會根據(jù)不同的合適進行適配狰贯,比如20201226和2020-12-26。通常大部分日期格式都是第二種赏廓,因此會定義2個函數(shù)涵紊,一個需要str和pattern 2個參數(shù),一個只需要str一個參數(shù)幔摸。
但是摸柄,在Scala中,你可以通過一個帶默認值的函數(shù)來完成Java里2個函數(shù)的功能既忆。
def str2Date(str : String, pattern : String = "yyyy-MM-dd") : Date = {
…
}
str2Date("20201226", "yyyy-MM-dd")
str2Date("2020-12-26")
在語法上驱负,通過pattern參數(shù)后面加一個等號來完成嗦玖。
遞歸和尾遞歸
遞歸大家都懂,但很多人都不知道尾遞歸是什么电媳。
在我的理解里踏揣,尾遞歸就是滿足這樣條件的遞歸:遞歸函數(shù)的調(diào)用出現(xiàn)在函數(shù)的最后一行,并且匾乓,這一行有且只有這個函數(shù)調(diào)用本身捞稿。
比如下面的寫法就是一個普通的遞歸函數(shù)。
def fact(i : Int) : Int = {
if (i == 1) 1
else i * fact(i - 1)
}
如果要改成尾遞歸形式拼缝,可以這么寫娱局。
def fact(i : Int) : Int = {
@annotation.tailrec
def inner(n : Int, result : Int) : Int = { // 1
if (n == 1) result // 2
else inner(n - 1, n * result) // 3
} // 4
inner(i, 1) // 5
}
這里用了一個嵌套函數(shù)的例子,證明了在Scala中函數(shù)屬于一等公民咧七。(這個點我們稍后再展開)
一次函數(shù)調(diào)用意味著JVM中一個棧幀的入棧和出棧衰齐,因此普通的遞歸函數(shù)容易出現(xiàn)“爆棧”继阻。而編譯器在看到尾遞歸的寫法的時候耻涛,可以進行優(yōu)化:把遞歸改為迭代,從而消除函數(shù)調(diào)用瘟檩。
從遞歸到迭代是有范式的抹缕,比如上面的寫法可以轉化為以下的等價寫法,不過墨辛,這是我的一個理解卓研,而真正的實現(xiàn),感覺可以再留一個坑在這里后面來填睹簇。
def fact(i : Int) : Int = {
var result : Int = 1 // 根據(jù)5的第二個參數(shù)
var n : Int = i // 根據(jù)5的第一個參數(shù)
while (n > 1) { // 根據(jù)2和3
result *= n // 根據(jù)3的n * result
n -= 1 // 根據(jù)3的n - 1
}
result
}
(順帶提一句奏赘,在Java里是沒有尾遞歸優(yōu)化的說法的)
下面終于來到了FP(函數(shù)式編程)的世界(激動)
一等公民
先叨叨一下什么是剛才說到的一等公民。在Java的世界里太惠,類是一等公民磨淌,因為你可以把它賦值給一個變量、你可以把它作為一個方法的輸入和輸出凿渊、你可以在任何地方定義一個類(在方法里伦糯、在類里、甚至是定義一個匿名內(nèi)部類)嗽元。在Java的世界里,函數(shù)是“從屬”于類的喂击,這就會顯得很繁瑣和臃腫剂癌,比如要根據(jù)不同的方式給一個List排序,還需要定義一個排序的接口翰绊。
而在Scala的世界(或者說函數(shù)式的世界)佩谷,函數(shù)就是一等公民旁壮,我們參考一下剛才的定義,你可以把它賦值給一個變量谐檀、你可以把它作為一個方法的輸入和輸出抡谐、你可以在任何地方定義一個函數(shù)(比如剛才那個例子里面的inner就是定義在fact里面的輔助函數(shù))。
當我們用i這個變量桐猬,val i : Int = 1
麦撵,去接收一個值的時候,實際上我們需要聲明i的類型是什么溃肪。而現(xiàn)在免胃,要支持把函數(shù)賦值給i這樣的特性,對于程序員來說是有“額外”的負擔的惫撰。那就是你需要知道函數(shù)的類型羔沙,并把它顯式的聲明出來。
在寫Java的時候厨钻,其實你不會那么的關注一個方法的類型扼雏。比如剛才的fact函數(shù),類型是(Int) => Int
夯膀,而inner函數(shù)的類型是(Int, Int) => Int
诗充。可以看到棍郎,函數(shù)的類型和入?yún)⒌念愋图皞€數(shù)其障,以及返回值都相關。(這和Java里判斷方法是否重載的邏輯不同)涂佃。
(考考你励翼,Int => Int => Int
,代表的是怎樣的一個函數(shù)辜荠?)
高階函數(shù)
剛才講了函數(shù)賦值給變量i的場景汽抚,那相應的,一個函數(shù)A可以作為另一個函數(shù)B的入?yún)⒒蛘叻祷刂挡 O馚這樣的函數(shù)造烁,我們稱為高階函數(shù)。
大家最耳熟能詳?shù)母唠A函數(shù)應該是Map和Reduce午笛。如果是從Java或者Spark切換過來的惭蟋,可能對于Map的理解是Stream的一個方法或者RDD的一個方法。實際上最正統(tǒng)的Map(來自于Lisp之父約翰·麥卡錫)药磺,應該支持2個參數(shù)告组,一個參數(shù)是一個集合,另一個參數(shù)是一個函數(shù)癌佩。它的作用是把函數(shù)應用在每一個集合元素上木缝,并返回一個新的集合便锨。
// 定義一個map函數(shù)
def map(f : Int => Int, c : List[Int]) : List[Int] = {
var res = List[Int]()
for(e <- c) {
res = res :+ f(e)
}
res
}
// 將列表的每個元素值翻倍
map(e => e * 2, List(1, 2, 3, 4))
// 將列表的每個元素值加1
map(e => e + 1, List(1, 2, 3, 4))
基于高階函數(shù),可以實現(xiàn)Java中很多的設計模式我碟,比如在高階函數(shù)中定義好一個通用的骨架放案,然后把需要動態(tài)變化的部分,通過函數(shù)參數(shù)傳進來矫俺,能實現(xiàn)類似于模版模式吱殉、策略模式、代理模式等多種效果恳守。
多扯幾句考婴,高階函數(shù)這個詞,聽著比較學究催烘,實際上沥阱,如果把上面那個例子的=>
改成->
就是Java中的Lambda表達式的寫法了,從使用的角度來說伊群,我們不需要高階函數(shù)這樣的說法考杉,不過如果在面試時候能把這一套講清楚,無疑是一個加分項舰始。
更多關于Java中的Lambda的內(nèi)容崇棠,可以參考《State of the Lambda》[2]這篇文檔(需要的化,可以翻譯給你哦)丸卷。
柯里化
上面講了以函數(shù)作為參數(shù)的高階函數(shù)枕稀,下面我們講講以函數(shù)作為返回值的高階函數(shù)。
假設有這樣一個東西(A):
當我輸入1時谜嫉,它會返回給我這樣一個東西(B)萎坷,接受一個Int型的輸入,并返回輸入加1后的結果沐兰。
當我輸入10時哆档,它會返回給我這樣一個東西(C),接受一個Int型的輸入住闯,并返回輸入加10后的結果瓜浸。
在Java的世界里,我們可能會定義一個叫Increase接口比原,B和C都是它的實現(xiàn)類插佛。,而A則是Increase的工廠量窘。
一個字雇寇,繁瑣。在FP的世界里,我們Duck不必這樣谢床。
def increaseMaker(i : Int)(n : Int) : Int = {
i + n
}
increaseMaker(5)(10)
// 得到15
val increase1 = increaseMaker(1) _
val increase5 = increaseMaker(5) _
increase1(10)
// 得到11
increase5(10)
// 得到15
定義一個構造increase的函數(shù),它有2個參數(shù)厘线。
通過increaseMaker(1) _
我們綁定了它的第一個參數(shù)识腿,在第二個參數(shù)的位置,用占位符代替造壮,代表未來再來綁定渡讼。這樣我們就構造了一個部分應用的函數(shù)。
再通過類似increase1(10)
的形式耳璧,拿到最后的結果成箫。
這底層所用到的技術,我們稱之為柯里化旨枯。它是指蹬昌,一個多參數(shù)的函數(shù),可以接受一個參數(shù)攀隔,并返回一個可以接受其余參數(shù)的函數(shù)的技術皂贩。
而返回的那個函數(shù),我們成為閉包昆汹。意思是明刷,一個攜帶著狀態(tài)的函數(shù),而這個狀態(tài)或者說變量的定義满粗,來自于函數(shù)之外辈末。
剛才那個例子,實際上還可以這么寫映皆,我認為參數(shù)組挤聘,就是下面這種寫法的語法糖:
def increaseMaker(i : Int) : Int => Int = {
def inner(n : Int) = {
i + n
}
inner
}
因為它,像Lambda演算這樣只能支持單參數(shù)的東西劫扒,才有了支持多參數(shù)的能力檬洞。
插播一下,柯里化這個詞沟饥,是為了紀念數(shù)學家柯里而命名的添怔。他的全名叫Haskell Curry,first name和last name都貢獻給了PL界贤旷。