擴展 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 的函數名稱,而 K
是 F/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 檔案的程式不會理解這一點,直到它們被修訂。
正如在「動機」下所述,即使你沒有在任何子句主體中使用名稱,命名函數也很有用。這表示我們可以分階段交付此功能。
讓剖析器識別 fun 名稱並檢查它們的識別。如果 fun 名稱在主體中使用,則讓它報告錯誤。在任何下游工具看到之前,讓它從 AST 中刪除 fun 名稱。
在這個階段,fun 名稱可以作為文件。
升級下游工具,以識別擴展的 'fun'
AST 節點,其中有兩個額外欄位:fun 名稱作為原子,以及一個標記,指示它是否未使用、僅在尾部位置使用,或更普遍地使用。
升級剖析器以報告 fun 名稱,但保留它們未使用的檢查。測試下游工具。
修改編譯器以使用新式、更安全的產生名稱形式。確保產生的名稱僅透過介面存取,因此所有內容都是一致的。
在這個階段,fun 名稱有助於降低程式碼修訂所帶來的風險,這些修訂會新增、移除或重新排序 fun;不改變函數中具有特定名稱的 fun 數量的變更,不應改變其名稱。
(選擇性)修訂程式碼產生器,以接受尾呼叫位置的 fun 名稱並產生跳轉。修改剖析器以允許這樣做。
此時,可以將迴圈作為參數傳遞,例如列表遍歷或二元搜尋。Erlang 詞彙或 BEAM 引擎的表示形式尚未需要任何變更。
新增一個新標籤。修訂 funcall 指令以在現有檢查失敗時檢查它,並推送閉包本身。新增一個新指令來建立新的閉包。修訂 Erlang 詞彙表示形式以編碼遞迴 fun。修訂類型測試指令以識別新值。教導 HiPE 該怎麼做。
這是最後一個階段。
此草稿中沒有。第一階段可以相當容易地完成。第二階段對我來說很困難,因為我甚至不確定所有相關的模組是什麼。
無。
本文檔已置於公有領域。