函數是計算機程式設計的核心。Scheme 等 Lisp 系程式設計語言提供了完善的函數功能。如可在任意代碼位置建立函數,一個函數的參數或傳回值也可以是函數,這種函數稱為高階函數 (Higher-Order Function) 。合理使用高階函數可幫助 建構合理的抽象層次 ,更好的複用代碼, 提升代碼可讀性和健壯性 。一些語言将這些特性稱為函數式程式設計。
C 語言不一樣, C 專注簡單和高性能,一躍成為并穩居最廣泛使用的程式設計語言老大。天下語言多為 C 系, Lisp 系語言雖使用者數量不多,但仍保持其強韌的生命力。如今這兩系語言有互相融合 (互相吸收優點) 的趨勢, Scheme 有 ChezScheme 這樣的高性能實作,許多 C 系語言也在積極引入和完善函數功能支援,甚至紮根底層的 C++ 也在 C++ 11 引入了 lambda 等許多函數式程式設計特性。
MIT 著名程式設計入門教材《計算機程式的構造和解釋》(《
Structure and Interpretation of Computer Programs 》,英文名簡稱 SICP) 練習 1.41 給出了一道高階函數練習題,看起來似乎簡單,做起來才發現暗藏玄機。練習
練習題目使用 Scheme 語言描述如下:
;; 定義一個函數 inc , 傳回其參數增加 1 後的值.
(define (inc x) (+ x 1))
;; 定義一個高階函數 double , 其傳回函數将連續調用兩次輸入函數 (雙倍調用) .
(define (double f)
(lambda (x) (f (f x))))
;; 請問: 下清單達式的值是多少 ?
(((double (double double)) inc) 5)
打開 Scheme 解釋器,依次輸入上述 3 條語句,即可揭曉答案。
高階函數是一個通用概念,這個練習不局限于 Scheme 或 Lisp 系語言,使用其他程式設計語言 (支援函數式程式設計的) 也很容易重寫這個練習。
JavaScript
使用 JavaScript 可簡單重寫此練習代碼如下。許多語言 (主要是 C 系語言?) 中 double 通常表示雙精度浮點數類型,是以我們将此函數重命名為 doubleApplied 以避免沖突。
function inc(x) {
return x + 1;
}
function doubleApplied(f) {
return function (x) { return f(f(x)); };
}
console.log(doubleApplied(doubleApplied(doubleApplied))(inc)(5));
将以上代碼儲存為腳本檔案 "double-applied.js" ,安裝 Nodejs 後執行
node double-applied.js
即可看到結果。或者直接打開浏覽器 Web 開發者工具,在 Web 控制台輸入執行以上 JavaScript 代碼即可看到結果。
C++
C++ 11 引入了許多函數式程式設計特性,此練習可使用 C++ 代碼重寫如下。C++ 是靜态類型語言,是以增加了一些靜态類型适配代碼。使用 C++ 模闆可編寫類型安全的通用模闆函數 doubleApplied ,編譯時将校驗此函數的參數必須是一個函數 (
std::function
) 。
#include <iostream>
#include <functional>
int inc(int x) {
return x + 1;
}
template<typename T>
std::function<T(T)> doubleApplied(std::function<T(T)> f) {
return [=](T x) { return f(f(x)); };
}
int main(int argc, char *argv[]) {
// doubleApplied 參數類型為 C++ 标準 std::function 類型,
// 調用 doubleApplied 前先将輸入參數轉換為此類型.
// 将普通原生函數 inc 轉換為 std::function 類型.
std::function<int(int)> inc{::inc};
// 将參數類型為 std::function<int(int)> 的 doubleApplied 模闆函數執行個體 (模闆參數為 int) 轉換為 std::function 類型.
std::function<std::function<int(int)>(std::function<int(int)>)> doubleAppliedFnInt{::doubleApplied<int>};
std::cout << doubleApplied(doubleApplied(doubleAppliedFnInt))(inc)(5) << std::endl;
}
CentOS 7 和 Ubuntu 16.04 及以上版本自帶的 g++ 編譯器均已支援 C++ 11 。将以上完整代碼儲存為檔案 "double-applied.cxx" ,執行如下指令編譯執行此程式,即可看到輸出結果。
g++ -std=c++11 double-applied.cxx -o double-applied && ./double-applied
Java
Java 8 起支援使用函數式接口和 lambda 表達式等進行函數式程式設計,此練習可使用 Java 代碼重寫如下。
import java.util.function.UnaryOperator;
public interface DoubleApplied {
static Integer inc(Integer x) {
return x + 1;
}
static <T> UnaryOperator<T> doubleApplied(UnaryOperator<T> f) {
// Java 函數式接口需要使用 apply 等方法調用.
return (x) -> f.apply(f.apply(x));
}
static void main(String[] args) {
// 将參數為 UnaryOperator<Integer> 類型的 doubleApplied 方法轉換為函數式接口 (類似 C++ 模闆函數執行個體化) .
UnaryOperator<UnaryOperator<Integer>> doubleAppliedFnInt = DoubleApplied::doubleApplied;
System.out.println(doubleApplied(doubleApplied(doubleAppliedFnInt)).apply(DoubleApplied::inc).apply(5));
}
}
Java 11 支援直接執行 Java 源碼檔案,即自動在記憶體中将 Java 源碼編譯為位元組碼并執行之。将以上完整代碼儲存為檔案 "DoubleApplied.java" ,執行
java DoubleApplied.java
即可看到輸出結果。
思考
不同程式設計語言隻是文法的不同,所表達的意思是相同的。使用 Scheme JavaScript C++ Java 幾種語言實際驗證,得到了相同的結果 (不同才有問題) 。從文法角度看, Scheme 語言是最簡潔清晰的。
請思考:
- 使用你偏好的其他語言能否實作此練習代碼?如何實作?
- 請先思考結果是什麼,再執行代碼驗證。想一想為什麼會是這個結果?