作者
Richard A. O'Keefe <ok(at)cs(dot)otago(dot)ac(dot)nz>
狀態
最終版/R15B 已在 OTP 版本 R15B 中實作
類型
標準追蹤
內容類型
text/plain
建立日期
2011-05-27
Erlang 版本
OTP_R14B04

EEP 37:具名稱的 Fun #

摘要 #

擴展 fun 的語法,允許在每個引數列表之前一致地存在變數名稱。這允許 fun 成為遞迴的。這個節點被綁定在將 fun 應用於其引數的 opcodes 中,因此不需要更改垃圾收集器的設計。

規範 #

目前,fun 有三種形式

fun Name/Arity
fun Module:Name/Arity

fun Fun_Clause {; Fun_Clause}... end

我們加入另一種形式

fun Variable Fun_Clause {; Variable Fun_Clause}... end

如果任何 Fun_Clause 具有 Variable,則所有 Fun_Clause 都必須具有,而且它們必須是相同的變數。與引數列表中的變數一樣,此變數是 fun 的局部變數,不與其上下文共享。在 fun 內部,變數會綁定到 fun 表達式的值。

有幾種可能的方法可以實作此功能。其中一種相當簡潔,因為它保留了垃圾收集器必須處理的資料結構的無循環特性。

一種實作現有 fun 的方法如下

  • a 建立一個具有產生名稱的輔助函數

      <foo>(...,X1,...,Xk) ...;
      ...
      <foo>(...,X1,...,Xk) ....
    

    該函數具有與 fun 相同的引數列表、守護條件和子句主體,但與上下文共享的每個變數都顯示為額外的引數。

  • b 將 fun 表達式翻譯為

      '%mk-fun'({fun <foo>/n+k, X1, ..., Xk})
    

    這會給元組一個特殊標籤,表示它代表一個 fun 值。

  • c 翻譯 Foo(E1,...,Em)

    A1 := E1, ..., Am := Em; funcall_m(Foo),其中 funcall_m 指令檢查其引數是否為預期 m 個引數的閉包,將元組的 X1,...,Xk 欄位移動到引數暫存器 A<m+1>..A<m+k>,然後跳轉到第一個欄位中的位址。

實作遞迴 fun 所需的全部內容是

  • a’ 建立一個輔助函數

      <foo>(...,X1,...,Xk,Variable) ...;
      ...
      <foo>(...,X1,...,Xk,Variable) ....
    
  • b’ 將 fun 表達式翻譯為

      '%mk-rec-fun'({fun <foo>/<n+k+1>, X1, ..., Xk})
    

    它只是應用第二個特殊標籤。

  • c’ funcall_m opcode 對於舊的

    和遞迴 fun 的作用相同,只是在跳轉之前,它將 fun 值 Foo 本身作為引數 A<m+k+1> 新增。這「綁定節點」。

因此,建立遞迴 fun 所需的空間或時間不比現有的 fun 多,而且不會涉及建立任何指標循環。它的程式碼可以插入到 funcall_m 指令的失敗路徑中,無論它們的形式為何。

動機 #

Fun 名稱可以達到三個目的。

首先,它們可以單純地作為文件。例如,

cfun_files(CFun) ->
    fun(F1, F2) ->
        [[?OBJ(T1,_) | _] | _] = F1,
        [[?OBJ(T2,_) | _] | _] = F2,
        CFun(T1, T2)
    end.

可以寫成

cfun_files(CFun) ->
    fun Compare([[?OBJ(T1,_)|_]|_], [[?OBJ(T2,_)|_]|_]) ->
    CFun(T1, T2)
    end.

名稱未使用時,可以將具名 fun 視為不存在名稱來實作。

其次,fun 的名稱可以內建到其產生的名稱中。在撰寫本文時,我們可能有

'-F/N-fun-K-'

其中 F/N 是包含 fun 的函數名稱,而 KF/N 中較早的 fun 的數量。我們可以改為將名稱內建,使用

'-F/N-fun-Name-[K-]'

其中 K 僅當外部函數包含多個具有相同名稱的 fun 時才會出現。這樣做的目的是,這些名稱在熱載入後更有可能有用。例如,如果我們從

f(...Xs, Ys, ...) ->
    ...
    sort(Xs, fun X_Key({_,N,_}) -> N end),
    sort(Ys, fun Y_Key({N,_,_}) -> N end),
    ...

開始,然後我們修訂它,交換對 sort/2 的兩個呼叫。使用具名 fun,兩個 fun 會保留其產生的名稱,而且模組是安全的。使用匿名函數,兩個 fun 有可能交換名稱;糟糕!

第三,Erlang 郵件清單中經常被問到的問題是「為什麼我不能使用遞迴 fun?」,我們現在可以回答,「你可以;它們看起來是這樣。」

