作者
Richard A. O'Keefe <ok(at)cs(dot)otago(dot)ac(dot)nz>
狀態
草案
類型
標準追蹤
建立日期
2008-07-15
Erlang 版本
OTP_R12B-4

EEP 15:可攜式 funs #

摘要 #

目前的 Erlang 有兩種 funs。「外部」fun,Module:Name/Arity,只是一個名稱,可以自由使用。「本地」fun 包含綁定到定義它的模組中的程式碼。這表示您無法將內部 funs 儲存在資料庫中或傳送到遠端系統並期望它們正常運作。

我提議使用一種「可攜式 fun」,這是一種語法上受限的 fun。此限制確保程式設計師知道(並且執行階段可以發現)確切需要哪些模組。這些 funs 可以安全地傳送到遠端節點,並且可以安全地儲存在資料庫中,稍後檢索並執行。當它來自的模組被卸載時,持有對此類 fun 的參考的程序也不需要被終止。

為了達到最佳速度,需要一種新的方式來實作這些 funs,因此這是一個相當大的變更。然而,一個解釋可攜式函式的原型是可行的。

規格 #

目前,Erlang 有

fun_expr -> 'fun' fun_clauses 'end' : ...

我們新增

fun_expr -> 'fun' '!' fun_clauses 'end' : ...

並做出以下限制

  1. 可攜式 fun 可能不包含普通的 funs。
  2. 可攜式 fun 可能不包含沒有模組前綴的呼叫 f(…),除非 f 是內建函式。
  3. 可攜式 fun 可能不包含任何形式為 M:f(…) 或 m:F(…) 或 M:F(…) 的呼叫。
  4. 可攜式 fun 可能不包含任何形式為 F(…) 的呼叫,除非 F 在其標頭中繫結。
  5. 在可以使用抽象模式的系統中,它們的限制方式與函式呼叫相同。

這些限制的目的是確保每次呼叫都是呼叫內建函式、已知模組的已知匯出,或是以參數形式接收的某種 fun。

內建函式 erlang:fun_info/1 在以下方面進行擴充

  1. 在 {type,Type} 項目中,Type 可以是 ‘portable’。
  2. 在可攜式 fun 的 {module,Module} 項目中,Module 將會存在,但實際上可攜式 fun 與該名稱的任何模組之間將沒有其他關聯。
  3. 在可攜式 fun 的 {name,Name} 項目中,Name 將始終為 []。
  4. 不會針對 ‘portable’ fun 返回為 ‘local’ fun 指定的任何項目。
  5. {calls,List} 將針對可攜式 fun 返回,其中 List 是一對 {Module,Imports} 的列表,其中在 fun 中遠端呼叫中使用的每個模組都列出一次,而 Imports 是一個 {Name,Arity} 配對的列表,如 *:module_info/0 中報告的那樣。這允許可攜式 fun 的接收者確定需要載入哪些模組以及它們預期匯出哪些函式。
  6. 為保持一致性,erlang:fun_info(fun M:F/A, calls) => [{M,[{F,A}]}]

內建函式 erlang:fun_info/2 也以類似方式擴充。此函式提供額外的鍵 ‘source’。

fun_info(Fun, source) #

  • 對於本地 fun,結果為 ‘undefined’。
  • 對於外部 fun,結果是剖析器為 fun M:F/A 返回的抽象語法樹。
  • 對於可攜式 fun,結果是剖析器為 fun!….. end 形式返回的抽象語法樹。

內建函式和保護述詞 erlang:is_function(Term) 和 erlang:is_function(Term, Arity) 接受可攜式 funs 以及外部和本地 funs。

提供兩個新的內建函式和保護述詞 erlang:is_portable_function(Term) 和 erlang:is_portable_function(Term, Arity),它們可以識別 ‘portable’ 和 ‘external’ 函式。

(此提案絕對需要修改以使名稱更清楚。)

動機 #

假設您有一個 Erlang 節點向其他節點上的客戶端報告事件。客戶端希望只接收少數事件。一種方法是讓報告器將所有事件傳送給所有客戶端,並讓客戶端進行篩選。更好的方法是讓客戶端告訴報告器他們想要哪些事件,然後讓它只傳送有趣的事件。但是客戶端如何告訴報告器他們對哪些事件感興趣呢?

一種方法是簡單地設定一組固定的事件類別。這太粗糙了。

另一種方法是定義一種事件描述語言,或許以某種方式基於匹配規範。這樣更好,但目前沒有辦法編譯匹配規範(這也是它的另一個用途!),所以匹配很慢,而且仍然有限;報告器可能想要提供篩選器可以使用的摘要函式。

