Kotlin極簡(jiǎn)教程:第8章 函數(shù)式編程

原文鏈接:https://github.com/EasyKotlin

值就是函數(shù)叁扫,函數(shù)就是值形帮。所有函數(shù)都消費(fèi)函數(shù)腰懂,所有函數(shù)都生產(chǎn)函數(shù)窜管。

"函數(shù)式編程", 又稱泛函編程, 是一種"編程范式"(programming paradigm),也就是如何編寫程序的方法論韧涨。它的基礎(chǔ)是 λ 演算(lambda calculus)牍戚。λ演算可以接受函數(shù)當(dāng)作輸入(參數(shù))和輸出(返回值)。

和指令式編程相比虑粥,函數(shù)式編程的思維方式更加注重函數(shù)的計(jì)算如孝。它的主要思想是把問題的解決方案寫成一系列嵌套的函數(shù)調(diào)用。

就像在OOP中娩贷,一切皆是對(duì)象第晰,編程的是由對(duì)象交合創(chuàng)造的世界;在FP中彬祖,一切皆是函數(shù)茁瘦,編程的世界是由函數(shù)交合創(chuàng)造的世界。

函數(shù)式編程中最古老的例子莫過于1958年被創(chuàng)造出來的Lisp了储笑。Lisp由約翰·麥卡錫(John McCarthy甜熔,1927-2011)在1958年基于λ演算所創(chuàng)造,采用抽象數(shù)據(jù)列表與遞歸作符號(hào)演算來衍生人工智能突倍。較現(xiàn)代的例子包括Haskell腔稀、ML、Erlang等∽阜剑現(xiàn)代的編程語言對(duì)函數(shù)式編程都做了不同程度的支持烧颖,例如:JavaScript, Coffee Script,PHP窄陡,Perl炕淮,Python, Ruby, C# , Java 等等(這將是一個(gè)不斷增長(zhǎng)的列表)。

函數(shù)式語言在Java 虛擬機(jī)(JVM)平臺(tái)上也迅速地嶄露頭角跳夭,例如Scala 涂圆、Clojure ; .NET 平臺(tái)也不例外币叹,例如:F# 润歉。

函數(shù)作為Kotlin中的一等公民,可以像其他對(duì)象一樣作為函數(shù)的輸入與輸出颈抚。關(guān)于對(duì)函數(shù)式編程的支持踩衩,相對(duì)于Scala的學(xué)院派風(fēng)格,Kotlin則是純的的工程派:實(shí)用性、簡(jiǎn)潔性上都要比Scala要好驱富。

本章我們來一起學(xué)習(xí)函數(shù)式編程以及在Kotlin中使用函數(shù)式編程的相關(guān)內(nèi)容锚赤。

8.1 函數(shù)式編程概述

Kotlin極簡(jiǎn)教程
Kotlin極簡(jiǎn)教程

函數(shù)式編程思想是一個(gè)非常古老的思想。我們簡(jiǎn)述如下:

  • 我們就從1900 年 David Hilbert 的第 10 問題(能否通過有限步驟來判定不定方程是否存在有理整數(shù)解褐鸥?) 開始說起吧线脚。

  • 1920,Sch?nfinkel叫榕,組合子邏輯(combinatory logic)浑侥。直到 Curry Haskell 1927 在普林斯頓大學(xué)當(dāng)講師時(shí)重新發(fā)現(xiàn)了 Moses Sch?nfinkel 關(guān)于組合子邏輯的成果。Moses Sch?nfinkel的成果預(yù)言了很多 Curry 在做的研究晰绎,于是他就跑去哥廷根大學(xué)與熟悉Moses Sch?nfinkel工作的Heinrich Behmann寓落、Paul Bernays兩人一起工作,并于 1930 年以一篇組合子邏輯的論文拿到了博士學(xué)位寒匙。Curry Brooks Haskell 整個(gè)職業(yè)生涯都在研究組合子零如,實(shí)際開創(chuàng)了這個(gè)研究領(lǐng)域躏将,λ演算中用單參數(shù)函數(shù)來表示多個(gè)參數(shù)函數(shù)的方法被稱為 Currying (柯里化)锄弱,雖然 Curry 同學(xué)多次指出這個(gè)其實(shí)是 Sch?nfinkel 已經(jīng)搞出來的,不過其他人都是因?yàn)樗昧瞬胖阑霰铮赃@名字就這定下來了会宪;并且有三門編程語言以他的名字命名,分別是:Curry, Brooks, Haskell蚯窥。Curry 在 1928 開始開發(fā)類型系統(tǒng)掸鹅,他搞的是基于組合子的 polymorphic,Church 則建立了基于函數(shù)的簡(jiǎn)單類型系統(tǒng)拦赠。

  • 1929, 哥德爾(Kurt G?del )完備性定理巍沙。G?del 首先證明了一個(gè)形式系統(tǒng)中的所有公式都可以表示為自然數(shù),并可以從一自然數(shù)反過來得出相應(yīng)的公式荷鼠。這對(duì)于今天的程序員都來說句携,數(shù)字編碼、程序即數(shù)據(jù)計(jì)算機(jī)原理最核心允乐、最基本的常識(shí)矮嫉,在那個(gè)時(shí)代卻腦洞大開的創(chuàng)見。

  • 1933牍疏,λ 演算蠢笋。 Church 在 1933 年搞出來一套以純?chǔ)搜菟銥榛A(chǔ)的邏輯,以期對(duì)數(shù)學(xué)進(jìn)行形式化描述鳞陨。 λ 演算和遞歸函數(shù)理論就是函數(shù)式編程的基礎(chǔ)昨寞。

  • 1936,確定性問題(decision problem,德文 Entscheidungsproblem (發(fā)音 [?nt??a??d??sp?o?ble?m])援岩。 Alan Turing 和 Alonzo Church熟史,兩人在同在1936年獨(dú)立給出了否定答案。

1935-1936這個(gè)時(shí)間段上窄俏,我們有了三個(gè)有效計(jì)算模型:通用圖靈機(jī)蹂匹、通用遞歸函數(shù)、λ可定義凹蜈。Rosser 1939 年正式確認(rèn)這三個(gè)模型是等效的限寞。

  • 1953-1957,F(xiàn)ORTRAN (FORmula TRANslating )仰坦,John Backus履植。1952 年 Halcombe Laning 提出了直接輸入數(shù)學(xué)公式的設(shè)想,并制作了 GEORGE編譯器演示該想法悄晃。受這個(gè)想法啟發(fā)玫霎,1953 年 IBM 的 John Backus 團(tuán)隊(duì)給 IBM 704 主機(jī)研發(fā)數(shù)學(xué)公式翻譯系統(tǒng)。第一個(gè) FORTRAN (FORmula TRANslating 的縮寫)編譯器 1957.4 正式發(fā)行妈橄。FORTRAN 程序的代碼行數(shù)比匯編少20倍庶近。FORTRAN 的成功,讓很多人認(rèn)識(shí)到直接把代數(shù)公式輸入進(jìn)電腦是可行的眷蚓,并開始渴望能用某種形式語言直接把自己的研究?jī)?nèi)容輸入到電腦里進(jìn)行運(yùn)算鼻种。John Backus 在1970年代搞了 FP 語言,1977 年發(fā)表沙热。雖然這門語言并不是最早的函數(shù)式編程語言叉钥,但他是 Functional Programming 這個(gè)詞兒的創(chuàng)造者, 1977 年他的圖靈獎(jiǎng)演講題為[“Can Programming Be Liberated From the von Neumann Style? A Functional Style and its Algebra of Programs”]

  • 1956篙贸, LISP投队, John McCarthy。John McCarthy 1956年在 Dartmouth一臺(tái) IBM 704 上搞人工智能研究時(shí)爵川,就想到要一個(gè)代數(shù)列表處理(algebraic list processing)語言敷鸦。他的項(xiàng)目需要用某種形式語言來編寫語句,以記錄關(guān)于世界的信息雁芙,而他感覺列表結(jié)構(gòu)這種形式挺合適轧膘,既方便編寫,也方便推演兔甘。于是就創(chuàng)造了LISP谎碍。正因?yàn)槭窃?IBM 704 上開搞的,所以 LISP 的表處理函數(shù)才會(huì)有奇葩的名字: car/cdr 什么的洞焙。其實(shí)是取 IBM704 機(jī)器字的不同部分蟆淀,c=content of拯啦,r=register number, a=address part, d=decrement part 。

8.1.1 面向?qū)ο缶幊蹋∣OP)與面向函數(shù)編程(FOP)

面向?qū)ο缶幊蹋∣OP)

在OOP中熔任,一切皆是對(duì)象褒链。

在面向?qū)ο蟮拿钍剑╥mperative)編程語言里面,構(gòu)建整個(gè)世界的基礎(chǔ)是類和類之間溝通用的消息疑苔,這些都可以用類圖(class diagram)來表述甫匹。《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》(Design Patterns: Elements of Reusable Object-Oriented Software惦费,作者ErichGamma兵迅、Richard Helm、Ralph Johnson薪贫、John Vlissides)一書中恍箭,在每一個(gè)模式的說明里都附上了至少一幅類圖。

OOP 的世界提倡開發(fā)者針對(duì)具體問題建立專門的數(shù)據(jù)結(jié)構(gòu)瞧省,相關(guān)的專門操作行為以“方法”的形式附加在數(shù)據(jù)結(jié)構(gòu)上扯夭,自頂向下地來構(gòu)建其編程世界。

OOP追求的是萬事萬物皆對(duì)象的理念鞍匾,自然地弱化了函數(shù)交洗。例如:函數(shù)無法作為普通數(shù)據(jù)那樣來傳遞(OOP在函數(shù)指針上的約束),所以在OOP中有各種各樣的候学、五花八門的設(shè)計(jì)模式藕筋。

