第5章 函數與函數式編程
凡此變數中函彼變數者,則此為彼之函數。 ( 李善蘭《代數學》)
函數式編程語言最重要的基礎是λ演算(lambda calculus),而且λ演算的函數可以傳入函數參數,也可以返回一個函數。函數式編程 (簡稱FP) 是一種編程范式(programming paradigm)。
函數式編程與命令式編程最大的不同是:函數式編程的焦點在數據的映射,命令式編程(imperative programming)的焦點是解決問題的步驟。函數式編程不僅僅指的是Lisp、Haskell、 Scala等之類的語言,更重要的是一種編程思維,解決問題的思考方式,也稱面向函數編程。
函數式編程的本質是函數的組合。例如,我們想要過濾出一個List中的奇數,用Kotlin代碼可以這樣寫
package com.easy.kotlinfun main(args: Array<String>) {val list = listOf(1, 2, 3, 4, 5, 6, 7)println(list.filter { it % 2 == 1 }) // lambda表達式
}
這個映射的過程可以使用下面的圖來形象化地說明
而同樣的邏輯我們使用命令式的思維方式來寫的話,代碼如下
package com.easy.kotlin;import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;import static java.lang.System.out;public class FilterOddsDemo {public static void main(String[] args) {List<Integer> list = Arrays.asList(new Integer[] {1, 2, 3, 4, 5, 6, 7});out.println(filterOdds(list)); // 輸出:[1, 3, 5, 7]}public static List<Integer> filterOdds(List<Integer> list) {List<Integer> result = new ArrayList();for (Integer i : list) {if (isOdd(i)) {result.add(i);}}return result;}private static boolean isOdd(Integer i) {return i % 2 != 0;}
}
我們可以看出,函數式編程是簡單自然、直觀易懂且美麗優雅的編程風格。函數式編程語言中通常都會提供常用的map、reduce、filter等基本函數,這些函數是對List、Map集合等基本數據結構的常用操作的高層次封裝,就像一個更加智能好用的工具箱。
5.1 函數式編程簡介
函數式編程是關于不變性和函數組合的編程范式。函數式編程有如下特征
- 一等函數支持(first-class function):函數也是一種數據類型,可以當做參數傳入另一個函數,同時一個函數也可以返回函數。
- 純函數(pure function)和不變性(immutable):純函數指的是沒有副作用的函數(函數不去改變外部的數據狀態)。例如,一個編譯器就是一個廣義上的純函數。在函數式編程中,傾向于使用純函數編程。正因為純函數不會去修改數據,同時又使用不可變數據,所以程序不會去修改一個已經存在的數據結構,而是根據一定的映射邏輯創建一份新的數據。函數式編程是去轉換數據而非修改原始數據。
- 函數的組合(compose function):在面向對象編程中,是通過對象之間發送消息來構建程序邏輯;而在函數式編程中,是通過不同函數的組合構建程序邏輯。
5.2 聲明函數
Kotlin中使用 fun 關鍵字來聲明函數,其語法實例如下圖所示
為了更加直觀的感受到函數也可以當做變量來使用,我們聲明一個函數類型的變量 sum 如下
>>> val sum = fun(x:Int, y:Int):Int { return x + y }
>>> sum
(kotlin.Int, kotlin.Int) -> kotlin.Int
我們可以看到這個函數變量 sum 的類型是
(kotlin.Int, kotlin.Int) -> kotlin.Int
這個帶箭頭( -> )的表達式就是一個函數類型,表示一個輸入兩個Int類型值,輸出一個Int類型值的函數。我們可以直接使用這個函數字面值 sum
>>> sum(1,1)
2
從上面的這個典型的例子我們可以看出,Kotlin也是一門面向表達式的語言。既然 sum 是一個代表函數類型的變量,稍后我們將看到一個函數可以當做參數傳入另一個函數中(高階函數)。
當然,我們仍然可以像C/C++/Java中一樣,直接帶上函數名來聲明一個函數
fun multiply(x: Int, y: Int): Int {return x * y
}multiply(2, 2) // 4
5.3 lambda表達式
我們在本章開頭部分講到了這段代碼
val list = listOf(1, 2, 3, 4, 5, 6, 7)
list.filter { it % 2 == 1 }
這里的filter函數的入參 { it % 2 == 1 } 就是一段 lambda表達式。實際上,因為filter函數只有一個參數,所有括號被省略了。所以,filter函數調用的完整寫法是
list.filter ({ it % 2 == 1 })
其中的filter函數聲明如下
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>
其實,filter函數的入參是一個函數 predicate: (T) -> Boolean 。 實際上,
{ it % 2 == 1 }
是一種簡寫的語法,完整的lambda表達式是這樣寫的
{ it -> it % 2 == 1 }
如果拆開來寫,就更加容易理解
>>> val isOdd = { it: Int -> it % 2 == 1 } // 直接使用lambda表達式聲明一個函數,這個函數判斷輸入的Int是不是奇數
>>> isOdd
(kotlin.Int) -> kotlin.Boolean // isOdd函數的類型
>>> val list = listOf(1, 2, 3, 4, 5, 6, 7)
>>> list.filter(isOdd) // 直接傳入isOdd函數
[1, 3, 5, 7]
5.4 高階函數
其實,在上面的代碼示例 list.filter(isOdd) 中,我們已經看到了高階函數了。現在我們再添加一層映射邏輯。我們有一個字符串列表
val strList = listOf("a", "ab", "abc", "abcd", "abcde", "abcdef", "abcdefg")
然后,我們想要過濾出字符串元素的長度是奇數的列表。我們把這個問題的解決邏輯拆成兩個函數來組合實現
val f = fun (x: Int) = x % 2 == 1 // 判斷輸入的Int是否奇數
val g = fun (s: String) = s.length // 返回輸入的字符串參數的長度
我們再使用函數 h 來封裝 “字符串元素的長度是奇數” 這個邏輯,實現代碼如下
val h = fun(g: (String) -> Int, f: (Int) -> Boolean): (String) -> Boolean {return { f(g(it)) }
}
但是,這個 h 函數的聲明未免有點太長了。尤其是3個函數類型聲明的箭頭表達式,顯得不夠簡潔。不過不用擔心。
Kotlin中有簡單好用的 Kotlin 類型別名, 我們使用 G,F,H 來聲明3個函數類型
typealias G = (String) -> Int
typealias F = (Int) -> Boolean
typealias H = (String) -> Boolean
那么,我們的 h 函數就可簡單優雅的寫成下面這樣了
val h = fun(g: G, f: F): H {return { f(g(it)) } // 需要注意的是,這里的 {} 是不能省略的
}
這個 h 函數的映射關系可用下圖說明
函數體中的這句代碼 return { f(g(it)) } , 這里的 {} 它代表這是一個lambda表達式,返回的是一個 (String) -> Boolean 函數類型。如果沒有 { } , 那么返回值就是一個布爾類型Boolean了。
通過上面的代碼例子,我們可以看到,在Kotlin中,我們可以簡單優雅的實現高階函數。OK,現在邏輯已經實現完了,下面我們在 main 函數中運行測試一下效果。
fun main(args: Array<String>) {val strList = listOf("a", "ab", "abc", "abcd", "abcde", "abcdef", "abcdefg")println(strList.filter(h(g, f))) // 輸出:[a, abc, abcde, abcdefg]
}
當你看到 h(g, f) 這樣的復合函數的代碼時,你一定很開心,感到很自然,這跟數學公式真是很貼近,簡單易懂。
本章小結
在Kotlin中,支持函數作為一等公民。它支持高階函數、lambda表達式等。我們不僅可以把函數當做普通變量一樣傳遞、返回,還可以把它分配給變量、放進數據結構或者進行一般性的操作。在Kotlin中進行函數式編程相當簡單自如。