淺談函數語言程式設計
函數語言程式設計(Functional Programming)是一種程式設計風格,它是相對於指令式程式設計風格而言的,常見的面向物件程式設計就是指令式程式設計風格。
指令式程式設計是面向計算機硬體的抽象,有變數(對應著儲存單元),賦值語句(獲取、儲存指令),表示式(記憶體引用和算術運算)和控制語句(跳轉語句)。
而函數語言程式設計是面向數學的抽象,將計算描述為一種表示式求值。這裡的函式實際就是數學中的函式,即自變數到因變數的對映。也就是說,一個函式的值僅決定於函式引數的值,不依賴其他狀態。
函數語言程式設計是一種抽象程度很高的程式設計正規化,純粹的函數語言程式設計語言編寫的函式沒有變數,因此,任意一個函式,只要輸入是確定的,輸出就是確定的,這種純函式我們稱之為沒有副作用。而允許使用變數的程式設計語言,由於函式內部的變數狀態不確定,同樣的輸入,可能得到不同的輸出,因此,這種函式是有副作用的。
在函式式語言當中,函式作為一等公民,可以在任何地方定義,在函式內或函式外,可以作為函式的引數或返回值,可以對函式進行組合,也可以將函式賦值給變數。嚴格意義上的函數語言程式設計意味著不適用可變的變數,賦值,迴圈和其他命令式控制結構進行程式設計。
函數語言程式設計風格帶來的好處是:
- 函數語言程式設計使用不可變物件作為變數,不會修改變數的值,而是返回一個新的值,如此這樣,更容易理清頭緒,使得單元測試和除錯更加容易;
- 可以很自由地傳遞不可變物件,但對於可變物件來說,傳遞給其他程式碼之前,需要先建造一個以防萬一的副本;
- 一旦不可變物件完成構造以後,就不會有執行緒因為併發訪問而破壞物件內部狀態,因為根本沒有執行緒可以改變不可變物件的狀態;
- 不可變物件讓雜湊表鍵值更安全,所以雜湊表鍵要求必須是不可變物件,否則使用可變物件,如果物件狀態發生變化,那麼在雜湊表中就找不到這個物件了;
具體到程式語言,Scala(靜態語言)和Python(動態語言)都能比較的支援函數語言程式設計風格,但是它們都不是純函式式的,也就是說它們同時支援指令式風格和函式式風格。而Java基本是指令式風格,但自從Java8引入lambda表示式以後也開始部分支援函式式風格。函數語言程式設計最典型的是諸如map, flatMap, reduce, filter等函式,它們的特點是支援某個函式作為上面這些函式的引數。
下面分別以Java、Scala和Python舉例函數語言程式設計,其中Java對函數語言程式設計只是間接的支援(通過函式式介面),支援度比較有限,而Scala和Python就對函數語言程式設計支援的比較好。
Java函數語言程式設計舉例:
1 package lxy.java.fp; 2 3 import java.util.*; 4 import java.util.function.*; 5 6 7 public class FPDemo { 8 //定義泛型方法,用以根據第二個引數指定的條件從第一個引數指定的集合中過濾部分元素,並返回過濾後的結果。這裡的第二個引數是一個函式式介面。 9 public static <T> List <T> filter(List <T> list, Predicate <T> p) { 10 List <T> results = new ArrayList <>(); 11 for (T s : list) { 12 if (p.test(s)) { 13 results.add(s); 14 } 15 } 16 return results; 17 } 18 19 public static void main(String[] args) { 20 List <String> myList = Arrays.asList("Hello", "Java", "Python", "Scala"); 21 22 //通過匿名類的方式 23 List <String> results = filter(myList, new Predicate <String>() { 24 public boolean test(String t) { 25 return t.length() >= 5; 26 } 27 }); 28 System.out.println("through anonymous class:"); 29 System.out.println("strings with length more than 5:"); 30 for (String result : results) { 31 System.out.println(result); 32 } 33 34 //行為引數化,通過匿名函式(即lambda表示式)方式, 35 System.out.println("through lambda expression:"); 36 System.out.println("strings with length more than 5:"); 37 List <String> results2 = filter(myList, s -> s.length() >= 5); 38 results2.forEach(s -> System.out.println(s)); 39 40 //很容易地將過濾條件由字串長度大於等於5改為字串以字母a結尾, 41 // 這就是行為引數化,即將具體的邏輯(即行為或者函式)引數化,使得filter函式更加抽象,提高了程式碼複用度 42 //否則需要寫2個filter函式,一個過濾出長度大於等於5的字串,另一個過濾出以字元a結尾的字串 43 System.out.println("strings ends with character 'a'"); 44 List <String> results3 = filter(myList, s -> s.endsWith("a")); 45 results3.forEach(System.out::println); 46 47 //Java流中的filter函式和map函式,注意這裡的filter是Java庫函式,和前面自定義的filter函式不一樣 48 System.out.println("strings with length more than 5 and its length:"); 49 myList.parallelStream().filter(s -> s.length() >= 5).map(s -> "(" + s + ", " + s.length() + ")") 50 .forEach(System.out::println); 51 52 //高階函式 53 Function <Integer, Integer> f = (Integer x) -> x + 1; 54 Function <Integer, Integer> g = (Integer x) -> x * x; 55 Function <Integer, Integer> h = f.andThen(g); 56 Function <Integer, Integer> r = f.compose(g); 57 58 System.out.println("higher-order function:"); 59 System.out.println("h(2)=g(f(2))=" + h.apply(2)); 60 System.out.println("r(2)=f(g(2))=" + r.apply(2)); 61 62 } 63 }
Scala函數語言程式設計舉例:
1 package lxy.scala.fp 2 3 object FPDemo { 4 //定義泛型方法,用以根據第二個引數指定的條件從第一個引數指定的列表中過濾部分元素,並返回過濾後的結果,結果型別仍然是List。 5 //這裡的第二個引數是一個函式, 該函式輸入引數型別為T,返回值型別為Boolean 6 def filter[T](list: List[T], f: T => Boolean) = 7 for {e <- list if f(e) == true} yield e 8 9 //高階函式,該函式定義為g(f(x)),其中函式f和g都是作為引數在呼叫該高階函式時指定的 10 def highOrderFunction1(x: Int, f: Int => Int, g: Int => Int) = g(f(x)) 11 12 //定義巢狀函式,針對每個引數,外層函式都會返回一個函式,即內層函式 13 //這裡factor是自由變數,number是繫結變數。 14 //閉包是一個函式,返回值依賴於宣告在函式外部的一個或多個變數 15 //函式multiplier返回的函式就是閉包,factor就是外部的變數,也叫自由變數,number是繫結變數(形式引數) 16 def multiplier(factor: Int): Int => Int = { 17 def multiplyByFactor(number: Int) = factor * number 18 19 return multiplyByFactor 20 } 21 22 //柯里化函式,型別是(Int)(Int) => Int 23 def multiplier2(factor: Int)(number: Int) = factor * number 24 25 //這個函式實際跟multiplier效果是一樣的,每傳入一個引數factor,都會返回一個函式 26 //閉包是一個函式,返回值依賴於宣告在函式外部的一個或多個變數 27 //函式multiplier3返回的函式就是閉包,factor就是外部的變數,也叫自由變數,number是繫結變數(形式引數) 28 def multiplier3(factor: Int) = multiplier2(factor) _ 29 30 def main(args: Array[String]): Unit = { 31 val myList = List("Hello", "Java", "Python", "Scala") 32 println("strings with length more than 5") 33 filter(myList, (s: String) => s.length >= 5).foreach(s => println(s)) 34 35 println("strings ends with character 'a'") 36 filter(myList, (s: String) => s.endsWith("a")).foreach(println) 37 38 println("strings with length more than 5 and its length:") 39 //這裡的filter不是自定義函式filter,而是庫函式,返回長度大於等於5的字串以及對應的長度 40 myList.filter(_.length >= 5).map(s => (s, s.length)).foreach(println) 41 42 43 val result = highOrderFunction1(2, (x: Int) => x + 1, (x: Int) => x * x) 44 println("f(x)=x+1, g(x)=x*x, g(f(2))=" + result) 45 46 47 println("multiplier(2)=" + multiplier(2)) 48 println("multiplier(2)(3)=" + multiplier(2)(3)) 49 val double = multiplier(2) 50 println("double(3)=" + double(3)) 51 52 println("multiplier3(3)=" + multiplier3(3)) 53 println("multiplier3(3)(4)=" + multiplier3(3)(4)) 54 val triple = multiplier3(3) 55 println("triple(4)=" + triple(4)) 56 57 } 58 }
Python函數語言程式設計舉例:
1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- 3 4 if __name__ == "__main__": 5 """ 6 Usage: ./fp_demo.py 7 """ 8 9 # 定義函式filter,第一個引數是列表,第二個引數是函式 10 def filter(list, f): 11 return [e for e in list if f(e) == True] 12 13 # python中沒有foreach函式,自己定義一個,其中第一個引數是函式,第二個引數是迭代器(列表) 14 def foreach(f, iterator): 15 for item in iterator: 16 f(item) 17 18 myList = ["Hello", "Java", "Python", "Scala"] 19 resultList = filter(myList, lambda e: len(e) >= 5) 20 print("strings with length more than 5", sep="\n") 21 foreach(lambda e: print(e, sep="\n"), resultList) 22 23 resultList2 = filter(myList, lambda e: e.endswith("a")) 24 print("strings ends with character 'a'", sep="\n") 25 foreach(lambda e: print(e, sep="\n"), resultList2) 26 27 # 這裡的map函式是python內建的 28 print("strings with length more than 5 and its length:", sep="\n") 29 foreach(lambda e: print(e, sep="\n"), map(lambda e: (e, len(e)), filter(myList, lambda e: len(e) >= 5))) 30 31 # 高階函式,該函式定義為g(f(x)),其中函式f和g都是作為引數在呼叫該高階函式時指定的 32 def highOrderFunction1(x, f, g): 33 return g(f(x)) 34 35 # 該函式在下面對highOrderFunction1函式的呼叫中被當做引數傳入 36 def f(x): 37 return x + 1 38 39 # 第二個引數傳入上面定義的f函式,作為第三個引數的的函式採用的是匿名函式 40 result = highOrderFunction1(2, f, g=lambda x: x * x) 41 print("f(x)=x+1, g(x)=x*x, g(f(2))=%d" % result, sep="\n") 42 43 # 定義巢狀函式,針對每個引數,外層函式都會返回一個函式,即內層函式 44 # 這裡factor是自由變數,number是繫結變數。 45 # 閉包是一個函式,返回值依賴於宣告在函式外部的一個或多個變數 46 # 函式multiplier返回的函式就是閉包,factor就是外部的變數,也叫自由變數,number是繫結變數(形式引數) 47 def multiplier(factor): 48 def multiplyByFactor(number): 49 return factor * number 50 return multiplyByFactor 51 52 53 print("multiplier(2)(3)=%s" % multiplier(2)(3)) 54 # double是一個函式,它將輸入引數乘以2倍以後返回 55 double = multiplier(2) 56 print("double(3)=%s" % double(3)) 57 58 print("multiplier(3)(4)=%s" % multiplier(3)(4)) 59 # triple是一個函式,它將輸入引數乘以3倍以後返回 60 triple = multiplier(3) 61 print("triple(4)=%s" % triple(4)) 62 63 # 第三個引數f是函式 64 def add(x, y, f): 65 return f(x) + f(y) 66 67 print("2 ^ 2 + 3 ^2 = %s" % add(2, 3, lambda x: x * x)) 68 print("(2 + 1) + (3 + 1) = %s" % add(2, 3, f))
最後來看一個數學題目,已知a<=b, 且a和b都是整數,求下面三個公式的值。
可以看到這三個公式分別是求a~b的和,a~b的平方和,a~b各自的階乘的和。可以看到三個公式是類似的,都是求和,只不過分別是對自身求和,對自身的平方求和以及對自身的階乘求和,也就是說這裡有3個計算邏輯,需要對這3個計算邏輯計算出來的數求和。如果是指令式程式設計風格,就只能寫三個函式來解決問題。但是如果採用函數語言程式設計風格,就可以只寫一個通用的求和函式來解決該問題,因為可以將這3個計算邏輯(函式)作為引數傳給之前的通用求和函式。下面分別用Java,Scala和Python來解決該問題。
Java程式碼
1 package lxy.java.fp; 2 3 import java.util.function.*; 4 5 6 public class Sum4Integers { 7 //通用求和函式,其中f是一個函式式介面,它接收一個整型引數並返回一個整型數值 8 //採用遞迴計算方法 9 private static int sum(Function<Integer, Integer> f, int a, int b) { 10 if (a > b) 11 return 0; 12 else 13 return f.apply(a) + sum(f, a + 1, b); 14 } 15 16 //求階乘函式,採用遞迴演算法,較複雜,不能作為匿名函式傳入上面的通用求和引數,因此需要預先定義 17 private static int factor(int x) { 18 if (x == 0) 19 return 1; 20 else 21 return x * factor(x - 1); 22 } 23 24 //求公式一函式,即求a~b的和 25 //作為引數的函式就是返回變數自身,較簡單,採用匿名函式 26 static int sumInts(int a, int b) { 27 return sum(x -> x, a, b); 28 } 29 30 //求公式二函式,即求a^2 ~ b^2的和 31 //作為引數的函式就是返回變數的平方,較簡單,採用匿名函式 32 static int sumSquares(int a, int b) { 33 return sum(x -> x * x, a, b); 34 } 35 36 //求公式三函式,即求a! ~ b!的和 37 //作為引數的函式就是求變數的階乘,較複雜(本身是遞迴函式),採用定義好的函式factor 38 static int sumFactors(int a, int b) { 39 return sum(Sum4Integers::factor, a, b); 40 } 41 42 public static void main(String[] args) { 43 System.out.println("1+2+3+4+5=" + sumInts(1, 5)); 44 System.out.println("1^2+2^2+3^2+4^2+5^2=" + sumSquares(1, 5)); 45 System.out.println("1!+2!+3!+4!+5!=" + sumFactors(1, 5)); 46 } 47 48 }
Scala程式碼
1 package lxy.scala.fp 2 3 4 object Sum4Integers { 5 //通用求和函式 6 //採用遞迴計算方法 7 private def sum(f: Int => Int, a: Int, b: Int): Int = if (a > b) 0 else f(a) + sum(f, a + 1, b) 8 9 //求階乘函式,採用遞迴演算法,較複雜,不能作為匿名函式傳入上面的通用求和引數,因此需要預先定義 10 private def factor(x: Int): Int = if (x == 0) 1 else x * factor(x - 1) 11 12 //求公式一函式,即求a~b的和 13 //作為引數的函式就是返回變數自身,較簡單,採用匿名函式 14 def sumInts(a: Int, b: Int) = sum(x => x, a, b) 15 16 //求公式二函式,即求a^2 ~ b^2的和 17 //作為引數的函式就是返回變數的平方,較簡單,採用匿名函式 18 def sumSquares(a: Int, b: Int) = sum(x => x * x, a, b) 19 20 //求公式三函式,即求a! ~ b!的和 21 //作為引數的函式就是求變數的階乘,較複雜(本身是遞迴函式),採用定義好的函式factor 22 def sumFactors(a: Int, b: Int) = sum(factor, a, b) 23 24 def main(args: Array[String]): Unit = { 25 println("1+2+3+4+5=" + sumInts(1, 5)) 26 println("1^2+2^2+3^2+4^2+5^2=" + sumSquares(1, 5)) 27 println("1!+2!+3!+4!+5!=" + sumFactors(1, 5)) 28 } 29 30 }
Python程式碼
1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- 3 4 if __name__ == "__main__": 5 """ 6 Usage: ./sum_integers.py 7 """ 8 9 # 通用求和函式,採用遞迴計算方法 10 def sum(f, a, b): 11 if a > b: 12 return 0 13 else: 14 return f(a) + sum(f, a + 1, b) 15 16 # 求階乘函式,採用遞迴演算法,較複雜,不能作為匿名函式傳入上面的通用求和引數,因此需要預先定義 17 def factor(x): 18 if x == 0: 19 return 1 20 else: 21 return x * factor(x - 1) 22 23 # 求公式一函式,即求a~b的和 24 # 作為sum函式的第一個引數的函式就是返回變數自身,較簡單,採用匿名函式 25 def sumInts(a, b): 26 return sum(lambda x: x, a, b) 27 28 # 求公式二函式,即求a^2 ~ b^2的和 29 # 作為sum函式的第一個引數的函式就是返回變數的平方,較簡單,採用匿名函式 30 def sumSquares(a, b): 31 return sum(lambda x: x * x, a, b) 32 33 # 求公式三函式,即求a! ~ b!的和 34 # 作為sum函式的第一個引數的函式就是求變數的階乘,較複雜(本身是遞迴函式),採用定義好的函式factor 35 def sumFactors(a, b): 36 return sum(factor, a, b) 37 38 print("1+2+3+4+5=%d" % sumInts(1, 5)) 39 print("1^2+2^2+3^2+4^2+5^2=%d" % sumSquares(1, 5)) 40 print("1!+2!+3!+4!+5!=%d" % sumFactors(1, 5))
從上面解決同一個問題的程式碼量比較來看,Scala和Python比較短,而Java比較長,而且Java對函數語言程式設計的支援目前還比較有限,因此函數語言程式設計建議採用Scala或者Python。
&n