GoF所著的《設(shè)計(jì)模式-可復(fù)用面向?qū)ο筌浖幕A(chǔ)》從面向?qū)ο笤O(shè)計(jì)的角度出發(fā)的纵散,通過對(duì)封裝梳码、繼承、多態(tài)伍掀、組合等技術(shù)的反復(fù)使用掰茶,提煉出一些可重復(fù)使用的面向?qū)ο笤O(shè)計(jì)技巧。而多態(tài)在其中又是重中之重蜜笤。

多態(tài)濒蒋、面向接口編程、依賴反轉(zhuǎn)等術(shù)語把兔,描述的思想其實(shí)是相同的沪伙。這種反轉(zhuǎn)模式實(shí)現(xiàn)了模塊與模塊之間的解耦。這樣的架構(gòu)是健壯的, 而為了實(shí)現(xiàn)這樣的健壯系統(tǒng)县好,在系統(tǒng)架構(gòu)中基本都需要使用多態(tài)性围橡。

絕大部分設(shè)計(jì)模式的實(shí)現(xiàn)都離不開多態(tài)性的思想。換一種說法就是缕贡,這些設(shè)計(jì)模式背后的本質(zhì)其實(shí)就是OOP的多態(tài)性翁授,而OOP中的多態(tài)本質(zhì)上又是受約束的函數(shù)指針拣播。

引用Charlie Calverts對(duì)多態(tài)的描述: “多態(tài)性是允許你將父對(duì)象設(shè)置成為和一個(gè)或更多的他的子對(duì)象相等的技術(shù),賦值之后收擦,父對(duì)象就可以根據(jù)當(dāng)前賦值給它的子對(duì)象的特性以不同的方式運(yùn)作贮配。”

簡(jiǎn)單的說塞赂,就是一句話:允許將子類類型的指針賦值給父類類型的指針泪勒。而我們?cè)贠OP中的那么多的設(shè)計(jì)模式,其實(shí)就是在OOP的多態(tài)性的約束規(guī)則下宴猾,對(duì)這些函數(shù)指針的調(diào)用模式的總結(jié)酣藻。

很多設(shè)計(jì)模式,在函數(shù)式編程中都可以用高階函數(shù)來代替實(shí)現(xiàn):

Kotlin極簡(jiǎn)教程
Kotlin極簡(jiǎn)教程

面向函數(shù)編程(FOP)

在FP中鳍置,一切皆是函數(shù)辽剧。

函數(shù)式編程(FP)是關(guān)于不變性和函數(shù)組合的一種編程范式。

函數(shù)式編程語言實(shí)現(xiàn)重用的思路很不一樣税产。函數(shù)式語言提倡在有限的幾種關(guān)鍵數(shù)據(jù)結(jié)構(gòu)(如list怕轿、set、map)上 辟拷, 運(yùn)用函數(shù)的組合 ( 高階函數(shù)) 操作撞羽,自底向上地來構(gòu)建世界。

當(dāng)然衫冻,我們?cè)诠こ虒?shí)踐中诀紊,是不能極端地追求純函數(shù)式的編程的。一個(gè)簡(jiǎn)單的原因就是:性能和效率隅俘。例如:對(duì)于有狀態(tài)的操作邻奠,命令式操作通常會(huì)比聲明式操作更有效率。純函數(shù)式編程是解決某些問題的偉大工具为居,但是在另外的一些問題場(chǎng)景中碌宴,并不適用。因?yàn)楦弊饔每偸钦鎸?shí)存在蒙畴。

OOP喜歡自頂向下架構(gòu)層層分解(解構(gòu))贰镣,F(xiàn)P喜歡自底向上層層組合(復(fù)合)。 而實(shí)際上膳凝,編程的本質(zhì)就是次化分解與復(fù)合的過程碑隆。通過這樣的過程,創(chuàng)造一個(gè)美妙的邏輯之塔世界蹬音。

我們經(jīng)常說一些代碼片段是優(yōu)雅的或美觀的上煤,實(shí)際上意味著它們更容易被人類有限的思維所處理。

對(duì)于程序的復(fù)合而言祟绊,好的代碼是它的表面積要比體積增長(zhǎng)的慢楼入。

代碼塊的“表面積”是我們復(fù)合代碼塊時(shí)所需要的信息(接口API協(xié)議定義)哥捕。代碼塊的“體積”就是接口內(nèi)部的實(shí)現(xiàn)邏輯(API內(nèi)部的實(shí)現(xiàn)代碼)。

在OOP中嘉熊,一個(gè)理想的對(duì)象應(yīng)該是只暴露它的抽象接口(純表面遥赚, 無體積),其方法則扮演箭頭的角色阐肤。如果為了理解一個(gè)對(duì)象如何與其他對(duì)象進(jìn)行復(fù)合凫佛,當(dāng)你發(fā)現(xiàn)不得不深入挖掘?qū)ο蟮膶?shí)現(xiàn)之時(shí),此時(shí)你所用的編程范式的原本優(yōu)勢(shì)就蕩然無存了孕惜。

FP通過函數(shù)組合來構(gòu)造其邏輯系統(tǒng)愧薛。FP傾向于把軟件分解為其需要執(zhí)行的行為或操作,而且通常采用自底向上的方法。函數(shù)式編程也提供了非常強(qiáng)大的對(duì)事物進(jìn)行抽象和組合的能力衫画。

在FP里面毫炉,函數(shù)是“一類公民”(first-class)。它們可以像1, 2, "hello"削罩,true瞄勾,對(duì)象…… 之類的“值”一樣,在任意位置誕生弥激,通過變量进陡,參數(shù)和數(shù)據(jù)結(jié)構(gòu)傳遞到其它地方,可以在任何位置被調(diào)用微服。

而在OOP中趾疚,很多所謂面向?qū)ο笤O(shè)計(jì)模式(design pattern),都是因?yàn)槊嫦驅(qū)ο笳Z言沒有first-class function(對(duì)應(yīng)的是多態(tài)性)以蕴,所以導(dǎo)致了每個(gè)函數(shù)必須被包在一個(gè)對(duì)象里面(受約束的函數(shù)指針)才能傳遞到其它地方糙麦。

勻稱的數(shù)據(jù)結(jié)構(gòu) + 勻稱的算法

在面向?qū)ο笫降木幊讨校磺薪允菍?duì)象(偏重?cái)?shù)據(jù)結(jié)構(gòu)舒裤、數(shù)據(jù)抽象喳资,輕算法)。我們把它叫做:胖?jǐn)?shù)據(jù)結(jié)構(gòu)-瘦算法(FDS-TA)腾供。

在面向函數(shù)式的編程中,一切皆是函數(shù)(偏重算法鲜滩,輕數(shù)據(jù)結(jié)構(gòu))伴鳖。我們把它叫做:瘦數(shù)據(jù)結(jié)構(gòu)-胖算法(TDS-FA)。

可是徙硅,這個(gè)世界很復(fù)雜榜聂,你怎么能說一切皆是啥呢?真實(shí)的編程世界嗓蘑,自然是勻稱的數(shù)據(jù)結(jié)構(gòu)結(jié)合勻稱的算法(SDS-SA)來創(chuàng)造的须肆。

我們?cè)诰幊讨心淠耍豢赡苁褂眉兊膶?duì)象(對(duì)象的行為方法其實(shí)就是函數(shù)),或者純的函數(shù)(調(diào)用函數(shù)的對(duì)象豌汇、函數(shù)操作的數(shù)據(jù)其實(shí)就是數(shù)據(jù)結(jié)構(gòu))來創(chuàng)造一個(gè)完整的世界幢炸。如果數(shù)據(jù)結(jié)構(gòu)算法拒贱,那么在解決實(shí)際問題中宛徊,往往是陰陽交合而成世界。還是那句經(jīng)典的:

程序 = 勻稱的數(shù)據(jù)結(jié)構(gòu) + 勻稱的算法

我們用一幅圖來簡(jiǎn)單說明:

OOP vs FP (2).png
OOP vs FP (2).png

函數(shù)與映射

一切皆是映射逻澳。函數(shù)式編程的代碼主要就是“對(duì)映射的描述”闸天。我們說組合是編程的本質(zhì),其實(shí)斜做,組合就是建立映射關(guān)系苞氮。

一個(gè)函數(shù)無非就是從輸入到輸出的映射,寫成數(shù)學(xué)表達(dá)式就是:

f : X -> Y
p : Y -> Z
p(f) : X ->Z

用編程語言表達(dá)就是:

fun f(x:X) : Y{}
fun p(y:Y) : Z{}
fun fp(f: (X)->Y, p: (Y)->Z) : Z {
    return {x -> p(f(x))}
}

8.1.2 函數(shù)式編程基本特性

在經(jīng)常被引用的論文 “Why Functional Programming Matters” 中瓤逼,作者 John Hughes 說明了模塊化是成功編程的關(guān)鍵葱淳,而函數(shù)編程可以極大地改進(jìn)模塊化。

在函數(shù)編程中抛姑,我們有一個(gè)內(nèi)置的框架來開發(fā)更小的赞厕、更簡(jiǎn)單的和更一般化的模塊, 然后將它們組合在一起定硝。

函數(shù)編程的一些基本特點(diǎn)包括:

  • 函數(shù)是"第一等公民"皿桑。
  • 閉包(Closure)和高階函數(shù)(Higher Order Function)。
  • Lambda演算與函數(shù)柯里化(Currying)蔬啡。
  • 懶惰計(jì)算(lazy evaluation)诲侮。
  • 使用遞歸作為控制流程的機(jī)制。
  • 引用透明性箱蟆。
  • 沒有副作用沟绪。

8.1.3 組合與范疇

函數(shù)式編程的本質(zhì)是函數(shù)的組合,組合的本質(zhì)是范疇(Category)。

和搞編程的一樣,數(shù)學(xué)家喜歡將問題不斷加以抽象從而將本質(zhì)問題抽取出來加以論證解決哟沫,范疇論就是這樣一門以抽象的方法來處理數(shù)學(xué)概念的學(xué)科筑公,主要用于研究一些數(shù)學(xué)結(jié)構(gòu)之間的映射關(guān)系(函數(shù))惩坑。

