開始寫函數

本篇要介紹在函數式語言與程式設計所講的函數,然後才介紹 Erlang 函數怎麼寫。曾經學過一點程式語言的人,可能都會覺得知道函數怎麼寫,認為不用再學。但是函數式語言所講的函數,可能跟一般所以為程式語言所該有的函數較不相同。

在函數式程式設計,有一篇文章 “Why Functional Programming Matters" 好好解釋了為什麼叫函數式程式設計、為什麼用函數式程式設計。大意為:函數式程式語言擁有三個特點,

  1. 變數只能賦值一次:這減少了閱讀程式的麻煩。任何變數,您只會在最前面看到變數被賦值為一定的可能值,之後不會有其他動作把變數的值改掉。
  2. 函數沒有狀態:是指函數不會參考到外部的任何東西。函數的狀態很單純,比較容易撰寫與分析。
  3. 高階函數的威力:可以用函數串連功能,把一二個小函數組合成高階函數。高階函數反應了我們處理問題經常是以合理拆解子問題方式進行的解題模式。另一方面,提共了直接解題的另一個選項:如果直接解題太慢,也有可能把問題拆解成幾個子函數,它們組成高階函數的效率比較好。

在看物件導向程式設計的書時,看過一本 Design Patterm Explained 書中提到「物件導向的世界中,凡事都是物件」的概念。所以別說寫成 class 的東西才是一種物件的規格,其實 class 中所包含的一些屬性、方法也都是物件,所以他才解釋出 Bridge Pattern 的意義。比照此例,我們說:「函數式程式設計的世界,萬事萬物都是函數」。雖然以 Erlang 來看並非如此,但起碼當您寫每一件 Erlang 程式,都是以函數為基本單位。

Erlang 函數式程式設計

請認清, 「Erlang 函數式程式設計」只是 Erlang 的一個面向,而不是 Erlang 的全部。

數學中,要定義一個函數時,會用一種句法:

f(x) = 0, if x = 0
f(x) = 1, if x = 1
f(x) = f(x-1) + f(x-2), if x > 1

Erlang 程式會寫成這樣:

f(0) -> 0;
f(1) -> 1;
f(N) when N > 1 -> f(x-1) + f(x-2).

這是一組完整的函數,所以您見到這函數的每一條規則定義首尾相接,用分號區隔並用逗號總結尾。每一條規則分為頭部身體二部份。頭部包括函數名稱與參數,有時會加上限制條件。身體包括函數的內容。每次函數呼叫時,從頭到尾依序比對規則,呼叫端會對每一條規則的頭部做樣式比對,並且檢查參數是否符合限制條件,一旦通過頭部的檢查,就會去執行函數的身體。

如果函數用到遞迴,記得遞迴函數一定分為基底部份遞迴部份。通常基底部份情況相當少、但情況比較特殊,可以將它們的函數規則列在前面,使呼叫時先執行到。遞迴部份可能也不會處理掉全部剩下的定義值域,所以在需要的時候要把限制條件寫好。以上述的費玻那西數 (Fibonacci’s Number) 為例,f(0) 、 f(1) 是基底部份, f(N) 是遞迴部份。而若是呼叫的 f(X) 的 X 小於 0 ,則沒有函數規則處理這種情況,即為例外。

具體的 Erlang 函數式程式設計

接下來要介紹比較具體的層面:怎麼寫一個 Erlang 程式檔案才能送交執行。前面提到 Erlang 程式基本單位是函數,不過,在實務層面, Erlang 程式有二個基本單位,一個是函數,另一個基本單位是模組。每一個最小的 Erlang 程式都要定義在模組中。

-module(arithmetics).
-export([plus/2,minus/2,multiply/2,divide/2]).
plus(X,Y) -> X+Y.
minus(X,Y) -> X-Y.
multiply(X,Y) -> X*Y.
divide(X,Y) -> X/Y.

以上,第一行定義模組名稱,模組名稱建議跟檔名相同,並且要以小寫字母開頭、中間需要時加底線做分隔符號。以 arithmetics 為例,檔名就是 arithmetics.erl 。第二行 -export([ … ]) 定義模組對外開放哪些函數,有放出的函數可讓外界以 arithmetics: 標頭呼叫本模組的函數,例如, arithmetics:plus(1,2) 。沒有放出的函數,則是只讓本模組內部自己使用。