另一種方法是傳送 fun,這實際上是顯而易見的做法。不幸的是,目前這無法運作,而且有不應該運作的原因。(例如,本地函式的主體可能已經進行了函式的內聯擴展,而這些函式在接收節點上的定義不同。)

另一種方法是以二進位檔形式傳送整個模組。這樣會有點笨重。它還會在報告器中產生管理可能大量模組的問題。除非報告器進行大量工作來驗證程式碼的安全性,否則它也是不安全的。從長遠來看,如果客戶端和報告器沒有使用完全相同的 BEAM(或其他 VM),也會產生版本偏差問題。

舉另一個例子,考慮將函式儲存在資料庫中。由於本地 fun 與特定模組的特定版本繫結,如果您在這個月儲存一個函式,升級您的系統,並在下個月還原模組,您不能期望它會正常運作。這表示,例如,您無法將二進位檔與知道如何解碼它的函式一起儲存。

再舉一個例子,考慮類似資料庫的東西,它會動態接收 matchspec(或類似 matchspec 的東西),並希望將其應用於數百萬筆記錄。將 matchspec 轉換為 Erlang 程式碼,甚至編譯結果都很容易,但現在您有一個要管理的模組,而不是一個可以透過垃圾回收器清理的簡單東西。

基本上,此提案的目標是讓 Erlang 在「函式是資料」的函式程式設計道路上更進一步。

然而,必須以這樣的方式來完成,即接收可攜式 fun 的程序不必完全信任來源。接收者必須能夠檢查可攜式 fun,而不僅僅是呼叫它。

原理 #

僅在現有的 fun 語法之上新增可攜性限制並不是一個好主意。這會破壞大多數使用 funs 的程式。

或許最明顯的做法是使用 #fun…end,因為 sharp 似乎是 Erlang 的「哎呀,我們在美好的舊時光中沒有想到這個」標記,就像它在 Common Lisp 中一樣。然而,我們想要使用該表示法表示匿名抽象模式,而且在任何情況下,sharp 在這個內容中都不是圖示。

bang 用於表示這是一種您可能想要傳送的 fun,事實也確實如此。至於它的放置位置,bang 被認為是對 ‘fun’ 關鍵字的後修改,而不是對引數列表的前修改,因此

fun!({a,X}) -> ...
   ;({b,Y}) -> ...
end

沒有重複的 bang。

當您傳送可攜式 fun 時,您會傳送什麼?

  • 當然是環境
  • 當然是某種標頭
  • 但程式碼看起來像什麼?

如果它是原生程式碼,您就無法將 fun 從 SPARC 傳送到 Mac。如果它是 BEAM 程式碼,您就無法將 fun 傳送到另一個系統,除非它具有完全相同的 BEAM 版本。在這兩種情況下,您都讓想要檢查程式碼的謹慎接收者感到非常困難。如果它是原始程式碼,則

  • 可以(延遲地!)編譯為 BEAM(或其他 VM)
  • 可以解釋
  • 可以除錯
  • 可以檢查
  • 我們不必擔心編譯器如何處理推導式 – 可悲的是,目前的編譯器會產生遞迴輔助函式,這會使事情複雜化,而且可以使用更好的方法

因此,可攜式 fun 的二進位格式將包含原始程式碼樹,可能會像 Kistler 的 Juice 一樣進行壓縮。原生表示法將包含指向 BEAM 程式碼區塊的指標,以及選擇性指向原生程式碼區塊的指標,但這些將在第一次呼叫時填入。

解釋的可能性意味著有一種廉價的方式來實作此 EEP 的原型:始終解釋。這也反對對現有 funs 進行任何更改;我們不想降低它們的速度。

回溯相容性 #

“fun!” 目前是語法錯誤,因此不會影響任何現有程式碼。

當我閱讀 erlang:fun_info/[1,2] 的文件時,程式設計師應該始終將這些函式視為開放式的。現有手冊所承諾的任何內容都沒有被刪除或更改,只提供了新的值。

任何呼叫 erlang:is_portable_function/[1,2] 的現有程式都無法運作,因為沒有這樣的函式。如果模組定義了 is_portable_function/1 或 /2,則不允許在保護中使用,但允許在其他地方使用;這種模組可能會受到影響。如果編譯器在模組中發現任何函式的定義,則應列印警告訊息,並且只使用模組的版本。

參考實作 #

無。

從長遠來看,這至少需要兩件事

  1. 一種 fun 表示法,它將指令保存在不屬於任何模組的二進位檔中,與經典的 Interlisp-D 實作類似,以便可以單獨進行垃圾回收這些 funs。無論如何,這都是理想的。

  2. 一種用於推導式的編譯策略,它像經典的 Pop-2 系統一樣,會產生內聯迴圈,而不是呼叫行外輔助函式。無論如何,這都是理想的;它應該明顯更快。

版權 #

本文件已放入公共領域。