在范疇論里艰赞,一個(gè)范疇(category)由三部分組成:

  • 對(duì)象(object)
  • 態(tài)射(morphism)
  • 組合(composition)操作符

范疇的對(duì)象

這里的對(duì)象可以看成是一類東西,例如數(shù)學(xué)上的群,環(huán)旗唁,以及有理數(shù)畦浓,無理數(shù)等都可以歸為一個(gè)對(duì)象。對(duì)應(yīng)到編程語言里检疫,可以理解為一個(gè)類型讶请,比如說整型,布爾型等屎媳。

態(tài)射

態(tài)射指的是一種映射關(guān)系夺溢,簡(jiǎn)單理解,態(tài)射的作用就是把一個(gè)對(duì)象 A 里的值 a 映射為 另一個(gè)對(duì)象 B 里的值 b = f(a)烛谊,這就是映射的概念风响。

態(tài)射的存在反映了對(duì)象內(nèi)部的結(jié)構(gòu),這是范疇論用來研究對(duì)象的主要手法:對(duì)象內(nèi)部的結(jié)構(gòu)特性是通過與別的對(duì)象的映射關(guān)系反映出來的丹禀,動(dòng)靜是相對(duì)的窑邦,范疇論通過研究映射關(guān)系來達(dá)到探知對(duì)象的內(nèi)部結(jié)構(gòu)的目的。

組合操作符

組合操作符投慈,用點(diǎn)(.)表示专挪,用于將態(tài)射進(jìn)行組合。組合操作符的作用是將兩個(gè)態(tài)射進(jìn)行組合攒读,例如朵诫,假設(shè)存在態(tài)射 f: A -> B, g: B -> C, 則 g.f : A -> C.

一個(gè)結(jié)構(gòu)要想成為一個(gè)范疇, 除了必須包含上述三樣?xùn)|西薄扁,它還要滿足以下三個(gè)限制:

  • 結(jié)合律: f.(g.h) = (f.g).h 剪返。

  • 封閉律:如果存在態(tài)射 f, g,則必然存在 h = f.g 邓梅。

  • 同一律:對(duì)結(jié)構(gòu)中的每一個(gè)對(duì)象 A, 必須存在一個(gè)單位態(tài)射 Ia: A -> A脱盲, 對(duì)于單位態(tài)射,顯然日缨,對(duì)任意其它態(tài)射 f, 有 f.I = f钱反。

在范疇論里另外研究的重點(diǎn)是范疇與范疇之間的關(guān)系,就正如對(duì)象與對(duì)象之間有態(tài)射一樣,范疇與范疇之間也存在映射關(guān)系面哥,從而可以將一個(gè)范疇映射為另一個(gè)范疇哎壳,這種映射在范疇論中叫作函子(functor),具體來說尚卫,對(duì)于給定的兩個(gè)范疇 A 和 B, 函子的作用有兩個(gè):

  • 將范疇 A 中的對(duì)象映射到范疇 B 中的對(duì)象归榕。
  • 將范疇 A 中的態(tài)射映射到范疇 B 中的態(tài)射。

顯然吱涉,函子反映了不同的范疇之間的內(nèi)在聯(lián)系刹泄。跟函數(shù)和泛函數(shù)的思想是相同的。

而我們的函數(shù)式編程探究的問題與思想理念可以說是跟范疇論完全吻合怎爵。如果把函數(shù)式編程的整個(gè)的世界看做一個(gè)對(duì)象特石,那么FP真正搞的事情就是建立通過函數(shù)之間的映射關(guān)系,來構(gòu)建這樣一個(gè)美麗的編程世界疙咸。

很多問題的解決(證明)其實(shí)都不涉及具體的(數(shù)據(jù))結(jié)構(gòu)县匠,而完全可以只依賴映射之間的組合運(yùn)算(composition)來搞定。這就是函數(shù)式編程的核心思想撒轮。

如果我們把程序看做圖論里面的一張圖G乞旦,數(shù)據(jù)結(jié)構(gòu)當(dāng)作是圖G的節(jié)點(diǎn)Node(數(shù)據(jù)結(jié)構(gòu),存儲(chǔ)狀態(tài))题山, 而算法邏輯就是這些節(jié)點(diǎn)Node之間的Edge (數(shù)據(jù)映射兰粉,Mapping), 那么這整幅圖 G(N,E) 就是一幅美妙的抽象邏輯之塔的 映射圖 顶瞳, 也就是我們編程創(chuàng)造的世界:

Kotlin極簡(jiǎn)教程
Kotlin極簡(jiǎn)教程

函數(shù)是"第一等公民"

函數(shù)式編程(FP)中玖姑,函數(shù)是"第一等公民"。

所謂"第一等公民"(first class)慨菱,有時(shí)稱為 閉包 或者 仿函數(shù)(functor)對(duì)象焰络,指的是函數(shù)與其他數(shù)據(jù)類型一樣,處于平等地位符喝,可以賦值給其他變量闪彼,也可以作為參數(shù),傳入另一個(gè)函數(shù)协饲,或者作為別的函數(shù)的返回值畏腕。這個(gè)以函數(shù)為參數(shù)的概念,跟C語言中的函數(shù)指針類似茉稠。

舉例來說描馅,下面代碼中的print變量就是一個(gè)函數(shù)(沒有函數(shù)名),可以作為另一個(gè)函數(shù)的參數(shù):

>>> val print = fun(x:Any){println(x)}
>>> listOf(1,2,3).forEach(print)
1
2
3

高階函數(shù)(Higher order Function)

FP 語言支持高階函數(shù)而线,高階函數(shù)就是多階映射铭污。高階函數(shù)用另一個(gè)函數(shù)作為其輸入?yún)?shù)恋日,也可以返回一個(gè)函數(shù)作為輸出。

代碼示例:

fun isOdd(x: Int) = x % 2 != 0
fun length(s: String) = s.length

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
    return { x -> f(g(x)) }
}

測(cè)試代碼:

fun main(args: Array<String>) {
    val oddLength = compose(::isOdd, ::length)
    val strings = listOf("a", "ab", "abc")
    println(strings.filter(oddLength)) // [a, abc]
}

這個(gè)compose函數(shù)况凉,其實(shí)就是數(shù)學(xué)中的復(fù)合函數(shù)的概念谚鄙,這是一個(gè)高階函數(shù)的例子:傳入的兩個(gè)參數(shù)f , g都是函數(shù)各拷,其返回值也是函數(shù)刁绒。

圖示如下:

Kotlin極簡(jiǎn)教程
Kotlin極簡(jiǎn)教程

這里的

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C 

中類型參數(shù)對(duì)應(yīng):

fun <String, Int, Boolean> compose(f: (Int) -> Boolean, g: (String) -> Int): (String) -> Boolean

這里的(Int) -> Boolean(String) -> Int烤黍、 (String) -> Boolean 都是函數(shù)類型知市。

其實(shí),從映射的角度看速蕊,就是二階映射嫂丙。對(duì)[a, ab, abc] 中每個(gè)元素 x 先映射成長(zhǎng)度g(x) = 1, 2, 3 , 再進(jìn)行第二次映射:f(g(x)) %2 != 0 , 長(zhǎng)度是奇數(shù)规哲?返回值是true的被過濾出來跟啤。

有了高階函數(shù),我們可以用優(yōu)雅的方式進(jìn)行模塊化編程唉锌。

另外隅肥,高階函數(shù)滿足結(jié)合律:

Kotlin極簡(jiǎn)教程
Kotlin極簡(jiǎn)教程

λ演算 (Lambda calculus 或者 λ-calculus)

?? 演算是函數(shù)式語言的基礎(chǔ)。在λ-演算的基礎(chǔ)上袄简,發(fā)展起來的π-演算腥放、χ-演算,成為近年來的并發(fā)程序的理論工具之一绿语,許多經(jīng)典的并發(fā)程序模型就是以π-演算為框架的秃症。λ 演算神奇之處在于,通過最基本的函數(shù)抽象和函數(shù)應(yīng)用法則吕粹,配套以適當(dāng)?shù)募记芍指蹋隳軌驑?gòu)造出任意復(fù)雜的可計(jì)算函數(shù)。

λ演算是一套用于研究函數(shù)定義匹耕、函數(shù)應(yīng)用和遞歸的形式系統(tǒng)聚请。它由 阿隆佐·丘奇(Alonzo Church,1903~1995)和 Stephen Cole Kleene 在 20 世紀(jì)三十年代引入泌神。當(dāng)時(shí)的背景是解決函數(shù)可計(jì)算的本質(zhì)性問題良漱,初期λ演算成功的解決了在可計(jì)算理論中的判定性問題,后來根據(jù)Church–Turing thesis欢际,證明了λ演算與圖靈機(jī)是等價(jià)的母市。

λ 演算可以被稱為最小的通用程序設(shè)計(jì)語言。它包括一條變換規(guī)則 (變量替換) 和一條函數(shù)定義方式损趋,λ演算之通用在于患久,任何一個(gè)可計(jì)算函數(shù)都能用這種形式來表達(dá)和求值。

λ演算強(qiáng)調(diào)的是變換規(guī)則的運(yùn)用,這里的變換規(guī)則本質(zhì)上就是函數(shù)映射蒋失。

Lambda 表達(dá)式(Lambda Expression) 是 λ演算 的一部分返帕。

λ演算中一切皆函數(shù),全體λ表達(dá)式構(gòu)成Λ空間篙挽,λ表達(dá)式為Λ空間到Λ空間的函數(shù)荆萤。

例如,在 lambda 演算中有許多方式都可以定義自然數(shù)铣卡,最常見的是Church 整數(shù)链韭,定義如下:

0 = λ f. λ x. x
1 = λ f. λ x. f x
2 = λ f. λ x. f (f x)
3 = λ f. λ x. f (f (f x))
...

