資安

高階函數謎題:雙倍調用

函數是計算機編程的核心。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 語言是最簡潔清晰的。

請思考:

  1. 使用你偏好的其他語言能否實現此練習代碼?如何實現?
  2. 請先思考結果是什麼,再執行代碼驗證。想一想為什麼會是這個結果?

Leave a Reply

Your email address will not be published. Required fields are marked *