Functional Programming for Mathematical Computing

Matthias Endler

數學運算的函數式程式設計

程式語言幫助我們描述問題的通用解法;其結果恰好可供機器執行。每種程式語言都有各自不同的優缺點,原因之一在於其語法與語意會大幅影響能夠輕鬆處理的問題範圍。

tl;dr: 我認為函數式程式設計比更常見的指令式方法更適合用於數學運算。

運用內建抽象處理數學

語言背後的理念(即底層的 programming paradigms(程式設計典範))形塑了圍繞其形成的社群特色。開發者會在語言核心周圍打造出由現成可用的函式庫與框架所組成的獨特生態系。因此,有些語言在特定領域表現更為突出,例如商業應用(可以想到 Cobol),另一些則非常適合系統程式設計(如 C 或 Rust)。

談到以電腦解決數學與數值問題時,可能會想到 Fortran。雖然 Fortran 是一種通用程式語言,但它最為人所知的還是科學運算。當然,該語言當初就是為此目的而創造——故有 Formula Translation(公式翻譯)之名。

它在此領域受歡迎的原因之一,在於其提供了一些內建的領域特定關鍵字來表達數學概念,同時兼顧效能。舉例來說,它為複數提供了專用的資料型別——COMPLEX——以及一個名為 DIMENSION 的關鍵字,該關鍵字與數學術語十分相似,可用來建立陣列與向量。

指令式與函數式風格

內建關鍵字有助於將語言的表達能力擴展到特定的問題領域,但這種做法有嚴重的侷限。無止盡地擴充語言核心(ad infinitum)並不可行;只會讓語言更難維護、也更難學習。因此,多數語言提供了其他抽象化方式——例如函式副程式類別物件——來將常式拆解為更小、更易於管理的部分。這些機制或許有助於控制程式的複雜度,但在處理數學問題時,必須小心不要讓樣板程式碼模糊了真正的解法。

範例一——階乘

舉例來說,假設要將下列計算正整數 n 階乘的公式轉譯為程式碼:

階乘的數學定義:n! = 1 * 2 * 3 … * n

使用指令式風格的 Java 來實作上述公式,程式碼可能如下:

public static long fact(final int n) {
    if (n < 0) {
        // Negative numbers not allowed
        return 0;
    }
    long prod = 1;
    for (int i = 1; i <= n; ++i) {
        prod *= i;
    }
    return prod;
}

對於如此簡短的問題定義而言,這是一個相當冗長的解法。(請注意,刻意撰寫了從 1 到 n 的顯式迴圈版本;遞迴函式會更簡短,但使用了一個數學公式本身並未引入的概念。)

此外,該程式包含了許多語言特定的關鍵字,例如 publicstaticSystem.err.println()。除此之外,程式設計師還必須為所有使用的變數明確指定資料型別——這是一項繁瑣的義務。

這一切都模糊了數學定義本身。

相較之下,請看看以下以 Haskell 這類函數式語言撰寫的版本。

fact n = product [1..n]

這幾乎是將問題定義直接轉譯為程式碼。它不需要顯式型別、暫存變數,也不需要存取修飾子(例如 public)。

範例二——內積

有人可能會說,上述 Haskell 程式之所以簡潔,是因為該語言恰好為該特定任務提供了合適的抽象(也就是 product 關鍵字與 [1..n] 區間語法)。因此,讓我們來檢視一個在 Haskell 與 Java 中皆未內建的簡單函式:兩個向量的內積。其數學定義如下:

向量內積的數學定義:a·b= aibi =a1b1+a2b2+···+anbn =abT

對於三維向量,可寫為

三維向量的內積:a·b = a1 * b1 + a2 * b2 + a3* b3

首先是 Haskell 的實作:

type Scalar a = a
data Vector a = Vector a a a deriving (Show)
dot :: (Num a) => Vector a -> Vector a -> Scalar a
(Vector a1 a2 a3) `dot` (Vector b1 b2 b3) = a1*b1 + a2*b2 + a3*b3

請注意,數學型別各可用一行來定義。進一步請注意,我們以中綴表示法定義 dot 函式,也就是將 dot 的第一個引數置於函式名稱之前、第二個引數置於其後。如此一來,程式碼看起來更貼近其數學等價形式。上述函式的呼叫範例如下:

(Vector 1 2 3) ’dot’ (Vector 3 2 1)

這樣的寫法簡短、精確且易於閱讀。

接下來是 Java 中類似的實作。

public static class Vector<T extends Number> {
    private T x, y, z;

    public Vector(T x, T y, T z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public double dot(Vector<?> v) {
        return (x.doubleValue() * v.x.doubleValue() +
                y.doubleValue() * v.y.doubleValue() +
                z.doubleValue() * v.z.doubleValue());
        }
    }

    public static void main(String[] args) {
        Vector<Integer> a = new Vector<Integer>(3, 2, 1);
        Vector<Integer> b = new Vector<Integer>(1, 2, 3);
        System.out.println(a.dot(b));
    }
}

若要為 Vector 提供適當的文字表示,還需要覆寫 toString() 方法。而在 Haskell 中,如程式碼所示,只需衍生自 Show typeclass(型別類別)即可。

建立新的抽象

若函式與型別不足以撰寫直觀的程式,Haskell 還提供了簡單的結構來建立新的運算子與關鍵字,進而擴充語言核心本身。這使得 domain-specific-languages(領域特定語言)得以實現,並讓開發者能更直接地處理實際問題,而非繞過程式語言本身的特殊限制(例如記憶體管理或陣列迭代)。Haskell 擁抱此一概念;Java 則沒有此類功能。

結論

我並非在此貶低 Java 或吹捧 Haskell。兩種語言各有其適用之處。我之所以選擇 Java,僅僅是因為許多程式設計師都能讀懂它。

這項比較更在於針對數值與符號運算,採取函數式與指令式方法之間的差異;就此而言,我每天都會選擇函數式方法。它能去除雜亂、產生優雅的解法。它提供了在高度抽象層次上工作、並以數學語言表達的便利方法,然而這些優勢仍被許多程式設計師所忽視。

Abraham H. Maslow(亞伯拉罕·H·馬斯洛)在其 1966 年出版的 The Psychology of Science(《科學心理學》)一書中的觀察似乎十分貼切:

「我猜想,如果你唯一的工具是一把槌子,你就會傾向於把所有東西都當成釘子來處理。」

原文由 Matthias Endler 發布

本文章由 muse-spark-1.2-contributor 進行翻譯