數(shù)學(xué)家們都崇尚簡(jiǎn)潔,只用一個(gè)關(guān)鍵字 'λ' 來表示對(duì)函數(shù)的抽象煮落。

其中的λ f. λ x.敞峭,λ f 是抽象出來的函數(shù), λ x是輸入?yún)?shù), . 語法用來分割參數(shù)表和函數(shù)體蝉仇。 為了更簡(jiǎn)潔旋讹,我們簡(jiǎn)記為F, 那么上面的Church 整數(shù)定義簡(jiǎn)寫為:

0 = F x
1 = F f x
2 = F f (f x)
3 = F f (f (f x))
...

使用λ演算定義布爾值:

TRUE = λ x. λ y. x
FALSE = λ x. λ y. y

用圖示如下:

Kotlin極簡(jiǎn)教程
Kotlin極簡(jiǎn)教程
Kotlin極簡(jiǎn)教程
Kotlin極簡(jiǎn)教程

在λ演算中只有函數(shù),一門編程語言中的數(shù)據(jù)類型轿衔,比如boolean沉迹、number、list等呀枢,都可以使用純?chǔ)搜菟銇韺?shí)現(xiàn)胚股。我們不用去關(guān)心數(shù)據(jù)的值是什么,重點(diǎn)是我們能對(duì)這個(gè)值做什么操作(apply function)裙秋。

使用λ演算定義一個(gè)恒等函數(shù)I :

 I = λ x . x

使用Kotlin代碼來寫琅拌,如下:

>>> val I = {x:Int -> x}
>>> I(0)
0
>>> I(1)
1
>>> I(100)
100

對(duì) I 而言任何一個(gè) x 都是它的不動(dòng)點(diǎn)(即對(duì)某個(gè)函數(shù) f(x) 存在這樣的一個(gè)輸入 x,使得函數(shù)的輸出仍舊等于輸入的 x 摘刑。形式化的表示即為 f(x) = x )进宝。

再例如,下面的 λ 表達(dá)式表示將x映射為 x+1 :

 λ x . x + 1

測(cè)試代碼:

( λ x . x + 1) 5

將輸出6 枷恕。

這樣的表達(dá)式党晋,在Kotlin中, 如果使用Lambda表達(dá)式我們這樣寫:

>>> val addOneLambda = {
...         x: Int ->
...         x + 1
...     }
>>> addOneLambda(1)
2

如果使用匿名函數(shù),這樣寫:

>>> val addOneAnonymouse = (fun(x: Int): Int {
...         return x + 1
...     })
>>> addOneAnonymouse(1)
2

在一些古老的編程語言中徐块,lambda表達(dá)式還是比較接近lambda演算的表達(dá)式的未玻。在現(xiàn)代程序語言中的lambda表達(dá)式,只是取名自lambda演算胡控,已經(jīng)與原始的lambda演算有很大差別了扳剿。例如:

Kotlin極簡(jiǎn)教程
Kotlin極簡(jiǎn)教程

在Javascript里沒有任何語法專門代表lambda, 只寫成這樣的嵌套函數(shù)function{ return function{...} }

函數(shù)柯里化(Currying)

很多基于 lambda calculus 的程序語言昼激,比如 ML 和 Haskell庇绽,都習(xí)慣用currying 的手法來表示函數(shù)锡搜。比如,如果你在 Haskell 里面這樣寫一個(gè)函數(shù):

f x y = x + y 

然后你就可以這樣把鏈表里的每個(gè)元素加上 2:

map (f 2) [1, 2, 3] 

它會(huì)輸出 [3, 4, 5]瞧掺。

Currying 用一元函數(shù)耕餐,來組合成多元函數(shù)。比如辟狈,上面的函數(shù) f 的定義在 Scheme 里面相當(dāng)于:

(define f (lambda (x) (lambda (y) (+ x y)))) 

它是說肠缔,函數(shù) f,接受一個(gè)參數(shù) x上陕,返回另一個(gè)函數(shù)(沒有名字)桩砰。這個(gè)匿名函數(shù),如果再接受一個(gè)參數(shù) y释簿,就會(huì)返回 x + y。所以上面的例子里面硼莽,(f 2) 返回的是一個(gè)匿名函數(shù)庶溶,它會(huì)把 2 加到自己的參數(shù)上面返回。所以把它 map 到 [1, 2, 3]懂鸵,我們就得到了 [3, 4, 5]偏螺。

我們?cè)偈褂肒otlin中的函數(shù)式編程來舉例說明。

首先匆光,我們看下普通的二元函數(shù)的寫法:

fun add(x: Int, y: Int): Int {
    return x + y
}

add(1, 2) // 輸出3

這種寫法最簡(jiǎn)單套像,只有一層映射。

柯里化的寫法:

fun curryAdd(x: Int): (Int) -> Int {
    return { y -> x + y }
}

curryAdd(1)(2)// 輸出3

我們先傳入?yún)?shù)x = 1终息, 返回函數(shù) curryAdd(1) = 1 + y夺巩;然后傳入?yún)?shù) y = 2, 返回最終的值 curryAdd(1)(2) = 3。

當(dāng)然周崭,我們也有 λ 表達(dá)式的寫法:

val lambdaCurryAdd = {
        x: Int ->
        {
            y: Int ->
            x + y
        }
    }

lambdaCurryAdd(1)(2)  // 輸出 3

這個(gè)做法其實(shí)來源于最早的 lambda calculus 的設(shè)計(jì)柳譬。因?yàn)?lambda calculus 的函數(shù)都只有一個(gè)參數(shù),所以為了能夠表示多參數(shù)的函數(shù)续镇, Haskell Curry (數(shù)學(xué)家和邏輯學(xué)家)美澳,發(fā)明了這個(gè)方法。

不過在編碼實(shí)踐中摸航,Currying 的工程實(shí)用性制跟、簡(jiǎn)潔性上不是那么的友好。大量使用 Currying酱虎,會(huì)導(dǎo)致代碼可讀性降低雨膨,復(fù)雜性增加,并且還可能因此引起意想不到的錯(cuò)誤逢净。 所以在我們的講求工程實(shí)踐性能的Kotlin語言中哥放,

古老而美麗的理論歼指,也許能夠給我?guī)硭枷氲膯⒌希窃诠こ虒?shí)踐中未必那么理想甥雕。

閉包(Closure)

閉包簡(jiǎn)單講就是一個(gè)代碼塊踩身,用{ }包起來。此時(shí)社露,程序代碼也就成了數(shù)據(jù)挟阻,可以被一個(gè)變量所引用(與C語言的函數(shù)指針比較類似)。閉包的最典型的應(yīng)用是實(shí)現(xiàn)回調(diào)函數(shù)(callback)峭弟。

閉包包含以下兩個(gè)組成部分:

  • 要執(zhí)行的代碼塊(由于自由變量被包含在代碼塊中附鸽,這些自由變量以及它們引用的對(duì)象沒有被釋放)
  • 自由變量的作用域

在PHP、Scala瞒瘸、Scheme坷备、Common Lisp、Smalltalk情臭、Groovy省撑、JavaScript、Ruby俯在、 Python竟秫、Go、Lua跷乐、objective c肥败、swift 以及Java(Java8及以上)等語言中都能找到對(duì)閉包不同程度的支持。

Lambda表達(dá)式可以表示閉包愕提。

惰性計(jì)算

除了高階函數(shù)馒稍、閉包、Lambda表達(dá)式的概念揪荣,F(xiàn)P 還引入了惰性計(jì)算的概念筷黔。惰性計(jì)算(盡可能延遲表達(dá)式求值)是許多函數(shù)式編程語言的特性。惰性集合在需要時(shí)提供其元素仗颈,無需預(yù)先計(jì)算它們佛舱,這帶來了一些好處。首先挨决,您可以將耗時(shí)的計(jì)算推遲到絕對(duì)需要的時(shí)候请祖。其次,您可以創(chuàng)造無限個(gè)集合脖祈,只要它們繼續(xù)收到請(qǐng)求肆捕,就會(huì)繼續(xù)提供元素。第三盖高,map 和 filter 等函數(shù)的惰性使用讓您能夠得到更高效的代碼(請(qǐng)參閱 參考資料 中的鏈接慎陵,加入由 Brian Goetz 組織的相關(guān)討論)眼虱。

在惰性計(jì)算中,表達(dá)式不是在綁定到變量時(shí)立即計(jì)算席纽,而是在求值程序需要產(chǎn)生表達(dá)式的值時(shí)進(jìn)行計(jì)算捏悬。

一個(gè)惰性計(jì)算的例子是生成無窮 Fibonacci 列表的函數(shù),但是對(duì) 第 n 個(gè)Fibonacci 數(shù)的計(jì)算相當(dāng)于只是從可能的無窮列表中提取一項(xiàng)润梯。

遞歸函數(shù)

遞歸指的是一個(gè)函數(shù)在其定義中直接或間接調(diào)用自身的一種方法, 它通常把一個(gè)大型的復(fù)雜的問題轉(zhuǎn)化為一個(gè)與原問題相似的規(guī)模較小的問題來解決(復(fù)用函數(shù)自身), 這樣可以極大的減少代碼量过牙。遞歸分為兩個(gè)階段:

  1. 遞推:把復(fù)雜的問題的求解推到比原問題簡(jiǎn)單一些的問題的求解;
  2. 回歸:當(dāng)獲得最簡(jiǎn)單的情況后,逐步返回,依次得到復(fù)雜的解。

遞歸的能力在于用有限的語句來定義對(duì)象的無限集合纺铭。

使用遞歸要注意的有兩點(diǎn):

  1. 遞歸就是在過程或函數(shù)里面調(diào)用自身;
  2. 在使用遞歸時(shí),必須有一個(gè)明確的遞歸結(jié)束條件,稱為遞歸出口寇钉。

下面我們舉例說明。