這仍然不允許相互遞迴 fun,但人們似乎沒有要求那麼多。

最後,下次有人爭論 Erlang 語法不一致,因為函數子句具有重複的名稱,而 fun 子句沒有時,我們就可以回覆「但是 fun 子句可以有重複的名稱,而且可能應該有。」

原理 #

似乎真的只有兩個主要問題。

fun 名稱變數的範圍應該是什麼?fun 中的某些變數在 fun 和其上下文之間共享。這樣做會讓我們寫出

f(...) ->
    fun G(...) -> ... end,
    fun H(...) -> ... end,
    ... use G and H ...

就像在 Scheme 中使用巢狀「define」一樣,只是當 H 可以使用 G 時,G 不能使用 H

由於你不會以這種方式取得相互遞迴,你不應該被欺騙認為你可能會這樣做。你最好寫出

f(...) ->
    GG = fun G(...) -> ... end,
    HH = fun H(...) -> ... end,
    ... use GG and HH ...

讓你清楚了解你所獲得的內容。

雖然 fun 子句主體中的變數可以與上下文共享,但引數中的變數不會,這是我發現令人困惑的地方。至少這樣,fun 名稱會遵循與其旁邊的引數列表中變數相同的範圍規則。

另一個主要問題是,遞迴 fun 值是否應該與現有 fun 值完全相同的表示形式,但其中有一個循環(在建構時綁定節點),或者是否要引入一個新的標籤(在呼叫時綁定節點)。Erlang 堆積中沒有循環一直是幾個垃圾收集器設計的主要因素。我認為更改這一點的難度會比此提案所需的變更高出一個數量級。看到節點可以在呼叫時(在不減慢對現有 fun 的呼叫速度的情況下)綁定,才讓我敢希望這個提案有一天可能會被接受。

現在的主要問題是,這無法讓我們定義一組相互遞迴的本地函數。現在採用此提案可能會阻礙處理相互遞迴的更好提案。

我認為這樣的提案不太可能很快出現。

有一個特殊情況,其中 fun 名稱僅在尾呼叫位置使用,這可以完全由編譯器產生跳回開頭的跳轉來處理。這完全不需要對執行階段系統產生任何影響。

回溯相容性 #

未使用新功能的程式碼不會改變其意義。可能會有一些程式碼依賴於產生函數名稱的形式;這需要變更。

所有語法工具都需要修改以處理新形式。現有的剖析轉換很可能會在新形式的程式碼上失敗,但在沒有的程式碼上會保持不變。

至少需要一個新的指令來建立適當區分的閉包。現有的分析 BEAM 檔案的程式不會理解這一點,直到它們被修訂。

正如在「動機」下所述,即使你沒有在任何子句主體中使用名稱,命名函數也很有用。這表示我們可以分階段交付此功能。

  1. 讓剖析器識別 fun 名稱並檢查它們的識別。如果 fun 名稱在主體中使用,則讓它報告錯誤。在任何下游工具看到之前,讓它從 AST 中刪除 fun 名稱。

    在這個階段,fun 名稱可以作為文件。

  2. 升級下游工具,以識別擴展的 'fun' AST 節點,其中有兩個額外欄位:fun 名稱作為原子,以及一個標記,指示它是否未使用、僅在尾部位置使用,或更普遍地使用。

    升級剖析器以報告 fun 名稱,但保留它們未使用的檢查。測試下游工具。

  3. 修改編譯器以使用新式、更安全的產生名稱形式。確保產生的名稱僅透過介面存取,因此所有內容都是一致的。

    在這個階段,fun 名稱有助於降低程式碼修訂所帶來的風險,這些修訂會新增、移除或重新排序 fun;不改變函數中具有特定名稱的 fun 數量的變更,不應改變其名稱。

  4. (選擇性)修訂程式碼產生器,以接受尾呼叫位置的 fun 名稱並產生跳轉。修改剖析器以允許這樣做。

    此時,可以將迴圈作為參數傳遞,例如列表遍歷或二元搜尋。Erlang 詞彙或 BEAM 引擎的表示形式尚未需要任何變更。

  5. 新增一個新標籤。修訂 funcall 指令以在現有檢查失敗時檢查它,並推送閉包本身。新增一個新指令來建立新的閉包。修訂 Erlang 詞彙表示形式以編碼遞迴 fun。修訂類型測試指令以識別新值。教導 HiPE 該怎麼做。

    這是最後一個階段。

參考實作 #

此草稿中沒有。第一階段可以相當容易地完成。第二階段對我來說很困難,因為我甚至不確定所有相關的模組是什麼。

參考文獻 #

無。

著作權 #

本文檔已置於公有領域。