Lambda
2004 年 9 月 8 日
由於動態語言越來越受到關注,更多人開始接觸一種稱為 Lambda(也稱為閉包、匿名函數或區塊)的程式設計概念。來自 C/C++/Java/C# 語言背景的人沒有 Lambda,因此不確定它們是什麼。以下是一個簡短的說明,對於已經在具有 Lambda 的語言中進行大量程式設計的人來說,這不會很有趣。
Lambda 已經存在很長一段時間了。我第一次在 Smalltalk 中正確地接觸到它們,它們在那裡被稱為區塊。Lisp 大量使用它們。它們也存在於 Ruby 腳本語言中,而且是許多 Ruby 使用者喜歡使用 Ruby 進行腳本編寫的主要原因。
Lambda 本質上是可以作為引數傳遞給函數呼叫的程式碼區塊。我將用一個簡單的範例來說明這一點。想像我有一個員工物件清單,而且我想要一個清單,其中包含那些是經理的員工,我使用 IsManager 屬性來判斷。使用 C#,我可能會這樣寫。
public static IList Managers(IList emps) { IList result = new ArrayList(); foreach(Employee e in emps) if (e.IsManager) result.Add(e); return result; }
在具有 Lambda 的語言(在本例中為 Ruby)中,我會這樣寫。
def managers(emps) return emps.select {|e| e.isManager} end
select 本質上是定義在 Ruby 集合類別上的方法。它將程式碼區塊(Lambda)作為引數。在 Ruby 中,你可以在大括號之間撰寫該程式碼區塊(這不是唯一的方法)。如果程式碼區塊需要任何引數,你可以在垂直線之間宣告這些引數。select 所做的就是反覆執行輸入陣列,使用每個元素執行程式碼區塊,並傳回區塊評估為 true 的那些元素的陣列。
現在,如果你是一位 C 程式設計師,你可能會想「我可以使用函數指標來做到這一點」,如果你是一位 Java 程式設計師,你可能會想「我可以使用匿名內部類別來做到這一點」。這些機制類似於 Lambda,但有兩個顯著的差異。
第一個是形式上的差異,lambda 通常定義封閉函數,這表示它們可以參照在定義時可見的變數。考慮這個方法
def highPaid(emps) threshold = 150 return emps.select {|e| e.salary > threshold} end
請注意,select 區塊中的程式碼參照封閉方法中定義的局部變數。許多沒有真實封閉函數的語言中 lambda 的替代方案無法做到這一點。lambda 允許您執行更有趣的事情。考慮這個函數。
def paidMore(amount) return lambda {|e| e.salary > amount} end
這個函數傳回一個 lambda,確實,它傳回一個其行為取決於傳入引數的 lambda。我可以建立一個這樣的函數並將它指定給一個變數。
highPaid = paidMore(150)
變數 highPaid
包含一個程式碼區塊,該區塊將傳回受測物件的薪水是否大於 150。我可能會像這樣使用它。
john = Employee.new john.salary = 200 print highPaid.call(john)
表示式 highPaid.call(john)
呼叫我先前定義的 e.salary > amount
程式碼,其中該程式碼中的 amount
變數繫結到我在建立 proc 物件時傳入的 150。即使在發出列印呼叫時該 150 值超出範圍,繫結仍會保留。
因此,關於 lambda 的第一個關鍵點是它們通常會建立封閉函數,也就是一個程式碼區塊加上來自它們所屬環境的繫結。您可以有不會建立封閉函數的 lambda,但這種動物不太有用,因此不太常見,這也是為什麼封閉函數通常用作 lambda 的替代術語。[1][2]
第二個差異比較不像定義明確的形式差異,但在實務上同樣重要,甚至更重要。支援 lambda 的語言允許您使用非常少的語法來定義它們。雖然這可能看起來不是一個重點,但我認為這很重要,這是讓它們經常自然使用的關鍵。看看 Lisp、Smalltalk 或 Ruby 程式碼,您會看到到處都是 lambda,其使用頻率遠高於其他語言中的類似結構。繫結到局部變數的能力是其中的一部分,但我認為最大的原因是使用它們的表示法簡單明瞭。
一個很好的例子是當前 Smalltalk 使用者開始使用 Java 時發生的事情。最初,包括我在內的許多人嘗試使用匿名內部類別來執行許多我們在 Smalltalk 中使用區塊所做的事情。但產生的程式碼實在太過雜亂醜陋,所以我們放棄了。
我在 Ruby 中大量使用 lambda,但我不會傾向於明確建立它們並傳遞它們。我使用 lambda 的大部分時間都是基於集合管線,類似於我先前展示的 select
方法。另一個常見的用法是「執行周圍方法」,例如處理檔案時。
File.open(filename) {|f| doSomethingWithFile(f)}
這裡的 open 方法開啟檔案,執行提供的區塊,然後關閉它。這對於交易(記得提交或回滾)或確實任何在最後必須記得做某事的操作來說,可能是一個非常方便的慣用語。我在 XML 轉換常式中廣泛使用它。
這種使用 lambda 的方式實際上遠少於 Lisp 和函數式程式設計世界中的人們所做的。但即使我使用有限,當使用沒有它們的語言進行程式設計時,我還是非常想念它們。它們是那些當你第一次遇到它們時看似微不足道的事情之一,但你很快就會喜歡它們。
Neal Gafter 在 封閉的歷史中發表了一篇精彩的文章。Vadim Nasardinov 引領我了解了 Guy Steele 關於 Java 中的封閉的這段有趣的歷史。
備註
1: 當我在 2004 年首次發布此 bliki 條目時,我使用術語「封閉」來指這些語言功能。當時「封閉」經常這樣使用。從那以後,「lambda」一詞變得更流行,所以我更改了此 bliki 條目以遵循用法。
2: Java 的匿名內部類別可以存取區域變數 - 但僅限於它們是最終的。