階乘函數(shù) fact(n) 一般這樣遞歸地定義:

fact(n) = if n=0 then 1 else n * fact(n-1)

我們使用Kotlin代碼實(shí)現(xiàn)這個(gè)函數(shù)如下:

fun factorial(n: Int): Int {
    println("factorial() called!  n=$n")
    if (n == 0) return 1;
    return n * factorial(n - 1);
}

測(cè)試代碼:

@Test
fun testFactorial() {
    Assert.assertTrue(factorial(0) == 1)
    Assert.assertTrue(factorial(1) == 1)
    Assert.assertTrue(factorial(3) == 6)
    Assert.assertTrue(factorial(10) == 3628800)
}

輸出:

factorial() called!  n=0
factorial() called!  n=1
factorial() called!  n=0
factorial() called!  n=3
factorial() called!  n=2
factorial() called!  n=1
factorial() called!  n=0
factorial() called!  n=10
factorial() called!  n=9
factorial() called!  n=8
factorial() called!  n=7
factorial() called!  n=6
factorial() called!  n=5
factorial() called!  n=4
factorial() called!  n=3
factorial() called!  n=2
factorial() called!  n=1
factorial() called!  n=0
BUILD SUCCESSFUL in 24s
6 actionable tasks: 5 executed, 1 up-to-date

我們可以看到在factorial計(jì)算的過程中舶赔,函數(shù)不斷的調(diào)用自身扫倡,然后不斷的展開,直到最后到達(dá)了終止的n==0顿痪,這是遞歸的原則之一镊辕,就是在遞歸的過程中,傳遞的參數(shù)一定要不斷的接近終止條件蚁袭,在上面的例子中就是n的值不斷減少,直至最后為0石咬。

再舉個(gè)Fibonacci數(shù)列的例子揩悄。

Fibonacci數(shù)列用數(shù)學(xué)中的數(shù)列的遞歸表達(dá)式定義如下:

fibonacci (0) = 0
fibonacci (1) = 1
fibonacci (n) = fibonacci (n - 1) + fibonacci (n - 2)

我們使用Kotlin代碼實(shí)現(xiàn)它:

fun fibonacci(n: Int): Int {
    if (n == 1 || n == 2) return 1;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

測(cè)試代碼:

@Test
fun testFibonacci() {
    Assert.assertTrue(fibonacci(1) == 1)
    Assert.assertTrue(fibonacci(2) == 1)
    Assert.assertTrue(fibonacci(3) == 2)
    Assert.assertTrue(fibonacci(4) == 3)
    Assert.assertTrue(fibonacci(5) == 5)
    Assert.assertTrue(fibonacci(6) == 8)
}

外篇: Scheme中的遞歸寫法

因?yàn)镾cheme 程序中充滿了一對(duì)對(duì)嵌套的小括號(hào),這些嵌套的符號(hào)體現(xiàn)了最基本的數(shù)學(xué)思想——遞歸鬼悠。所以删性,為了多維度的來理解遞歸,我們給出Scheme中的遞歸寫法:

(define factorial
  (lambda (n)
    (if (= n 0)
        1
        (* n (factorial (- n 1))))))

(define fibonacci
  (lambda (n)
    (cond ((= n 0) 0)
          ((= n 1) 1)
          (else (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))))

其中關(guān)鍵字lambda, 表明我們定義的(即任何封閉的開括號(hào)立即離開λ及其相應(yīng)的關(guān)閉括號(hào))是一個(gè)函數(shù)焕窝。

Lambda演算和函數(shù)式語言的計(jì)算模型天生較為接近蹬挺,Lambda表達(dá)式一般是這些語言必備的基本特性。

Scheme是Lisp方言它掂,遵循極簡(jiǎn)主義哲學(xué)巴帮,有著獨(dú)特的魅力。Scheme的一個(gè)主要特性是可以像操作數(shù)據(jù)一樣操作函數(shù)調(diào)用虐秋。

Y組合子(Y - Combinator)

在現(xiàn)代編程語言中榕茧,函數(shù)都是具名的,而在傳統(tǒng)的Lambda Calculus中客给,函數(shù)都是沒有名字的用押。這樣就出現(xiàn)了一個(gè)問題 —— 如何在Lambda Calculus中實(shí)現(xiàn)遞歸函數(shù),即匿名遞歸函數(shù)靶剑。Haskell B. Curry (編程語言 Haskell 就是以此人命名的)發(fā)現(xiàn)了一種不動(dòng)點(diǎn)組合子 —— Y Combinator蜻拨,用于解決匿名遞歸函數(shù)實(shí)現(xiàn)的問題池充。Y 組合子(Y Combinator),其定義是:

Y = λf.(λx.f (x x)) (λx.f (x x))

對(duì)于任意函數(shù) g缎讼,可以通過推導(dǎo)得到Y g = g (Y g) ((高階)函數(shù)的不動(dòng)點(diǎn) )收夸,從而證明 λ演算圖靈完備 的。 Y 組合子 的重要性由此可見一斑休涤。

她讓人絞盡腦汁咱圆,也琢磨不定!她讓人心力憔悴功氨,又百般回味序苏!
她,看似平淡捷凄,卻深藏玄機(jī)忱详!她,貌不驚人跺涤,卻天下無敵匈睁!
她是誰?她就是 Y 組合子:Y = λf.(λx.f (x x)) (λx.f (x x))桶错,不動(dòng)點(diǎn)組合子中最著名的一個(gè)航唆。

Y 組合子讓我們可以定義匿名的遞歸函數(shù)。Y組合子是Lambda演算的一部分院刁,也是函數(shù)式編程的理論基礎(chǔ)糯钙。僅僅通過Lambda表達(dá)式這個(gè)最基本的 原子 實(shí)現(xiàn)循環(huán)迭代。Y 組合子本身是函數(shù)退腥,其輸入也是函數(shù)(在 Lisp 中連程序都是函數(shù))任岸。

頗有道生一、一生二狡刘、二生三享潜、三生萬物的韻味。

舉個(gè)例子說明: 我們先使用類C語言中較為熟悉的JavaScript來實(shí)現(xiàn)一個(gè)Y組合子函數(shù)嗅蔬, 因?yàn)镴avaScript語言的動(dòng)態(tài)特性剑按,使得該實(shí)現(xiàn)相比許多需要聲明各種類型的語言要簡(jiǎn)潔許多:

function Y(f) {
    return (function (g) {
        return g(g);
    })(function (g) {
        return f(function (x) {
            return g(g)(x);
        });
    });
}

var fact = Y(function (rec) {
    return function (n) {
        return n == 0 ? 1 : n * rec(n - 1);
    };
});

我們使用了Y函數(shù)組合一段匿名函數(shù)代碼,實(shí)現(xiàn)了一個(gè)匿名的遞歸階乘函數(shù)购城。

直接將這兩個(gè)函數(shù)放到瀏覽器的Console中去執(zhí)行吕座,我們將看到如下輸出:

fact(10)
3628800
Kotlin極簡(jiǎn)教程
Kotlin極簡(jiǎn)教程

這個(gè)Y函數(shù)相當(dāng)繞腦。要是在Clojure(JVM上的Lisp方言)中瘪板,這個(gè)Y函數(shù)實(shí)現(xiàn)如下:

(defn Y [r]
 ((fn [f] (f f))
 (fn [f]
 (r (fn [x] ((f f) x))))))

使用Scheme語言來表達(dá):

(define Y 
  (lambda (f)
    ((lambda (x) (f (lambda (y) ((x x) y))))
     (lambda (x) (f (lambda (y) ((x x) y)))))))

我們可以看出吴趴,使用Scheme語言表達(dá)的Y組合子跟 原生的 λ演算 表達(dá)式基本一樣。

用CoffeeScript實(shí)現(xiàn)一個(gè) Y combinator就長(zhǎng)這樣:

coffee> Y = (f) -> ((x) -> (x x)) ((x) -> (f ((y) -> ((x x) y))))
[Function]

這個(gè)看起就相當(dāng)簡(jiǎn)潔優(yōu)雅了侮攀。我們使用這個(gè) Y combinator 實(shí)現(xiàn)一個(gè)匿名遞歸的Fibonacci函數(shù):

coffee> fib = Y (f) -> (n) ->  if n < 2 then n else f(n-1) + f(n-2)
[Function]
coffee> index = [0..10]
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
coffee> index.map(fib)
[ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 ]

實(shí)現(xiàn)一個(gè)匿名遞歸階乘函數(shù):

coffee> fact = Y (f)  ->(n) -> if n==0 then 1 else n*f(n-1)
[Function]
coffee> fact(10)
3628800

上面的Coffee代碼的命令行REPL運(yùn)行環(huán)境搭建非常簡(jiǎn)單:

$ npm install -g coffee-script
$ coffee
coffee>

對(duì)CoffeeScript感興趣的讀者锣枝,可以參考:http://coffee-script.org/厢拭。

但是,這個(gè)Y組合子 要是 使用 OOP 語言編程范式撇叁, 就要顯得復(fù)雜許多供鸠。為了更加深刻地認(rèn)識(shí)OOP 與 FP編程范式,我們使用Java 8 以及 Kotlin 的實(shí)例來說明陨闹。這里使用Java給出示例的原因楞捂,是為了給出Kotlin與Java語言上的對(duì)比,在下一章節(jié)中趋厉,我們將要學(xué)習(xí)Kotlin與Java的互操作寨闹。

首先我們使用Java的匿名內(nèi)部類實(shí)現(xiàn)Y組合子 :

package com.easy.kotlin;

public class YCombinator {
    public static Lambda<Lambda> yCombinator(final Lambda<Lambda> f) {
        return new Lambda<Lambda>() {
            @Override
            public Lambda call(Object input) {
                final Lambda<Lambda> u = (Lambda<Lambda>)input;
                return u.call(u);
            }
        }.call(new Lambda<Lambda>() {
            @Override
            public Lambda call(Object input) {
                final Lambda<Lambda> x = (Lambda<Lambda>)input;
                return f.call(new Lambda<Object>() {
                    @Override
                    public Object call(Object input) {
                        return x.call(x).call(input);
                    }
                });
            }
        });
    }