高階函數

高階函數就是接受另一個函數當參數的函數。舉個最簡單的例子,數學上有個稱為函數組合 (function composition) 的東西:

(fg)(x) ≡ f(g(x))

那個 。 符號是一個高階函數,知道 f g 二個函數,就會先用 g 套到參數 x 上,才用 f 套到 g(x) 的結果上。

Erlang 可以這樣寫:

fter(F,G) ->
    fun (X) ->
        A_g = erlang:apply(G, [X]),
        erlang:apply(F, [A_g])
    end.

fter/2 表示要接受二個函數作為參數。而中間的 fun (X) -> … end 句型是匿名函數寫法,指定要接受一個參數 X 。 fter/2 函數先把 G 套到 X 上求解,然後在解答上再套上 F 。

呼叫這個函數,要先知道把函數表達為參數的方法是使用函數簽章 (function signature) 表示。如果有一個函數名字叫 a ,要接受三個參數,它的函數簽章就是 fun a/3 。函數簽章也可以帶有模組名稱,寫成 fun some:a/3 。函數簽章可以用來代表函數,每一個函數都有一個函數簽章。而如果有二個函數簽章相同,執行時會讓 Erlang 平台混淆,因此,如果任何程式有相同的函數簽章,在編譯時, Erlang 會提出警告。

知道了這些,現在可以定義平方和立方為

square(X) ->
        fter(fun multiply/2, fun multiply/2).
cube(X) ->
        fter(fun multiply/2, fun square/1).

匿名函數

函數的原型差不多就是 Alonzo Church 的 Lambda Calculus ,使用 Labmda 表示法來描述各種可能的計算表示型式。 Lambda Calculus 實現在程式語言中,就是匿名函數。

Erlang 有可以定義匿名函數的語法:

fun (參數列) -> 函數內容 end

例如,以下依序是反身函數和恆真函數。

id() ->
        fun (X) -> X end.

always_true() ->
        fun (_) -> true end.

基本上沒太多例子可介紹。此外要說明一點,當遇到 Erlang Lambda 表示法的句子時,它要停下來等待接受參數才開始執行,即使不接受參數也一樣停下來。這造成了所謂惰性計算 (Lazy Evaluation) 的效果。本來 Erlang 求值都是強性計算 (Eager Evaluation) ,意思是凡是處理參數,都要先把參數計算好,然後才算函數整體。不過,將函數的內容部份改成不接受參數的 Lambda 表示法,把這個函數拿去當作另一個函數的參數,對另一個函數而言有惰性計算的效果。例如,下列函數為普通的強性計算。

a(B, C) ->
        ... .

b() ->
        ... .

c() ->
        ... .

其中 b/0 進入無窮遞迴程序。假如我們呼叫 a(b(), c()) 會讓程式無法停止,因為計算過程卡在強性計算,先計算 b() 而遭遇無法停止的處境。但如果將 b/0 修改成:

b() ->
        fun() ->
                ... .

而呼叫改成 a(fun b/0, c()) ,函數 a/2 就有機會先執行,然後看看在 a/2 中要怎麼使用 b/0 函數。

控制惰性計算的技巧,在有一部份無限遞迴呼叫的情況很有用。例如,做程式語言的剖析器時,因為語法結構通常是一部份有無窮遞迴而另一部份會剖析到結尾辭彙,可能會卡在部份遞迴呼叫的情況,就要使用沒有參數的 Lambda 表示法讓參數的執行期變得晚一點。

以上,我介紹了 Erlang 函數式程式設計的意義、函數的寫法、高階函數,以及匿名函數、與惰性或強性計算的概念。請慢慢享受 Erlang 計算的強大威力。

廣告

About 黃耀賢 (Yau-Hsien Huang)

熱愛 Erlang ,並且有相關工作經驗。喜歡程式語言。喜歡邏輯。目前用 Python 工作。
本篇發表於 Erlang, Introduction。將永久鏈結加入書籤。

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

您的留言將使用 WordPress.com 帳號。 登出 / 變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 / 變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 / 變更 )

Google+ photo

您的留言將使用 Google+ 帳號。 登出 / 變更 )

連結到 %s