函數式編程意味着使用函數來創建干凈且可維護的軟件的最佳效果。 本文通過 JavaScript 和 Java 中的實際示例說明函數范式背后的概念。
函數式編程從一開始就一直是軟件開發的弄潮兒,但在現代賦予了新的涵義。 本文着眼於函數式編程背后的概念,並通過 JavaScript 和 Java 中的示例提供實際理解。
函數式編程定義
函數是代碼組織的基礎; 它們存在於所有高階編程語言中。 一般來說,函數式編程意味着使用函數來創建干凈且可維護的軟件的最佳效果。 更具體地說,函數式編程是一組編碼方法,通常被描述為一種編程范式。
函數式編程有時被定義為與面向對象編程 (OOP) 和過程式編程相對立。 這是一種誤導,因為這些方法並不是相互排斥的,而且大多數系統傾向於同時使用這三種方法。
函數式編程在某些情況下提供了明顯的好處,它在許多語言和框架中被大量使用,並且在當前的軟件趨勢中很突出。 它是一個有用且強大的工具,應該成為每個開發人員的概念和語法工具包的一部分。
純函數
函數式編程的理想是所謂的純函數。 純函數是一種結果僅取決於輸入參數的函數,並且其操作不會引發副作用,即除了返回值之外不產生任何外部影響。
純函數的美妙之處在於其架構的簡單性。 因為一個純函數被簡化為只有參數和返回值(即它的 API),所以它可以被視為一個復雜性的終點:它與它運行的外部系統的唯一交互是通過定義的 API。
這與 OOP 形成對比,在 OOP 中,對象方法被設計為與對象的狀態(對象成員)交互,與過程式代碼形成對比,后者通常從函數內部操縱外部狀態。
然而,在實際實踐中,函數最終往往需要與更廣泛的上下文進行交互,正如 React 的 useEffect 鈎子所證明的那樣。
不變性
函數式編程哲學的另一個原則是不在函數外修改數據。實際上,這意味着避免修改函數的輸入參數。相反,函數的返回值應該反映完成的工作。這是一種避免副作用的方法。當函數在更大的系統中運行時,它可以更容易地推斷函數的影響。
First Class Functions
除了純函數特點之外,在實際編碼實踐中,函數式編程取決於First Class Functions. First Class Functions是被視為“事物本身”的函數,能夠獨立存在並被獨立處理。函數式編程試圖利用語言支持將函數用作變量、參數和返回值來創建優雅的代碼。
因為First Class Functions是如此靈活和有用,即使是像 Java 和 C# 這樣的強 OOP 語言也已經轉向合並 First Class Functions 支持。這就是 Java 8 支持 Lambda 表達式背后的推動力。
另一種描述First Class Functions的方法是將函數作為數據。也就是說,可以像任何其他數據一樣將First Class Functions分配給變量。當您編寫 let myFunc = function(){} 時,您正在使用函數作為數據。
高階函數
接受函數作為參數或返回函數的函數稱為高階函數——對函數進行運算的函數。
近年來,JavaScipt 和 Java 都添加了改進的函數語法。 Java 添加了箭頭運算符和雙冒號運算符。 JavaScript 添加了箭頭運算符。 這些運算符旨在使定義和使用函數更容易,尤其是作為匿名函數內聯。 匿名函數是在沒有給定引用變量的情況下定義和使用的函數。
函數式編程示例:集合
也許函數式編程最突出的例子是處理集合。 這是因為能夠針對集合中的元素應用多種不同的功能函數,這是純函數思想的自然契合。
考慮清單 1,它利用 JavaScript map() 函數將數組中的字母大寫。
Listing 1. Using map() and an anonymous function in JavaScript
let letters = ["a", "b", "c"];
console.info( letters.map((x) => x.toUpperCase()) ); // outputs ["A", "B", "C"]
這種語法的美妙之處在於代碼非常集中。 不需要命令式管道,例如循環和數組操作。 這段代碼清楚地表達了正在做的事情的思考過程。
使用 Java 的箭頭運算符也可以實現相同的效果,如清單 2 所示。
Listing 2. Using map() and an anonymous function in Java
import java.util.*;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toList;
//...
List lower = Arrays.asList("a","b","c");
System.out.println(lower.stream().map(s -> s.toUpperCase()).collect(toList())); // outputs ["A", "B", "C"]
清單 2 使用 Java 8 的stream庫來執行轉換為大寫字母列表的相同任務。 請注意,核心箭頭運算符語法實際上與 JavaScript 相同,它們做同樣的事情,即創建一個接受參數、執行邏輯並返回值的函數。 (需要注意的是,如果這樣定義的函數體周圍缺少大括號,則自動給出返回值。)
繼續使用 Java,考慮清單 3 中的雙冒號運算符。該運算符允許您引用類上的方法:在本例中,是 String 類上的 toUpperCase 方法。 清單 3 的作用與清單 2 相同。不同的語法適用於不同的場景。
Listing 3. Java Double Colon Operator
// ...
List upper = lower.stream().map(String::toUpperCase).collect(toList());
在上面的所有三個示例中,您可以看到高階函數在起作用。 兩種語言中的 map() 函數都接受一個函數作為參數。
換句話說,您可以將函數傳遞給其他函數(在 Array API 中或以其他方式)作為函數接口。 提供者函數(使用參數函數)是通用邏輯的插件。
這看起來很像 OOP 中的策略模式(實際上,在 Java 中,在幕后生成了具有單個方法的接口),但是函數的緊湊性使得組件協議非常緊湊。
作為另一個示例,請考慮清單 4,它在 Node.js 的 Express 框架中定義了一個路由處理程序。
Listing 4. Functional route handler in Express
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('One Love!');
});
清單 4 是函數式編程的一個很好的例子,因為它允許對映射路由和處理請求和響應所需的確切定義進行清晰的定義——盡管可能有人認為在函數體內操作響應對象是一種副作用 .
Curried 函數
現在考慮返回值是函數的函數式編程概念。 與作為參數的函數相比,這種情況不太常見。 清單 5 有一個來自常見 React 模式的示例,其中鏈接了粗箭頭語法。
Listing 5. A curried function in React
handleChange = field => e => {
e.preventDefault();
// Handle event
}
上述代碼的目的是創建一個事件處理程序,它將接受一個字段為參數,然后是事件。 這很有用,因為您可以將相同的 handleChange 應用於多個字段。 簡而言之,同一個處理程序可用於多個字段。
清單 5 是一個柯里化函數的示例。 “Curried函數”這個名字有點令人沮喪。 它是為了紀念一個人,這很好,但它沒有描述這個概念,這會引發困惑。 無論如何,我們的想法是,當您有返回函數的函數時,您可以將調用鏈接在一起,這比創建具有多個參數的單個函數更靈活。
在調用這些類型的函數時,您會遇到獨特的“鏈式括號”語法:handleChange(field)(event)。
大范圍編程
前面的示例提供了在重點上下文中對函數式編程的動手理解,但函數式編程旨在為大型編程帶來更大的好處。 換句話說,函數式編程旨在創建更緊湊、更具彈性的大型系統。
很難提供這方面的例子,但一個真實的例子是 React 推廣功能組件的舉措。 React 團隊已經注意到,組件的更簡潔的功能風格提供了界面架構變得更大而組合的好處。
另一個大量使用函數式編程的系統是 ReactiveX。 建立在 ReactiveX 使用的那種事件流上的大型系統可以從解耦的軟件組件交互中受益。 Angular 全面采用 ReactiveX (RxJS) 作為對這種能力的認可。
變量范圍和上下文
最后,作為范式不一定是函數式編程的一部分,但在進行函數式編程時需要注意的一個問題是變量范圍和上下文。
在 JavaScript 中,上下文特指 this 關鍵字解析的內容。 在 JavaScript 箭頭運算符的情況下, this 指的是封閉上下文。 使用傳統語法定義的函數接收其自己的上下文。 DOM 對象上的事件處理程序可以利用這一事實來確保 this 關鍵字引用正在處理的元素。
范圍是指變量的范圍,即哪些變量是可見的。 對於所有 JavaScript 函數(胖箭頭函數和傳統函數)以及 Java 箭頭定義的匿名函數,作用域是封閉函數體的作用域——盡管在 Java 中,只有那些實際上是 final 的變量可以是 訪問。 這就是為什么這些函數被稱為閉包。 該術語意味着函數被包含在其包含范圍內。
記住這一點很重要:這樣的匿名函數可以完全訪問作用域中的變量。 內部函數可以對外部函數的變量進行操作。 這可以被認為是非純函數的副作用。