    public static void main(String[] args) {
        Lambda<Lambda> y = yCombinator(new Lambda<Lambda>() {
            @Override
            public Lambda call(Object input) {
                final Lambda<Integer> fab = (Lambda<Integer>)input;
                return new Lambda<Integer>() {
                    @Override
                    public Integer call(Object input) {
                        Integer n = Integer.parseInt(input.toString());
                        if (n < 2) {
                            return Integer.valueOf(1);
                        } else {
                            return n * fab.call(n - 1);
                        }
                    }
                };
            }
        });
        System.out.println(y.call(10));//輸出: 3628800
    }

    interface Lambda<E> {
        E call(Object input);
    }
}

這里定義了一個(gè)Lambda<E>類型, 然后通過E call(Object input)方法實(shí)現(xiàn)自調(diào)用君账,方法實(shí)現(xiàn)里有多處轉(zhuǎn)型以及嵌套調(diào)用繁堡。邏輯比較繞,代碼可讀性也比較差乡数。當(dāng)然椭蹄,這個(gè)問題本身也比較復(fù)雜。

我們使用Java 8的Lambda表達(dá)式來改寫下匿名內(nèi)部類:

package com.easy.kotlin;

public class YCombinator2 {

    public static Lambda<Lambda> yCombinator2(final Lambda<Lambda> f) {
        return ((Lambda<Lambda>)(Object input) -> {
            final Lambda<Lambda> u = (Lambda<Lambda>)input;
            return u.call(u);
        }).call(
            ((Lambda<Lambda>)(Object input) -> {
                final Lambda<Lambda> v = (Lambda<Lambda>)input;
                return f.call((Lambda<Object>)(Object p) -> {
                    return v.call(v).call(p);
                });
            })
        );

    }

    public static void main(String[] args) {
        Lambda<Lambda> y2 = yCombinator2(
            (Lambda<Lambda>)(Object input) -> {
                Lambda<Integer> fab = (Lambda<Integer>)input;
                return (Lambda<Integer>)(Object p) -> {
                    Integer n = Integer.parseInt(p.toString());
                    if (n < 2) {
                        return Integer.valueOf(1);
                    } else {
                        return n * fab.call(n - 1);
                    }
                };
            });

        System.out.println(y2.call(10));//輸出: 3628800
    }

    interface Lambda<E> {
        E call(Object input);
    }

}

最后净赴,我們使用Kotlin的對(duì)象表達(dá)式(順便復(fù)習(xí)回顧一下上一章節(jié)的相關(guān)內(nèi)容)實(shí)現(xiàn)Y組合子:

package com.easy.kotlin

// lambda f. (lambda x. (f(x x)) lambda x. (f(x x)))
object YCombinatorKt {

    fun yCombinator(f: Lambda<Lambda<*>>): Lambda<Lambda<*>> {

        return object : Lambda<Lambda<*>> {

            override fun call(n: Any): Lambda<*> {
                val u = n as Lambda<Lambda<*>>
                return u.call(u)
            }
        }.call(object : Lambda<Lambda<*>> {

            override fun call(n: Any): Lambda<*> {
                val x = n as Lambda<Lambda<*>>

                return f.call(object : Lambda<Any> {
                    override fun call(n: Any): Any {
                        return x.call(x).call(n)!!
                    }
                })
            }

        }) as Lambda<Lambda<*>>
    }

    @JvmStatic fun main(args: Array<String>) {

        val y = yCombinator(object : Lambda<Lambda<*>> {

            override fun call(n: Any): Lambda<*> {
                val fab = n as Lambda<Int>

                return object : Lambda<Int> {

                    override fun call(n: Any): Int {
                        val n = Integer.parseInt(n.toString())
                        if (n < 2) {
                            return Integer.valueOf(1)
                        } else {
                            return n * fab.call(n - 1)
                        }
                    }
                }
            }
        })

        println(y.call(10)) //輸出: 3628800
    }

    interface Lambda<E> {
        fun call(n: Any): E
    }
}

關(guān)于Y combinator的更多實(shí)現(xiàn)绳矩,可以參考:https://gist.github.com/Jason-Chen-2017/88e13b63fa5b7c612fddf999739964b0 ; 另外玖翅,關(guān)于Y combinator的原理介紹埋酬,推薦看《The Little Schemer 》這本書。

從上面的例子烧栋,我們可以看出OOP中的對(duì)接口以及多態(tài)類型,跟FP中的函數(shù)的思想表達(dá)的拳球,本質(zhì)上是一個(gè)東西审姓,這個(gè)東西到底是什么呢?我們姑且稱之為“編程之道”罷祝峻!

Y combinator 給我們提供了一種方法魔吐,讓我們?cè)谝粋€(gè)只支持first-class函數(shù),但是沒有內(nèi)建遞歸的編程語言里完成遞歸莱找。所以Y combinator給我們展示了一個(gè)語言完全可以定義遞歸函數(shù)酬姆,即使這個(gè)語言的定義一點(diǎn)也沒提到遞歸。它給我們展示了一件美妙的事:僅僅函數(shù)式編程自己奥溺,就可以讓我們做到我們從來不認(rèn)為可以做到的事(而且還不止這一個(gè)例子)辞色。

嚴(yán)謹(jǐn)而精巧的lambda演算體系,從最基本的概念“函數(shù)”入手浮定,創(chuàng)造出一個(gè)絢爛而宏偉的世界相满,這不能不說是人類思維的驕傲层亿。

沒有"副作用"

Kotlin極簡(jiǎn)教程
Kotlin極簡(jiǎn)教程

所謂"副作用"(side effect),指的是函數(shù)內(nèi)部與外部互動(dòng)(最典型的情況立美,就是修改全局變量的值)匿又,產(chǎn)生運(yùn)算以外的其他結(jié)果。

函數(shù)式編程強(qiáng)調(diào)沒有"副作用"建蹄,意味著函數(shù)要保持獨(dú)立碌更,所有功能就是返回一個(gè)新的值,沒有其他行為洞慎,尤其是不得修改外部變量的值痛单。

函數(shù)式編程的動(dòng)機(jī),一開始就是為了處理運(yùn)算(computation)拢蛋,不考慮系統(tǒng)的讀寫(I/O)桦他。"語句"屬于對(duì)系統(tǒng)的讀寫操作,所以就被排斥在外谆棱。

當(dāng)然快压,實(shí)際應(yīng)用中,不做I/O是不可能的垃瞧。因此蔫劣,編程過程中,函數(shù)式編程只要求把I/O限制到最小个从,不要有不必要的讀寫行為脉幢,保持計(jì)算過程的單純性。

函數(shù)式編程只是返回新的值嗦锐,不修改系統(tǒng)變量嫌松。因此,不修改變量奕污,也是它的一個(gè)重要特點(diǎn)萎羔。

在其他類型的語言中,變量往往用來保存"狀態(tài)"(state)碳默。不修改變量贾陷,意味著狀態(tài)不能保存在變量中。函數(shù)式編程使用參數(shù)保存狀態(tài)嘱根,最好的例子就是遞歸髓废。

引用透明性

函數(shù)程序通常還加強(qiáng)引用透明性,即如果提供同樣的輸入该抒,那么函數(shù)總是返回同樣的結(jié)果慌洪。就是說,表達(dá)式的值不依賴于可以改變值的全局狀態(tài)。這樣我們就可以從形式上邏輯推斷程序行為蒋譬。因?yàn)楸磉_(dá)式的意義只取決于其子表達(dá)式而不是計(jì)算順序或者其他表達(dá)式的副作用割岛。這有助于我們來驗(yàn)證代碼正確性、簡(jiǎn)化算法犯助,有助于找出優(yōu)化它的方法癣漆。

8.2 在Kotlin中使用函數(shù)式編程

好了親,前文中我們?cè)诤瘮?shù)式編程的世界里遨游了一番剂买,現(xiàn)在我們把思緒收回來惠爽,放到在Kotlin中的函數(shù)式編程中來档桃。

嚴(yán)格的面向?qū)ο蟮挠^點(diǎn)虽画,使得很多問題的解決方案變得較為笨拙。為了將一行有用的代碼包裝到Runnable或者Callable 這兩個(gè)Java中最流行的函數(shù)式示例中妓美,我們不得不去寫五六行模板范例代碼坐慰。為了讓事情簡(jiǎn)單化(在Java 8中较性,增加Lambda表達(dá)式的支持),我們?cè)贙otlin中使用普通的函數(shù)來替代函數(shù)式接口结胀。事實(shí)上赞咙,函數(shù)式編程中的函數(shù),比C語言中的函數(shù)或者Java中的方法都要強(qiáng)大的多糟港。

在Kotlin中攀操,支持函數(shù)作為一等公民。它支持高階函數(shù)秸抚、Lambda表達(dá)式等速和。我們不僅可以把函數(shù)當(dāng)做普通變量一樣傳遞、返回剥汤,還可以把它分配給變量颠放、放進(jìn)數(shù)據(jù)結(jié)構(gòu)或者進(jìn)行一般性的操作。它們可以是未經(jīng)命名的吭敢,也就是匿名函數(shù)慈迈。我們也可以直接把一段代碼丟到 {}中,這就是閉包省有。

在前面的章節(jié)中,其實(shí)我們已經(jīng)涉及到一些關(guān)于函數(shù)的地方谴麦,我們將在這里系統(tǒng)地學(xué)習(xí)一下Kotlin的函數(shù)式編程蠢沿。

8.2.1 Kotlin中的函數(shù)

首先,我們來看下Kotlin中函數(shù)的概念匾效。

函數(shù)聲明

Kotlin 中的函數(shù)使用 fun 關(guān)鍵字聲明

fun double(x: Int): Int {
    return 2*x
}

