點擊這裏 > 去京東商城購買閱讀
點擊這裏 > 去天貓商城購買閱讀
值就是函數,函數就是值。全部函數都消費函數,全部函數都生產函數。html
"函數式編程", 又稱泛函編程, 是一種"編程範式"(programming paradigm),也就是如何編寫程序的方法論。它的基礎是 λ 演算(lambda calculus)。λ演算能夠接受函數看成輸入(參數)和輸出(返回值)。java
和指令式編程相比,函數式編程的思惟方式更加註重函數的計算。它的主要思想是把問題的解決方案寫成一系列嵌套的函數調用。git
就像在OOP中,一切皆是對象,編程的是由對象交合創造的世界;
在FP中,一切皆是函數,編程的世界是由函數交合創造的世界。程序員
函數式編程中最古老的例子莫過於1958年被創造出來的Lisp了。Lisp由約翰·麥卡錫(John McCarthy,1927-2011)在1958年基於λ演算所創造,採用抽象數據列表與遞歸做符號演算來衍生人工智能。較現代的例子包括Haskell、ML、Erlang等。現代的編程語言對函數式編程都作了不一樣程度的支持,例如:JavaScript, Coffee Script,PHP,Perl,Python, Ruby, C# , Java 等等(這將是一個不斷增加的列表)。github
函數式語言在Java 虛擬機(JVM)平臺上也迅速地嶄露頭角,例如Scala 、Clojure ; .NET 平臺也不例外,例如:F# 。算法
函數做爲Kotlin中的一等公民,能夠像其餘對象同樣做爲函數的輸入與輸出。關於對函數式編程的支持,相對於Scala的學院派風格,Kotlin則是純的的工程派:實用性、簡潔性上都要比Scala要好。npm
本章咱們來一塊兒學習函數式編程以及在Kotlin中使用函數式編程的相關內容。編程
函數式編程思想是一個很是古老的思想。咱們簡述以下:swift
1935-1936這個時間段上,咱們有了三個有效計算模型:通用圖靈機、通用遞歸函數、λ可定義。Rosser 1939 年正式確認這三個模型是等效的。後端
在OOP中,一切皆是對象。
在面向對象的命令式(imperative)編程語言裏面,構建整個世界的基礎是類和類之間溝通用的消息,這些均可以用類圖(class diagram)來表述。《設計模式:可複用面向對象軟件的基礎》(Design Patterns: Elements of Reusable Object-Oriented Software,做者ErichGamma、Richard Helm、Ralph Johnson、John Vlissides)一書中,在每個模式的說明裏都附上了至少一幅類圖。
OOP 的世界提倡開發者針對具體問題創建專門的數據結構,相關的專門操做行爲以「方法」的形式附加在數據結構上,自頂向下地來構建其編程世界。
OOP追求的是萬事萬物皆對象的理念,天然地弱化了函數。例如:函數沒法做爲普通數據那樣來傳遞(OOP在函數指針上的約束),因此在OOP中有各類各樣的、五花八門的設計模式。
GoF所著的《設計模式-可複用面向對象軟件的基礎》從面向對象設計的角度出發的,經過對封裝、繼承、多態、組合等技術的反覆使用,提煉出一些可重複使用的面向對象設計技巧。而多態在其中又是重中之重。
多態、面向接口編程、依賴反轉等術語,描述的思想實際上是相同的。這種反轉模式實現了模塊與模塊之間的解耦。這樣的架構是健壯的, 而爲了實現這樣的健壯系統,在系統架構中基本都須要使用多態性。
絕大部分設計模式的實現都離不開多態性的思想。換一種說法就是,這些設計模式背後的本質其實就是OOP的多態性,而OOP中的多態本質上又是受約束的函數指針。
引用Charlie Calverts對多態的描述: 「多態性是容許你將父對象設置成爲和一個或更多的他的子對象相等的技術,賦值以後,父對象就能夠根據當前賦值給它的子對象的特性以不一樣的方式運做。」
簡單的說,就是一句話:容許將子類類型的指針賦值給父類類型的指針。而咱們在OOP中的那麼多的設計模式,其實就是在OOP的多態性的約束規則下,對這些函數指針的調用模式的總結。
不少設計模式,在函數式編程中均可以用高階函數來代替實現:
在FP中,一切皆是函數。
函數式編程(FP)是關於不變性和函數組合的一種編程範式。
函數式編程語言實現重用的思路很不同。函數式語言提倡在有限的幾種關鍵數據結構(如list、set、map)上 , 運用函數的組合 ( 高階函數) 操做,自底向上地來構建世界。
固然,咱們在工程實踐中,是不能極端地追求純函數式的編程的。一個簡單的緣由就是:性能和效率。例如:對於有狀態的操做,命令式操做一般會比聲明式操做更有效率。純函數式編程是解決某些問題的偉大工具,可是在另外的一些問題場景中,並不適用。由於反作用老是真實存在。
OOP喜歡自頂向下架構層層分解(解構),FP喜歡自底向上層層組合(複合)。 而實際上,編程的本質就是次化分解與複合的過程。經過這樣的過程,創造一個美妙的邏輯之塔世界。
咱們常常說一些代碼片斷是優雅的或美觀的,實際上意味着它們更容易被人類有限的思惟所處理。
對於程序的複合而言,好的代碼是它的表面積要比體積增加的慢。
代碼塊的「表面積」是是咱們複合代碼塊時所須要的信息(接口API協議定義)。代碼塊的「體積」就是接口內部的實現邏輯(API內部的實現代碼)。
在OOP中,一個理想的對象應該是隻暴露它的抽象接口(純表面, 無體積),其方法則扮演箭頭的角色。若是爲了理解一個對象如何與其餘對象進行復合,當你發現不得不深刻挖掘對象的實現之時,此時你所用的編程範式的本來優點就蕩然無存了。
FP經過函數組合來構造其邏輯系統。FP傾向於把軟件分解爲其須要執行的行爲或操做,並且一般採用自底向上的方法。函數式編程也提供了很是強大的對事物進行抽象和組合的能力。
在FP裏面,函數是「一類公民」(first-class)。它們能夠像1, 2, "hello",true,對象…… 之類的「值」同樣,在任意位置誕生,經過變量,參數和數據結構傳遞到其它地方,能夠在任何位置被調用。
而在OOP中,不少所謂面向對象設計模式(design pattern),都是由於面嚮對象語言沒有first-class function(對應的是多態性),因此致使了每一個函數必須被包在一個對象裏面(受約束的函數指針)才能傳遞到其它地方。
在面向對象式的編程中,一切皆是對象(偏重數據結構、數據抽象,輕算法)。咱們把它叫作:胖數據結構-瘦算法(FDS-TA)。
在面向函數式的編程中,一切皆是函數(偏重算法,輕數據結構)。咱們把它叫作:瘦數據結構-胖算法(TDS-FA)。
但是,這個世界很複雜,你怎麼能說一切皆是啥呢?真實的編程世界,天然是勻稱的數據結構結合勻稱的算法(SDS-SA)來創造的。
咱們在編程中,不可能使用純的對象(對象的行爲方法其實就是函數),或者純的函數(調用函數的對象、函數操做的數據其實就是數據結構)來創造一個完整的世界。若是數據結構
是陰
,算法
是陽
,那麼在解決實際問題中,每每是陰陽交合而成世界。仍是那句經典的:
程序 = 勻稱的數據結構 + 勻稱的算法
咱們用一幅圖來簡單說明:
一切皆是映射。函數式編程的代碼主要就是「對映射的描述」。咱們說組合是編程的本質,其實,組合就是創建映射關係。
一個函數無非就是從輸入到輸出的映射,寫成數學表達式就是:
f: X -> Y
p:Y -> Z
p(f) : X ->Z
用編程語言表達就是:
fun f(x:X) : Y{} fun p(y:Y) : Z{} fun fp(f: (X)->Y, p: (Y)->Z) : Z { return {x -> p(f(x))} }
在常常被引用的論文 「Why Functional Programming Matters」 中,做者 John Hughes 說明了模塊化是成功編程的關鍵,而函數編程能夠極大地改進模塊化。
在函數編程中,咱們有一個內置的框架來開發更小的、更簡單的和更通常化的模塊, 而後將它們組合在一塊兒。
函數編程的一些基本特色包括:
函數式編程的本質是函數的組合,組合的本質是範疇(Category)。
和搞編程的同樣,數學家喜歡將問題不斷加以抽象從而將本質問題抽取出來加以論證解決,範疇論就是這樣一門以抽象的方法來處理數學概念的學科,主要用於研究一些數學結構之間的映射關係(函數)。
在範疇論裏,一個範疇(category)由三部分組成:
這裏的對象能夠當作是一類東西,例如數學上的羣,環,以及有理數,無理數等均可以歸爲一個對象。對應到編程語言裏,能夠理解爲一個類型,好比說整型,布爾型等。
態射指的是一種映射關係,簡單理解,態射的做用就是把一個對象 A 裏的值 a 映射爲 另外一個對象 B 裏的值 b = f(a),這就是映射的概念。
態射的存在反映了對象內部的結構,這是範疇論用來研究對象的主要手法:對象內部的結構特性是經過與別的對象的映射關係反映出來的,動靜是相對的,範疇論經過研究映射關係來達到探知對象的內部結構的目的。
組合操做符,用點(.)表示,用於將態射進行組合。組合操做符的做用是將兩個態射進行組合,例如,假設存在態射 f: A -> B, g: B -> C, 則 g.f : A -> C.
一個結構要想成爲一個範疇, 除了必須包含上述三樣東西,它還要知足如下三個限制:
在範疇論裏另外研究的重點是範疇與範疇之間的關係,就正如對象與對象之間有態射同樣,範疇與範疇之間也存在映射關係,從而能夠將一個範疇映射爲另外一個範疇,這種映射在範疇論中叫做函子(functor),具體來講,對於給定的兩個範疇 A 和 B, 函子的做用有兩個:
顯然,函子反映了不一樣的範疇之間的內在聯繫。跟函數和泛函數的思想是相同的。
而咱們的函數式編程探究的問題與思想理念能夠說是跟範疇論徹底吻合。若是把函數式編程的整個的世界看作一個對象,那麼FP真正搞的事情就是創建經過函數之間的映射關係,來構建這樣一個美麗的編程世界。
不少問題的解決(證實)其實都不涉及具體的(數據)結構,而徹底能夠只依賴映射之間的組合運算(composition)來搞定。這就是函數式編程的核心思想。
若是咱們把程序
看作圖論裏面的一張圖G,數據結構
看成是圖G的節點Node(數據結構,存儲狀態), 而算法
邏輯就是這些節點Node之間的Edge (數據映射,Mapping), 那麼這整幅圖 G(N,E)
就是一幅美妙的抽象邏輯之塔的 映射圖
, 也就是咱們編程創造的世界:
函數式編程(FP)中,函數是"第一等公民"。
所謂"第一等公民"(first class),有時稱爲 閉包或者 仿函數(functor)對象,
指的是函數與其餘數據類型同樣,處於平等地位,能夠賦值給其餘變量,也能夠做爲參數,傳入另外一個函數,或者做爲別的函數的返回值。這個以函數爲參數的概念,跟C語言中的函數指針相似。
舉例來講,下面代碼中的print變量就是一個函數(沒有函數名),能夠做爲另外一個函數的參數:
>>> val print = fun(x:Any){println(x)} >>> listOf(1,2,3).forEach(print) 1 2 3
FP 語言支持高階函數,高階函數就是多階映射。高階函數用另外一個函數做爲其輸入參數,也能夠返回一個函數做爲輸出。
代碼示例:
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)) } }
測試代碼:
fun main(args: Array<String>) { val oddLength = compose(::isOdd, ::length) val strings = listOf("a", "ab", "abc") println(strings.filter(oddLength)) // [a, abc] }
這個compose函數,其實就是數學中的複合函數的概念,這是一個高階函數的例子:傳入的兩個參數f , g都是函數,其返回值也是函數。
圖示以下:
這裏的
fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C
中類型參數對應:
fun <String, Int, Boolean> compose(f: (Int) -> Boolean, g: (String) -> Int): (String) -> Boolean
這裏的(Int) -> Boolean
、 (String) -> Int
、 (String) -> Boolean
都是函數類型。
其實,從映射的角度看,就是二階映射。對[a, ab, abc] 中每一個元素 x 先映射成長度g(x) = 1, 2, 3 , 再進行第二次映射:f(g(x)) %2 != 0 , 長度是奇數?返回值是true的被過濾出來。
有了高階函數,咱們能夠用優雅的方式進行模塊化編程。
另外,高階函數知足結合律:
? 演算是函數式語言的基礎。在λ-演算的基礎上,發展起來的π-演算、χ-演算,成爲近年來的併發程序的理論工具之一,許多經典的併發程序模型就是以π-演算爲框架的。λ 演算神奇之處在於,經過最基本的函數抽象和函數應用法則,配套以適當的技巧,便可以構造出任意複雜的可計算函數。
λ演算是一套用於研究函數定義、函數應用和遞歸的形式系統。它由 阿隆佐·丘奇(Alonzo Church,1903~1995)和 Stephen Cole Kleene 在 20 世紀三十年代引入。當時的背景是解決函數可計算的本質性問題,初期λ演算成功的解決了在可計算理論中的斷定性問題,後來根據Church–Turing thesis,證實了λ演算與圖靈機是等價的。
λ 演算能夠被稱爲最小的通用程序設計語言。它包括一條變換規則 (變量替換) 和一條函數定義方式,λ演算之通用在於,任何一個可計算函數都能用這種形式來表達和求值。
λ演算強調的是變換規則的運用,這裏的變換規則本質上就是函數映射。
Lambda 表達式(Lambda Expression) 是 λ演算 的一部分。
λ演算中一切皆函數,全體λ表達式構成Λ空間,λ表達式爲Λ空間到Λ空間的函數。
例如,在 lambda 演算中有許多方式均可以定義天然數,最多見的是Church 整數,定義以下:
0 = λ f. λ x. x 1 = λ f. λ x. f x 2 = λ f. λ x. f (f x) 3 = λ f. λ x. f (f (f x)) ...
數學家們都崇尚簡潔,只用一個關鍵字 'λ' 來表示對函數的抽象。
其中的λ f. λ x.
,λ f
是抽象出來的函數, λ x
是輸入參數, .
語法用來分割參數表和函數體。 爲了更簡潔,咱們簡記爲F, 那麼上面的Church 整數定義簡寫爲:
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
用圖示以下:
在λ演算中只有函數,一門編程語言中的數據類型,好比boolean、number、list等,均可以使用純λ演算來實現。咱們不用去關心數據的值是什麼,重點是咱們能對這個值作什麼操做(apply function)。
使用λ演算定義一個恆等函數I :
I = λ x . x
使用Kotlin代碼來寫,以下:
>>> val I = {x:Int -> x} >>> I(0) 0 >>> I(1) 1 >>> I(100) 100
對 I 而言任何一個 x 都是它的不動點(即對某個函數 f(x) 存在這樣的一個輸入 x,使得函數的輸出仍舊等於輸入的 x 。形式化的表示即爲 f(x) = x )。
再例如,下面的 λ 表達式表示將x映射爲 x+1 :
λ x . x + 1
測試代碼:
( λ x . x + 1) 5
將輸出6 。
這樣的表達式,在Kotlin中, 若是使用Lambda表達式咱們這樣寫:
>>> val addOneLambda = { ... x: Int -> ... x + 1 ... } >>> addOneLambda(1) 2
若是使用匿名函數,這樣寫:
>>> val addOneAnonymouse = (fun(x: Int): Int { ... return x + 1 ... }) >>> addOneAnonymouse(1) 2
在一些古老的編程語言中,lambda表達式仍是比較接近lambda演算的表達式的。在現代程序語言中的lambda表達式,只是取名自lambda演算,已經與原始的lambda演算有很大差異了。例如:
在Javascript裏沒有任何語法專門表明lambda, 只寫成這樣的嵌套函數function{ return function{...} }
。
不少基於 lambda calculus 的程序語言,好比 ML 和 Haskell,都習慣用currying 的手法來表示函數。好比,若是你在 Haskell 裏面這樣寫一個函數:
f x y = x + y
而後你就能夠這樣把鏈表裏的每一個元素加上 2:
map (f 2) [1, 2, 3]
它會輸出 [3, 4, 5]。
Currying 用一元函數,來組合成多元函數。好比,上面的函數 f 的定義在 Scheme 裏面至關於:
(define f (lambda (x) (lambda (y) (+ x y))))
它是說,函數 f,接受一個參數 x,返回另外一個函數(沒有名字)。這個匿名函數,若是再接受一個參數 y,就會返回 x + y。因此上面的例子裏面,(f 2) 返回的是一個匿名函數,它會把 2 加到本身的參數上面返回。因此把它 map 到 [1, 2, 3],咱們就獲得了 [3, 4, 5]。
咱們再使用Kotlin中的函數式編程來舉例說明。
首先,咱們看下普通的二元函數的寫法:
fun add(x: Int, y: Int): Int { return x + y } add(1, 2) // 輸出3
這種寫法最簡單,只有一層映射。
柯里化的寫法:
fun curryAdd(x: Int): (Int) -> Int { return { y -> x + y } } curryAdd(1)(2)// 輸出3
咱們先傳入參數x = 1, 返回函數 curryAdd(1) = 1 + y;
而後傳入參數 y = 2, 返回最終的值 curryAdd(1)(2) = 3。
固然,咱們也有 λ 表達式的寫法:
val lambdaCurryAdd = { x: Int -> { y: Int -> x + y } } lambdaCurryAdd(1)(2) // 輸出 3
這個作法其實來源於最先的 lambda calculus 的設計。由於 lambda calculus 的函數都只有一個參數,因此爲了可以表示多參數的函數, Haskell Curry (數學家和邏輯學家),發明了這個方法。
不過在編碼實踐中,Currying 的工程實用性、簡潔性上不是那麼的友好。大量使用 Currying,會致使代碼可讀性下降,複雜性增長,而且還可能所以引發意想不到的錯誤。 因此在咱們的講求工程實踐性能的Kotlin語言中,
古老而美麗的理論,也許可以給我帶來思想的啓迪,可是在工程實踐中未必那麼理想。
閉包簡單講就是一個代碼塊,用{ }
包起來。此時,程序代碼也就成了數據,能夠被一個變量所引用(與C語言的函數指針比較相似)。閉包的最典型的應用是實現回調函數(callback)。
閉包包含如下兩個組成部分:
在PHP、Scala、Scheme、Common Lisp、Smalltalk、Groovy、JavaScript、Ruby、 Python、Go、Lua、objective c、swift 以及Java(Java8及以上)等語言中都能找到對閉包不一樣程度的支持。
Lambda表達式能夠表示閉包。
除了高階函數、閉包、Lambda表達式的概念,FP 還引入了惰性計算的概念。惰性計算(儘量延遲表達式求值)是許多函數式編程語言的特性。惰性集合在須要時提供其元素,無需預先計算它們,這帶來了一些好處。首先,您能夠將耗時的計算推遲到絕對須要的時候。其次,您能夠創造無限個集合,只要它們繼續收到請求,就會繼續提供元素。第三,map 和 filter 等函數的惰性使用讓您可以獲得更高效的代碼(請參閱 參考資料 中的連接,加入由 Brian Goetz 組織的相關討論)。
在惰性計算中,表達式不是在綁定到變量時當即計算,而是在求值程序須要產生表達式的值時進行計算。
一個惰性計算的例子是生成無窮 Fibonacci 列表的函數,可是對 第 n 個Fibonacci 數的計算至關於只是從可能的無窮列表中提取一項。
遞歸指的是一個函數在其定義中直接或間接調用自身的一種方法, 它一般把一個大型的複雜的問題轉化爲一個與原問題類似的規模較小的問題來解決(複用函數自身), 這樣能夠極大的減小代碼量。遞歸分爲兩個階段:
1.遞推:把複雜的問題的求解推到比原問題簡單一些的問題的求解;
2.迴歸:當得到最簡單的狀況後,逐步返回,依次獲得複雜的解。
遞歸的能力在於用有限的語句來定義對象的無限集合。
使用遞歸要注意的有兩點:
(1)遞歸就是在過程或函數裏面調用自身;
(2)在使用遞歸時,必須有一個明確的遞歸結束條件,稱爲遞歸出口。
下面咱們舉例說明。
階乘函數 fact(n) 通常這樣遞歸地定義:
fact(n) = if n=0 then 1 else n * fact(n-1)
咱們使用Kotlin代碼實現這個函數以下:
fun factorial(n: Int): Int { println("factorial() called! n=$n") if (n == 0) return 1; return n * factorial(n - 1); }
測試代碼:
@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計算的過程當中,函數不斷的調用自身,而後不斷的展開,直到最後到達了終止的n==0,這是遞歸的原則之一,就是在遞歸的過程當中,傳遞的參數必定要不斷的接近終止條件,在上面的例子中就是n的值不斷減小,直至最後爲0。
再舉個Fibonacci數列的例子。
Fibonacci數列用數學中的數列的遞歸表達式定義以下:
fibonacci (0) = 0
fibonacci (1) = 1
fibonacci (n) = fibonacci (n - 1) + fibonacci (n - 2)
咱們使用Kotlin代碼實現它:
fun fibonacci(n: Int): Int { if (n == 1 || n == 2) return 1; return fibonacci(n - 1) + fibonacci(n - 2); }
測試代碼:
@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中的遞歸寫法
由於Scheme 程序中充滿了一對對嵌套的小括號,這些嵌套的符號體現了最基本的數學思想——遞歸。因此,爲了多維度的來理解遞歸,咱們給出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)))))))
其中關鍵字lambda, 代表咱們定義的(即任何封閉的開括號當即離開λ及其相應的關閉括號)是一個函數。
Lambda演算和函數式語言的計算模型天生較爲接近,Lambda表達式通常是這些語言必備的基本特性。
Scheme是Lisp方言,遵循極簡主義哲學,有着獨特的魅力。Scheme的一個主要特性是能夠像操做數據同樣操做函數調用。
在現代編程語言中,函數都是具名的,而在傳統的Lambda Calculus中,函數都是沒有名字的。這樣就出現了一個問題 —— 如何在Lambda Calculus中實現遞歸函數,即匿名遞歸函數。Haskell B. Curry (編程語言 Haskell 就是以此人命名的)發現了一種不動點組合子 —— Y Combinator,用於解決匿名遞歸函數實現的問題。Y 組合子(Y Combinator),其定義是:
Y = λf.(λx.f (x x)) (λx.f (x x))
對於任意函數 g,能夠經過推導獲得 Y g = g (Y g)
((高階)函數的不動點 ),從而證實 λ演算 是 圖靈完備 的。 Y 組合子 的重要性因而可知一斑。
她讓人絞盡腦汁,也琢磨不定!她讓人心力憔悴,又百般回味!
她,看似平淡,卻深藏玄機!她,貌不驚人,卻天下無敵!
她是誰?她就是 Y 組合子:Y = λf.(λx.f (x x)) (λx.f (x x)),不動點組合子中最著名的一個。
Y 組合子讓咱們能夠定義匿名的遞歸函數。Y組合子是Lambda演算的一部分,也是函數式編程的理論基礎。僅僅經過Lambda表達式這個最基本的 原子 實現循環迭代。Y 組合子自己是函數,其輸入也是函數(在 Lisp 中連程序都是函數)。
很有道生1、一輩子2、二生3、三生萬物的韻味。
舉個例子說明: 咱們先使用類C語言中較爲熟悉的JavaScript來實現一個Y組合子函數, 由於JavaScript語言的動態特性,使得該實現相比許多須要聲明各類類型的語言要簡潔許多:
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函數組合一段匿名函數代碼,實現了一個匿名的遞歸階乘函數。
直接將這兩個函數放到瀏覽器的Console中去執行,咱們將看到以下輸出:
fact(10) 3628800
這個Y函數至關繞腦。要是在Clojure(JVM上的Lisp方言)中,這個Y函數實現以下:
(defn Y [r] ((fn [f] (f f)) (fn [f] (r (fn [x] ((f f) x))))))
使用Scheme語言來表達:
(define Y (lambda (f) ((lambda (x) (f (lambda (y) ((x x) y)))) (lambda (x) (f (lambda (y) ((x x) y)))))))
咱們能夠看出,使用Scheme語言表達的Y組合子跟 原生的 λ演算 表達式基本同樣。
用CoffeeScript實現一個 Y combinator就長這樣:
coffee> Y = (f) -> ((x) -> (x x)) ((x) -> (f ((y) -> ((x x) y)))) [Function]
這個看起就至關簡潔優雅了。咱們使用這個 Y combinator 實現一個匿名遞歸的Fibonacci函數:
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 ]
實現一個匿名遞歸階乘函數:
coffee> fact = Y (f) ->(n) -> if n==0 then 1 else n*f(n-1) [Function] coffee> fact(10) 3628800
上面的Coffee代碼的命令行REPL運行環境搭建很是簡單:
$ npm install -g coffee-script $ coffee coffee>
對CoffeeScript感興趣的讀者,能夠參考:http://coffee-script.org/。
可是,這個Y組合子 要是 使用 OOP 語言編程範式, 就要顯得複雜許多。爲了更加深入地認識OOP 與 FP編程範式,咱們使用Java 8 以及 Kotlin 的實例來講明。這裏使用Java給出示例的緣由,是爲了給出Kotlin與Java語言上的對比,在下一章節中,咱們將要學習Kotlin與Java的互操做。
首先咱們使用Java的匿名內部類實現Y組合子 :
package com.easy.kotlin; /** * Created by jack on 2017/7/9. */ 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); } }
這裏定義了一個Lambda<E>
類型, 而後經過E call(Object input)
方法實現自調用,方法實現裏有多處轉型以及嵌套調用。邏輯比較繞,代碼可讀性也比較差。固然,這個問題自己也比較複雜。
咱們使用Java 8的Lambda表達式來改寫下匿名內部類:
package com.easy.kotlin; /** * Created by jack on 2017/7/9. */ 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的對象表達式(順便複習回顧一下上一章節的相關內容)實現Y組合子:
package com.easy.kotlin /** * Created by jack on 2017/7/9. * * lambda f. (lambda x. (f(x x)) lambda x. (f(x x))) * * OOP YCombinator * */ 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 } }
咱們可使用 Kotlin FP (Lambda, function) 寫一個 Y-combinator 函數嗎?
Y = λf.(λx.f (x x)) (λx.f (x x))
固然,對於函數式編程也徹底支持的 Kotlin 也有 FP 風格的Y 組合子實現:
/** * Created by jack on 2017/7/9. * * lambda f. (lambda x. (f(x x)) lambda x. (f(x x))) * * FP YCombinator */ // 爲了方便易懂,使用 X 用作函數 (X)->Int 的別名 typealias X = (Int) -> Int // G 遞歸引用 G 本身 interface G : Function1<G, X> // create a fun G from lazy blocking fun G(block: (G) -> X) = object : G { // 調用函數自身 `block(g)` like as `g(g)` override fun invoke(g: G) = block(g) } typealias F = Function1<X, X> fun Y(f: F) = (fun(g: G) = g(g))(G { g -> f({ x -> g(g)(x) }) }) val fact = Y { rec -> { n -> if (n == 0) 1 else n * rec(n - 1) } } val fib = Y { f -> { n -> if (n == 1 || n == 2) 1 else f(n - 1) + f(n - 2) } } fun main(args: Array<String>) { println(fact(10)) println(fib(10)) for (i in 1..10) { println("$i!= ${fact(i)}") } for (i in 1..10) { println("fib($i) = ${fib(i)}") } }
其中,在接口 G 繼承了Function1<G, X>接口:
interface G : Function1<G, X>
這個Function1 接口是繼承自kotlin.Function 接口:
public interface Function<out R>
Function1 有一個抽象算子函數invoke , 用來調用入參 p1 :
public interface Function1<in P1, out R> : kotlin.Function<R> { public abstract operator fun invoke(p1: P1): R }
咱們定義的 G 函數,入參是block函數 (G) -> X
, block函數的入參又是 G 類型,經過 invoke 函數來實現 調用自身:
fun G(block: (G) -> X) = object : G { // 調用函數自身 `block(g)` like as `g(g)` override fun invoke(g: G) = block(g) }
這樣,咱們就能夠實現一個 Y 組合子函數了:
typealias F = Function1<X, X> fun Y(f: F) = (fun(g: G) = g(g))(G { g -> f({ x -> g(g)(x) }) })
咱們經過 Y 組合子定義階乘遞歸函數和 Fibonacci 數列函數:
val fact = Y { rec -> { n -> if (n == 0) 1 else n * rec(n - 1) } } val fib = Y { f -> { n -> if (n == 1 || n == 2) 1 else f(n - 1) + f(n - 2) } }
測試代碼:
fun main(args: Array<String>) { val square: X = { x -> x * x } println(square(9)) println(fact(10)) println(fib(10)) for (i in 1..10) { println("$i!= ${fact(i)}") } for (i in 1..10) { println("fib($i) = ${fib(i)}") } }
【 Github 源碼工程: https://github.com/EasyKotlin... 】
關於Y combinator的更多實現,能夠參考:https://gist.github.com/Jason... ; 另外,關於Y combinator的原理介紹,推薦看《The Little Schemer 》這本書。
從上面的例子,咱們能夠看出OOP中的對接口以及多態類型,跟FP中的函數的思想表達的,本質上是一個東西,這個東西究竟是什麼呢?咱們姑且稱之爲「編程之道」罷!
Y combinator 給咱們提供了一種方法,讓咱們在一個只支持first-class函數,可是沒有內建遞歸的編程語言裏完成遞歸。因此Y combinator給咱們展現了一個語言徹底能夠定義遞歸函數,即便這個語言的定義一點也沒提到遞歸。它給咱們展現了一件美妙的事:僅僅函數式編程本身,就可讓咱們作到咱們歷來不認爲能夠作到的事(並且還不止這一個例子)。
嚴謹而精巧的lambda演算體系,從最基本的概念「函數」入手,創造出一個絢爛而宏偉的世界,這不能不說是人類思惟的驕傲。
所謂"反作用"(side effect),指的是函數內部與外部互動(最典型的狀況,就是修改全局變量的值),產生運算之外的其餘結果。
函數式編程強調沒有"反作用",意味着函數要保持獨立,全部功能就是返回一個新的值,沒有其餘行爲,尤爲是不得修改外部變量的值。
函數式編程的動機,一開始就是爲了處理運算(computation),不考慮系統的讀寫(I/O)。"語句"屬於對系統的讀寫操做,因此就被排斥在外。
固然,實際應用中,不作I/O是不可能的。所以,編程過程當中,函數式編程只要求把I/O限制到最小,不要有沒必要要的讀寫行爲,保持計算過程的單純性。
函數式編程只是返回新的值,不修改系統變量。所以,不修改變量,也是它的一個重要特色。
在其餘類型的語言中,變量每每用來保存"狀態"(state)。不修改變量,意味着狀態不能保存在變量中。函數式編程使用參數保存狀態,最好的例子就是遞歸。
函數程序一般還增強引用透明性,即若是提供一樣的輸入,那麼函數老是返回一樣的結果。就是說,表達式的值不依賴於能夠改變值的全局狀態。這樣咱們就能夠從形式上邏輯推斷程序行爲。由於表達式的意義只取決於其子表達式而不是計算順序或者其餘表達式的反作用。這有助於咱們來驗證代碼正確性、簡化算法,有助於找出優化它的方法。
好了親,前文中咱們在函數式編程的世界裏遨遊了一番,如今咱們把思緒收回來,放到在Kotlin中的函數式編程中來。
嚴格的面向對象的觀點,使得不少問題的解決方案變得較爲笨拙。爲了將一行有用的代碼包裝到Runnable或者Callable 這兩個Java中最流行的函數式示例中,咱們不得不去寫五六行模板範例代碼。爲了讓事情簡單化(在Java 8中,增長Lambda表達式的支持),咱們在Kotlin中使用普通的函數來替代函數式接口。事實上,函數式編程中的函數,比C語言中的函數或者Java中的方法都要強大的多。
在Kotlin中,支持函數做爲一等公民。它支持高階函數、Lambda表達式等。咱們不只能夠把函數當作普通變量同樣傳遞、返回,還能夠把它分配給變量、放進數據結構或者進行通常性的操做。它們能夠是未經命名的,也就是匿名函數。咱們也能夠直接把一段代碼丟到 {}
中,這就是閉包。
在前面的章節中,其實咱們已經涉及到一些關於函數的地方,咱們將在這裏系統地學習一下Kotlin的函數式編程。
首先,咱們來看下Kotlin中函數的概念。
Kotlin 中的函數使用 fun 關鍵字聲明
fun double(x: Int): Int { return 2*x }
調用函數使用傳統的方法
fun test() { val doubleTwo = double(2) println("double(2) = $doubleTwo") }
輸出:double(2) = 4
調用成員函數使用點表示法
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對象FPBasics來演示。
經過 擴展 聲明完成一個類的新功能 擴展 ,而無需繼承該類或使用設計模式(例如,裝飾者模式)。
一個擴展String類的swap函數的例子:
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 }
這個 this 關鍵字在擴展函數內部對應到接收者對象(傳過來的在點符號前的對象)。 如今,咱們對任意 String 調用該函數了:
val str = "abcd" val swapStr = str.swap(0, str.lastIndex) println("str.swap(0, str.lastIndex) = $swapStr")
輸出: str.swap(0, str.lastIndex) = dbca
在如下場景中,函數還能夠用中綴表示法調用:
infix
關鍵字標註例如,給 Int 定義擴展
infix fun Int.shl(x: Int): Int { ... }
用中綴表示法調用擴展函數:
1 shl 2
等同於這樣
1.shl(2)
函數參數使用 Pascal 表示法定義,即 name: type。參數用逗號隔開。每一個參數必須顯式指定其類型。
fun powerOf(number: Int, exponent: Int): Int { return Math.pow(number.toDouble(), exponent.toDouble()).toInt() }
測試代碼:
val eight = powerOf(2, 3) println("powerOf(2,3) = $eight")
輸出:powerOf(2,3) = 8
函數參數能夠有默認值,當省略相應的參數時使用默認值。這能夠減小重載數量。
fun add(x: Int = 0, y: Int = 0): Int { return x + y }
默認值經過類型後面的 = 及給出的值來定義。
測試代碼:
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
另外,覆蓋帶默認參數的函數時,老是使用與基類型方法相同的默認參數值。
當覆蓋一個帶有默認參數值的方法時,簽名中不帶默認參數值:
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 { // 不能有默認值 return super.add(x, y) } }
能夠在調用函數時使用命名的函數參數。當一個函數有大量的參數或默認參數時這會很是方便。
給定如下函數
fun reformat(str: String, normalizeCase: Boolean = true, upperCaseFirstLetter: Boolean = true, divideByCamelHumps: Boolean = false, wordSeparator: Char = ' ') { }
咱們可使用默認參數來調用它
reformat(str)
然而,當使用非默認參數調用它時,該調用看起來就像
reformat(str, true, true, false, '_')
使用命名參數咱們可使代碼更具備可讀性
reformat(str, normalizeCase = true, upperCaseFirstLetter = true, divideByCamelHumps = false, wordSeparator = '_' )
而且若是咱們不須要全部的參數
reformat(str, wordSeparator = '_')
函數的參數(一般是最後一個)能夠用 vararg
修飾符標記:
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 }
容許將可變數量的參數傳遞給函數:
val list = asList(1, 2, 3)
具備塊代碼體的函數必須始終顯式指定返回類型,除非他們旨在返回 Unit
。
Kotlin 不推斷具備塊代碼體的函數的返回類型,由於這樣的函數在代碼體中可能有複雜的控制流,而且返回類型對於讀者(有時對於編譯器)也是不明顯的。
若是一個函數不返回任何有用的值,它的返回類型是 Unit
。Unit
是一種只有一個Unit
值的類型。這個值不須要顯式返回:
fun printHello(name: String?): Unit { if (name != null) println("Hello ${name}") else println("Hi there!") // `return Unit` 或者 `return` 是可選的 }
Unit
返回類型聲明也是可選的。上面的代碼等同於
fun printHello(name: String?) { ..... }
當函數返回單個表達式時,能夠省略花括號而且在 = 符號以後指定代碼體便可
fun double(x: Int): Int = x * 2
當返回值類型可由編譯器推斷時,顯式聲明返回類型是可選的:
fun double(x: Int) = x * 2
在 Kotlin 中函數能夠在文件頂層聲明,這意味着你不須要像一些語言如 Java、C# 或 Scala 那樣建立一個類來保存一個函數。此外除了頂層函數,Kotlin 中函數也能夠聲明在局部做用域、做爲成員函數以及擴展函數。
Kotlin 支持局部函數,即一個函數在另外一個函數內部
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)) }
局部函數能夠訪問外部函數(即閉包)中的局部變量delta。
println("sum(1,2,3) = ${sum(0, 1, 2, 3)}")
輸出:
sum(1,2,3) = 6
成員函數是在類或對象內部定義的函數
class Sample() { fun foo() { print("Foo") } }
成員函數以點表示法調用
Sample().foo() // 建立類 Sample 實例並調用 foo
函數能夠有泛型參數,經過在函數名前使用尖括號指定。
例如Iterable的map函數:
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> { return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform) }
高階函數是將函數用做參數或返回值的函數。例如,Iterable的filter函數:
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> { return filterTo(ArrayList<T>(), predicate) }
它的輸入參數predicate: (T) -> Boolean
就是一個函數。其中,函數類型聲明的語法是:
(X)->Y
表示這個函數是從類型X到類型Y的映射。即這個函數輸入X類型,輸出Y類型。
這個函數咱們這樣調用:
fun isOdd(x: Int): Boolean { return x % 2 == 1 } val list = listOf(1, 2, 3, 4, 5) list.filter(::isOdd)
其中,::
用來引用一個函數。
咱們也可使用匿名函數來實現這個predicate函數:
list.filter((fun(x: Int): Boolean { return x % 2 == 1 }))
咱們也能夠直接使用更簡單的Lambda表達式來實現一個predicate函數:
list.filter { it % 2 == 1 }
{}
括着->
以前聲明(參數類型能夠省略)->
後面上面的寫法跟:
list.filter({ it % 2 == 1 })
等價,若是 lambda 是該調用的惟一參數,則調用中的圓括號能夠省略。
使用Lambda表達式定義一個函數字面值:
>>> val sum = { x: Int, y: Int -> x + y } >>> sum(1,1) 2
咱們在使用嵌套的Lambda表達式來定義一個柯里化的sum函數:
>>> val sum = {x:Int -> {y:Int -> x+y }} >>> sum (kotlin.Int) -> (kotlin.Int) -> kotlin.Int >>> sum(1)(1) 2
it
:單個參數的隱式名稱Kotlin中另外一個有用的約定是,若是函數字面值只有一個參數,
那麼它的聲明能夠省略(連同 ->
),其名稱是 it
。
代碼示例:
>>> val list = listOf(1,2,3,4,5) >>> list.map { it * 2 } [2, 4, 6, 8, 10]
Lambda 表達式或者匿名函數,以及局部函數和對象表達式(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
咱們再使用閉包來寫一個使用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() }
一個輸出:
I = 1 J = 10 I = 2 I = 3 ... J = 20
Kotlin 提供了使用指定的 接收者對象 調用函數字面值的功能。
使用匿名函數的語法,咱們能夠直接指定函數字面值的接收者類型。
下面咱們使用帶接收者的函數類型聲明一個變量,並在以後使用它。代碼示例:
>>> val sum = fun Int.(other: Int): Int = this + other >>> 1.sum(1) 2
當接收者類型能夠從上下文推斷時,lambda 表達式能夠用做帶接收者的函數字面值。
class HTML { fun body() { println("HTML BODY") } } fun html(init: HTML.() -> Unit): HTML { // HTML.()中的HTML是接受者類型 val html = HTML() // 建立接收者對象 html.init() // 將該接收者對象傳給該 lambda return html }
測試代碼:
html { body() }
輸出:HTML BODY
使用這個特性,咱們能夠構建一個HTML的DSL語言。
有時候咱們須要訪問一個參數類型:
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? }
在這裏咱們向上遍歷一棵樹而且檢查每一個節點是否是特定的類型。
這都沒有問題,可是調用處不是很優雅:
treeNode.findParentOfType(MyTreeNode::class.java)
咱們真正想要的只是傳一個類型給該函數,即像這樣調用它:
treeNode.findParentOfType<MyTreeNode>()
爲可以這麼作,內聯函數支持具體化的類型參數,因而咱們能夠這樣寫:
inline fun <reified T> TreeNode.findParentOfType(): T? { var p = parent while (p != null && p !is T) { p = p.parent } return p as T? }
咱們使用 reified
修飾符來限定類型參數,如今能夠在函數內部訪問它了,
幾乎就像是一個普通的類同樣。因爲函數是內聯的,不須要反射,正常的操做符如 !is
和 as
如今都能用了。
雖然在許多狀況下可能不須要反射,但咱們仍然能夠對一個具體化的類型參數使用它:
inline fun <reified T> membersOf() = T::class.members fun main(s: Array<String>) { println(membersOf<StringBuilder>().joinToString("\n")) }
普通的函數(未標記爲內聯函數的)沒有具體化參數。
Kotlin 支持一種稱爲尾遞歸的函數式編程風格。 這容許一些一般用循環寫的算法改用遞歸函數來寫,而無堆棧溢出的風險。 當一個函數用 tailrec 修飾符標記並知足所需的形式時,編譯器會優化該遞歸,生成一個快速而高效的基於循環的版本。
tailrec fun findFixPoint(x: Double = 1.0): Double = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x)) // 函數必須將其自身調用做爲它執行的最後一個操做
這段代碼計算餘弦的不動點(fixpoint of cosine),這是一個數學常數。 它只是重複地從 1.0 開始調用 Math.cos,直到結果再也不改變,產生0.7390851332151607的結果。最終代碼至關於這種更傳統風格的代碼:
private fun findFixPoint(): Double { var x = 1.0 while (true) { val y = Math.cos(x) if (x == y) return y x = y } }
要符合 tailrec 修飾符的條件的話,函數必須將其自身調用做爲它執行的最後一個操做。在遞歸調用後有更多代碼時,不能使用尾遞歸,而且不能用在 try/catch/finally 塊中。尾部遞歸在 JVM 後端中支持。
Kotlin 還爲集合類引入了許多擴展函數。例如,使用 map() 和 filter() 函數能夠流暢地操縱數據,具體的函數的使用以及示例咱們已經在 集合類 章節中介紹。
本章咱們一塊兒學習了函數式編程的簡史、Lambda演算、Y組合子與遞歸等核心函數式的編程思想等相關內容。而後重點介紹了在Kotlin中如何使用函數式風格編程,其中重點介紹了Kotlin中函數的相關知識,以及高階函數、Lambda表達式、閉包等核心語法,並給出相應的實例說明。
咱們將在下一章 中介紹Kotlin的 輕量級線程:協程(Coroutines)的相關知識,咱們將看到在Kotlin中,程序的邏輯能夠在協程中順序地表達,而底層庫會爲咱們解決其異步性。
本章示例代碼工程: https://github.com/EasyKotlin...