函數(shù)用法

調(diào)用函數(shù)使用傳統(tǒng)的方法

fun test() {
    val doubleTwo = double(2)
    println("double(2) = $doubleTwo")
}

輸出:double(2) = 4

調(diào)用成員函數(shù)使用點(diǎn)表示法

object FPBasics {

    fun double(x: Int): Int {
        return 2 * x
    }

    fun test() {
        val doubleTwo = double(2)
        println("double(2) = $doubleTwo")
    }
}

fun main(args: Array<String>) {
    FPBasics.test()
}

我們這里直接用object對(duì)象FPBasics來演示舷蟀。

8.2.2 擴(kuò)展函數(shù)

通過 擴(kuò)展 聲明完成一個(gè)類的新功能 擴(kuò)展 ,而無需繼承該類或使用設(shè)計(jì)模式(例如,裝飾者模式)野宜。

一個(gè)擴(kuò)展String類的swap函數(shù)的例子:

fun String.swap(index1: Int, index2: Int): String {
    val charArray = this.toCharArray()
    val tmp = charArray[index1]
    charArray[index1] = charArray[index2]
    charArray[index2] = tmp

    return charArrayToString(charArray)
}

fun charArrayToString(charArray: CharArray): String {
    var result = ""
    charArray.forEach { it -> result = result + it }
    return result
}

這個(gè) this 關(guān)鍵字在擴(kuò)展函數(shù)內(nèi)部對(duì)應(yīng)到接收者對(duì)象(傳過來的在點(diǎn)符號(hào)前的對(duì)象)扫步。 現(xiàn)在,我們對(duì)任意 String 調(diào)用該函數(shù)了:

val str = "abcd"
val swapStr = str.swap(0, str.lastIndex)
println("str.swap(0, str.lastIndex) = $swapStr")

輸出: str.swap(0, str.lastIndex) = dbca

8.2.3 中綴函數(shù)

在以下場(chǎng)景中匈子,函數(shù)還可以用中綴表示法調(diào)用:

  • 成員函數(shù)或擴(kuò)展函數(shù)
  • 只有一個(gè)參數(shù)
  • infix 關(guān)鍵字標(biāo)注

例如河胎,給 Int 定義擴(kuò)展

infix fun Int.shl(x: Int): Int {
 ...
}

用中綴表示法調(diào)用擴(kuò)展函數(shù):

1 shl 2

等同于這樣

1.shl(2)

8.2.4 函數(shù)參數(shù)

函數(shù)參數(shù)使用 Pascal 表示法定義,即 name: type虎敦。參數(shù)用逗號(hào)隔開游岳。每個(gè)參數(shù)必須顯式指定其類型。

fun powerOf(number: Int, exponent: Int): Int {
    return Math.pow(number.toDouble(), exponent.toDouble()).toInt()
}

測(cè)試代碼:

val eight = powerOf(2, 3)
println("powerOf(2,3) = $eight")

輸出:powerOf(2,3) = 8

默認(rèn)參數(shù)

函數(shù)參數(shù)可以有默認(rèn)值其徙,當(dāng)省略相應(yīng)的參數(shù)時(shí)使用默認(rèn)值胚迫。這可以減少重載數(shù)量。

fun add(x: Int = 0, y: Int = 0): Int {
    return x + y
}

默認(rèn)值通過類型后面的 = 及給出的值來定義唾那。

測(cè)試代碼:

val zero = add()
val one = add(1)
val two = add(1, 1)
println("add() = $zero")
println("add(1) = $one")
println("add(1, 1) = $two")

輸出:

add() = 0
add(1) = 1
add(1, 1) = 2

另外访锻,覆蓋帶默認(rèn)參數(shù)的函數(shù)時(shí),總是使用與基類型方法相同的默認(rèn)參數(shù)值闹获。
當(dāng)覆蓋一個(gè)帶有默認(rèn)參數(shù)值的方法時(shí),簽名中不帶默認(rèn)參數(shù)值:

open class DefaultParamBase {
    open fun add(x: Int = 0, y: Int = 0): Int {
        return x + y
    }
}

class DefaultParam : DefaultParamBase() {
    override fun add(x: Int, y: Int): Int { // 不能有默認(rèn)值
        return super.add(x, y)
    }
}

命名參數(shù)

可以在調(diào)用函數(shù)時(shí)使用命名的函數(shù)參數(shù)昌罩。當(dāng)一個(gè)函數(shù)有大量的參數(shù)或默認(rèn)參數(shù)時(shí)這會(huì)非常方便茎用。

給定以下函數(shù)

fun reformat(str: String,
    normalizeCase: Boolean = true,
    upperCaseFirstLetter: Boolean = true,
    divideByCamelHumps: Boolean = false,
    wordSeparator: Char = ' ') {
}

我們可以使用默認(rèn)參數(shù)來調(diào)用它

reformat(str)

然而遣总,當(dāng)使用非默認(rèn)參數(shù)調(diào)用它時(shí),該調(diào)用看起來就像

reformat(str, true, true, false, '_')

使用命名參數(shù)我們可以使代碼更具有可讀性

reformat(str,
    normalizeCase = true,
    upperCaseFirstLetter = true,
    divideByCamelHumps = false,
    wordSeparator = '_'
)

并且如果我們不需要所有的參數(shù)

reformat(str, wordSeparator = '_')

可變數(shù)量的參數(shù)(Varargs)

函數(shù)的參數(shù)(通常是最后一個(gè))可以用 vararg 修飾符標(biāo)記:

fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // ts is an Array
        result.add(t)
    return result
}

允許將可變數(shù)量的參數(shù)傳遞給函數(shù):

val list = asList(1, 2, 3)

8.2.5 函數(shù)返回類型

函數(shù)返回類型需要顯式聲明

具有塊代碼體的函數(shù)必須始終顯式指定返回類型轨功,除非他們旨在返回 Unit旭斥。

Kotlin 不推斷具有塊代碼體的函數(shù)的返回類型,因?yàn)檫@樣的函數(shù)在代碼體中可能有復(fù)雜的控制流古涧,并且返回類型對(duì)于讀者(有時(shí)對(duì)于編譯器)也是不明顯的垂券。

返回 Unit 的函數(shù)

如果一個(gè)函數(shù)不返回任何有用的值,它的返回類型是 Unit羡滑。Unit 是一種只有一個(gè)Unit 值的類型菇爪。這個(gè)值不需要顯式返回:

fun printHello(name: String?): Unit {
    if (name != null)
        println("Hello ${name}")
    else
        println("Hi there!")
    // `return Unit` 或者 `return` 是可選的
}

Unit 返回類型聲明也是可選的。上面的代碼等同于

fun printHello(name: String?) {
    .....
}

8.2.6 單表達(dá)式函數(shù)

當(dāng)函數(shù)返回單個(gè)表達(dá)式時(shí)柒昏,可以省略花括號(hào)并且在 = 符號(hào)之后指定代碼體即可

fun double(x: Int): Int = x * 2

當(dāng)返回值類型可由編譯器推斷時(shí)凳宙,顯式聲明返回類型是可選的:

fun double(x: Int) = x * 2

8.2.7 函數(shù)作用域

在 Kotlin 中函數(shù)可以在文件頂層聲明,這意味著你不需要像一些語言如 Java职祷、C# 或 Scala 那樣創(chuàng)建一個(gè)類來保存一個(gè)函數(shù)氏涩。此外除了頂層函數(shù)届囚,Kotlin 中函數(shù)也可以聲明在局部作用域、作為成員函數(shù)以及擴(kuò)展函數(shù)是尖。

局部函數(shù)(嵌套函數(shù))

Kotlin 支持局部函數(shù)意系,即一個(gè)函數(shù)在另一個(gè)函數(shù)內(nèi)部

fun sum(x: Int, y: Int, z: Int): Int {
    val delta = 0;
    fun add(a: Int, b: Int): Int {
        return a + b + delta
    }
    return add(x + add(y, z))
}

局部函數(shù)可以訪問外部函數(shù)(即閉包)中的局部變量delta。

println("sum(1,2,3) = ${sum(0, 1, 2, 3)}")

輸出:sum(1,2,3) = 6

成員函數(shù)

成員函數(shù)是在類或?qū)ο髢?nèi)部定義的函數(shù)

class Sample() {
    fun foo() { print("Foo") }
}

成員函數(shù)以點(diǎn)表示法調(diào)用

Sample().foo() // 創(chuàng)建類 Sample 實(shí)例并調(diào)用 foo

8.2.8 泛型函數(shù)

函數(shù)可以有泛型參數(shù)饺汹,通過在函數(shù)名前使用尖括號(hào)指定蛔添。

例如Iterable的map函數(shù):

public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}

8.2.9 高階函數(shù)

高階函數(shù)是將函數(shù)用作參數(shù)或返回值的函數(shù)。例如首繁,Iterable的filter函數(shù):

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

它的輸入?yún)?shù)predicate: (T) -> Boolean就是一個(gè)函數(shù)作郭。其中,函數(shù)類型聲明的語法是:

(X)->Y

表示這個(gè)函數(shù)是從類型X到類型Y的映射弦疮。即這個(gè)函數(shù)輸入X類型夹攒,輸出Y類型。

這個(gè)函數(shù)我們這樣調(diào)用:

fun isOdd(x: Int): Boolean {
    return x % 2 == 1
}

val list = listOf(1, 2, 3, 4, 5)
list.filter(::isOdd)

其中胁塞,::用來引用一個(gè)函數(shù)咏尝。

8.2.10 匿名函數(shù)

我們也可以使用匿名函數(shù)來實(shí)現(xiàn)這個(gè)predicate函數(shù):

list.filter((fun(x: Int): Boolean {
    return x % 2 == 1
}))

8.2.11 Lambda 表達(dá)式

我們也可以直接使用更簡(jiǎn)單的Lambda表達(dá)式來實(shí)現(xiàn)一個(gè)predicate函數(shù):

list.filter {
    it % 2 == 1
}
  • lambda 表達(dá)式總是被大括號(hào) {} 括著
  • 其參數(shù)(如果有的話)在 -> 之前聲明(參數(shù)類型可以省略)
  • 函數(shù)體(如果存在的話)在 -> 后面

上面的寫法跟:

list.filter({
    it % 2 == 1
})

等價(jià),如果 lambda 是該調(diào)用的唯一參數(shù)啸罢,則調(diào)用中的圓括號(hào)可以省略编检。

使用Lambda表達(dá)式定義一個(gè)函數(shù)字面值:

>>> val sum = { x: Int, y: Int -> x + y }
>>> sum(1,1)
2

我們?cè)谑褂们短椎腖ambda表達(dá)式來定義一個(gè)柯里化的sum函數(shù):

>>> val sum = {x:Int ->  {y:Int -> x+y }}
>>> sum
(kotlin.Int) -> (kotlin.Int) -> kotlin.Int
>>> sum(1)(1)
2

8.2.11 it:?jiǎn)蝹€(gè)參數(shù)的隱式名稱

Kotlin中另一個(gè)有用的約定是,如果函數(shù)字面值只有一個(gè)參數(shù)扰才,
那么它的聲明可以省略(連同 ->)允懂,其名稱是 it

代碼示例:

>>> val list = listOf(1,2,3,4,5)
>>> list.map { it * 2 }
[2, 4, 6, 8, 10]

8.2.12 閉包(Closure)

Lambda 表達(dá)式或者匿名函數(shù)衩匣,以及局部函數(shù)和對(duì)象表達(dá)式(object declarations)可以訪問其 閉包 蕾总,即在外部作用域中聲明的變量。 與 Java 不同的是可以修改閉包中捕獲的變量:

fun sumGTZero(c: Iterable<Int>): Int {
    var sum = 0
    c.filter { it > 0 }.forEach {
        sum += it
    }
    return sum
}

val list = listOf(1, 2, 3, 4, 5)
sumGTZero(list) // 輸出 15

我們?cè)偈褂瞄]包來寫一個(gè)使用Java中的Thread接口的例子:

fun closureDemo() {
    Thread({
        for (i in 1..10) {
            println("I = $i")
            Thread.sleep(1000)
        }

    }).start()

    Thread({
        for (j in 10..20) {
            println("J = $j")
            Thread.sleep(2000)
        }
        Thread.sleep(1000)
    }).start()
}

一個(gè)輸出:

I = 1
J = 10
I = 2
I = 3
...
J = 20

8.2.13 帶接收者的函數(shù)字面值

Kotlin 提供了使用指定的 接收者對(duì)象 調(diào)用函數(shù)字面值的功能琅捏。

使用匿名函數(shù)的語法生百,我們可以直接指定函數(shù)字面值的接收者類型。

下面我們使用帶接收者的函數(shù)類型聲明一個(gè)變量柄延,并在之后使用它蚀浆。代碼示例:

>>> val sum = fun Int.(other: Int): Int = this + other
>>> 1.sum(1)
2

當(dāng)接收者類型可以從上下文推斷時(shí),lambda 表達(dá)式可以用作帶接收者的函數(shù)字面值搜吧。

class HTML {
    fun body() {
        println("HTML BODY")
    }
}

fun html(init: HTML.() -> Unit): HTML { // HTML.()中的HTML是接受者類型
    val html = HTML()  // 創(chuàng)建接收者對(duì)象
    html.init()        // 將該接收者對(duì)象傳給該 lambda
    return html
}

測(cè)試代碼:

html {
    body()
}

輸出:HTML BODY

使用這個(gè)特性市俊,我們可以構(gòu)建一個(gè)HTML的DSL語言。

8.2.14 具體化的類型參數(shù)

有時(shí)候我們需要訪問一個(gè)參數(shù)類型:

fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
    var p = parent
    while (p != null && !clazz.isInstance(p)) {
        p = p.parent
    }
    @Suppress("UNCHECKED_CAST")
    return p as T?
}

在這里我們向上遍歷一棵樹并且檢查每個(gè)節(jié)點(diǎn)是不是特定的類型滤奈。
這都沒有問題秕衙,但是調(diào)用處不是很優(yōu)雅:

treeNode.findParentOfType(MyTreeNode::class.java)

我們真正想要的只是傳一個(gè)類型給該函數(shù),即像這樣調(diào)用它:

treeNode.findParentOfType<MyTreeNode>()

為能夠這么做僵刮,內(nèi)聯(lián)函數(shù)支持具體化的類型參數(shù)据忘,于是我們可以這樣寫:

inline fun <reified T> TreeNode.findParentOfType(): T? {
    var p = parent
    while (p != null && p !is T) {
        p = p.parent
    }
    return p as T?
}

我們使用 reified 修飾符來限定類型參數(shù),現(xiàn)在可以在函數(shù)內(nèi)部訪問它了搞糕,
幾乎就像是一個(gè)普通的類一樣勇吊。由于函數(shù)是內(nèi)聯(lián)的,不需要反射窍仰,正常的操作符如 !isas 現(xiàn)在都能用了汉规。

雖然在許多情況下可能不需要反射,但我們?nèi)匀豢梢詫?duì)一個(gè)具體化的類型參數(shù)使用它:

inline fun <reified T> membersOf() = T::class.members

fun main(s: Array<String>) {
    println(membersOf<StringBuilder>().joinToString("\n"))
}

普通的函數(shù)(未標(biāo)記為內(nèi)聯(lián)函數(shù)的)沒有具體化參數(shù)驹吮。

8.2.10 尾遞歸tailrec

Kotlin 支持一種稱為尾遞歸的函數(shù)式編程風(fēng)格针史。 這允許一些通常用循環(huán)寫的算法改用遞歸函數(shù)來寫,而無堆棧溢出的風(fēng)險(xiǎn)碟狞。 當(dāng)一個(gè)函數(shù)用 tailrec 修飾符標(biāo)記并滿足所需的形式時(shí)啄枕,編譯器會(huì)優(yōu)化該遞歸,生成一個(gè)快速而高效的基于循環(huán)的版本族沃。

tailrec fun findFixPoint(x: Double = 1.0): Double
        = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x)) // 函數(shù)必須將其自身調(diào)用作為它執(zhí)行的最后一個(gè)操作

這段代碼計(jì)算余弦的不動(dòng)點(diǎn)(fixpoint of cosine)频祝,這是一個(gè)數(shù)學(xué)常數(shù)。 它只是重復(fù)地從 1.0 開始調(diào)用 Math.cos脆淹,直到結(jié)果不再改變常空,產(chǎn)生0.7390851332151607的結(jié)果。最終代碼相當(dāng)于這種更傳統(tǒng)風(fēng)格的代碼:

private fun findFixPoint(): Double {
    var x = 1.0
    while (true) {
        val y = Math.cos(x)
        if (x == y) return y
        x = y
    }
}

要符合 tailrec 修飾符的條件的話盖溺,函數(shù)必須將其自身調(diào)用作為它執(zhí)行的最后一個(gè)操作漓糙。在遞歸調(diào)用后有更多代碼時(shí),不能使用尾遞歸烘嘱,并且不能用在 try/catch/finally 塊中昆禽。尾部遞歸在 JVM 后端中支持。

Kotlin 還為集合類引入了許多擴(kuò)展函數(shù)拙友。例如为狸,使用 map() 和 filter() 函數(shù)可以流暢地操縱數(shù)據(jù),具體的函數(shù)的使用以及示例我們已經(jīng)在 集合類 章節(jié)中介紹遗契。

本章小結(jié)

本章我們一起學(xué)習(xí)了函數(shù)式編程的簡(jiǎn)史辐棒、Lambda演算、Y組合子與遞歸等核心函數(shù)式的編程思想等相關(guān)內(nèi)容牍蜂。然后重點(diǎn)介紹了在Kotlin中如何使用函數(shù)式風(fēng)格編程漾根,其中重點(diǎn)介紹了Kotlin中函數(shù)的相關(guān)知識(shí),以及高階函數(shù)鲫竞、Lambda表達(dá)式辐怕、閉包等核心語法,并給出相應(yīng)的實(shí)例說明从绘。

我們將在下一章 中介紹Kotlin的 輕量級(jí)線程:協(xié)程(Coroutines)的相關(guān)知識(shí)寄疏,我們將看到在Kotlin中是牢,程序的邏輯可以在協(xié)程中順序地表達(dá),而底層庫會(huì)為我們解決其異步性陕截。

本章示例代碼工程:https://github.com/EasyKotlin/chapter8_fp

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末驳棱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子农曲,更是在濱河造成了極大的恐慌社搅,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乳规,死亡現(xiàn)場(chǎng)離奇詭異形葬,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)暮的,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門笙以,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人青扔,你說我怎么就攤上這事源织。” “怎么了微猖?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵谈息,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我凛剥,道長(zhǎng)侠仇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任犁珠,我火速辦了婚禮逻炊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘犁享。我一直安慰自己余素,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布炊昆。 她就那樣靜靜地躺著桨吊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪凤巨。 梳的紋絲不亂的頭發(fā)上视乐,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音敢茁,去河邊找鬼佑淀。 笑死,一個(gè)胖子當(dāng)著我的面吹牛彰檬,可吹牛的內(nèi)容都是我干的伸刃。 我是一名探鬼主播谎砾,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼捧颅!你這毒婦竟也來了棺榔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤隘道,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后郎笆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谭梗,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年宛蚓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了激捏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡凄吏,死狀恐怖远舅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情痕钢,我是刑警寧澤图柏,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站任连,受9級(jí)特大地震影響蚤吹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜随抠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一裁着、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拱她,春花似錦二驰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至氧猬,卻和暖如春背犯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盅抚。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工漠魏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人妄均。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓柱锹,卻偏偏與公主長(zhǎng)得像哪自,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子禁熏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容