作者
Richard A. O'Keefe <ok(at)cs(dot)otago(dot)ac(dot)nz>
狀態
草稿
類型
標準追蹤
建立於
2009年2月25日
Erlang 版本
OTP_R12B-5

EEP 29:抽象模式,第一階段 #

摘要 #

抽象模式是命名的模式/守衛組合,可用於

  • 模式中,以支援抽象資料型態
  • 作為使用者定義的守衛,保證守衛安全
  • 作為普通函式
  • 取代許多但並非所有巨集的使用。

完整的提案共有六個階段,這只是第一階段。這個階段僅允許可由內聯替換處理的簡單抽象模式,因此不需要變更 Erlang 虛擬機。

規格 #

我們引入抽象模式宣告和呼叫。語法是根據 parse.yrl 中的語法調整而來的。

form -> abstract_pattern dot.

abstract_pattern -> '#' atom clause_args clause_guard
                    '->' expr.

為了方便日後參考,我們將使用示意規則

#A(H1, ..., Hn) when G -> B.

其中空的 clause_guard 表示 G 為「true」。H1, ..., HnB 都必須是模式。

抽象模式不得直接或間接遞迴。

expr_700 -> pattern_call.

pattern_call -> '#' atom argument_list

pattern_call 的 argument_list 中的表達式必須是

  • 模式中的模式
  • 守衛中其他地方的守衛表達式
  • 普通表達式中其他地方的任何表達式。

有兩種方式理解抽象模式的語意:作為函式呼叫和作為內聯替換。

如果將第一階段的抽象模式視為函式,則對應於兩個函式。根據我們的示意規則,我們得到

'#A->'(H1, ..., Hn) when G -> B.

也就是說,抽象模式的部分含義是一個函式,它的運作方式就如同它看起來的樣子。(名稱「#A->」僅用於說明,不應按字面意思理解。特別是,此規格並未規定此類函式應可以直接存取,更不用說應以該形式的名稱存取。)因此

#permute([R,A,T]) when is_atom(A) -> [T,A,R].

在一個方向上,它的作用就像

'#permute->'([R,A,T]) when is_atom(A) -> [T,A,R].

一樣。由於抽象模式不允許遞迴,並且不能有任何副作用,因此可以在守衛中安全地呼叫它們。作為守衛測試,#A(E1,...,En) 等同於 (true = '#A->'(E1,...,En))

在另一個方向上,我們得到

'#A='(B) when G -> {H1, ..., Hn}.

模式匹配

#A(P1, ..., Pn) = E

等同於

{P1, ..., Pn} = '#A='(E)

當某些模式 Hi, B 使用「=」時,定義會稍微複雜一點。例如,假設我們有

#foo([H|T] = X) -> {H,T}.

一種簡單的轉換會是

'#foo='({H,T}) -> [H|T] = X.

這會無法運作,因為 X 將是未定義的。這裡的基本問題是,模式中的「=」是對稱的,而表達式中的「=」則不是。真正的轉換必須是

#A(H11=H12=.., ..., Hn1=Hn2=..) when G -> B

等同於

'#A='(B)
when G, X1=H11, X1=H12, ..., Xn=Hn1, Xn=Hn2, ...
-> {X1, ..., Xn}

其中綁定 Xi=Hij 會根據資料流進行排序和重新排序(也就是說,從 Xi=Hij 切換到 Hij=Xi)。在 #foo/1 範例中,我們會得到

'#foo='({H,T}) when X1 = [H|T], X = X1 -> {X1}.

排序和重新排序的過程比聽起來更容易。當有一個等式 Xi=Hij 時,若 Hij 中的每個變數都已知,或者 Xi 已知,則如果 Hij 全都已知,則加入 Xi=Hij,若 Xi 已知,則加入 Hij = Xi

當 B 包含「=」時,也建議在正向方向進行這種依資料流的排序和重新排序。

有時,即使經過依資料流排序和重新排序,也無法建構抽象模式的某個方向。這通常是因為一方包含另一方沒有出現的變數。例如,

#first(X) -> {X,_}.
#second(Y) -> {_,Y}.

可以用作模式,但不能用作函式。編譯器應為此類抽象模式發出警告,但允許它們。將此類模式作為函式呼叫應為執行階段錯誤。應該可以抑制警告,或許可以透過

-compile({pattern_only,[{first,1,second,1}]}).

(這在目前的語法範圍內。理想情況下應該是 #first/1#second/1。)

另一個範例,

#is_date(#date(_,_,_)) -> true.

可以用作函式,即使/尤其在守衛中,但不能用作模式。編譯器應為此類抽象模式發出警告,但允許它們。將此類模式作為呼叫也應為執行階段錯誤。應該可以抑制警告,或許可以透過

-compile({function_only,[{is_date,1}]}).

透過內聯替換進行定義很簡單。以下所有重寫都假設變數的標準重新命名。

f(... #A(P1,...,Pn) ...) when Gf -> Bf

重寫為

f(... B ...)
when G, Xi=Hij..., {P1,...,Pn} = {X1,...,Xn}, Gf -> Bf

case ... of ... #(P1,...,Pn) ... when Gc -> Bc

重寫為

case ... of ... B ...
when G, Xi=Hij..., {P1,...,Pn} = {X1,...,Xn}, Gc -> Bc

P = E

重寫為

case E of P -> ok end

在守衛表達式中,

(... #A(E1, ..., En) ...)

重寫為

{H1,...,Hn} = {E1,...,En}, G, (... B ...)

作為守衛測試,

#A(E1, ..., En)

重寫為

{H1,...,Hn} = {E1,...,En}, G, true = B

作為普通表達式,

#A(E1, ..., En)

重寫為

case {E1,...,En} of {H1,...,Hn} when G -> B end

動機 #

即使在這種受限制的形式下,抽象模式也解決了 Erlang 郵件清單上不斷出現的許多問題。發明它們主要有兩個目的:大幅減少對前處理器的需求,以及支援抽象資料型態的使用。事實證明,它們還可以減少程式設計師必須做的鍵盤工作量,並增加編譯器可用的型態資訊量。

巨集通常用於提供具名常數。例如,

-define(unknown, "UNKNOWN").
f(?unknown, Actors) -> Actors;
f(N, Actors) -> lists:keydelete(N, #actor.name, Actors).

這裡不使用函式,因為函式呼叫可能不會出現在模式中。抽象模式是受限制的函式,它們可以出現在模式中

#unknown() -> "UNKNOWN".
f(#unknown(), Actors) -> Actors;
f(N, Actors) -> lists:keydelete(n, #actor.name, Actors).

有時這些常數必須計算出來。例如,

-define(START_TIMEOUT, 1000 * 30).

由於守衛中的變數綁定,我們也可以做到

#start_timeout() when N = 1000*30 -> N.

有些巨集無法做到,因為需要守衛測試以及模式。巨集無法在兩個地方出現。

#date(D, M, Y)
when is_integer(Y), Y >= 1600, Y =< 2500,
     is_integer(M), M >= 1,    M =< 12,
     is_integer(D), D >= 1,    D =< 31
-> {Y, M, D}.

#vector3(X, Y, Z)
when is_float(X), is_float(Y), is_float(Z)
-> {X, Y, Z}.

#mod_func(M, F) when is_atom(M), is_atom(F) -> {M, F}.

#mod_func_arity(M, F, A)
when is_atom(M), is_atom(F), is_integer(A), A >= 0
-> {M, F, A}.

有些巨集無法被抽象模式取代。

-define(DBG(DbgLvl, Format, Data),
    dbg(DbgLvl, Format, Data)).

無法成為抽象模式,因為右手邊涉及對普通函式的呼叫。

有些巨集定義守衛測試。例如,

-define(tab, 9).
-define(space, 32).
-define(is_tab(X), X == ?tab).
-define(is_space(X), X == ?space).
-define(is_underline(X), X == $_).
-define(is_number(X), X >= $0, X =< $9).
-define(is_upper(X), X >= $A, X =< $Z).
-define(is_lower(X), X >= $a, X =< $z).

token([X|File], L, Result, Gen, BsNl)
  when ?is_upper(X) ->
    GenNew = case Gen of not_set -> var; _ -> Gen end,
    {Rem, Var} = tok_var(File, [X]),
    token(Rem, L, [{var,Var}|Result], GenNew, BsNl);
token([X|File], L, Result, Gen, BsNl)
  when ?is_lower(X) ->
    GenNew = case Gen of not_set -> var; _ -> Gen end,
    {Rem, Var} = tok_var(File, [X]),
    token(Rem, L, [{var,Var}|Result], GenNew, BsNl);
token([X|File], L, Result, Gen, BsNl)
  when ?is_underline(X) ->
    GenNew = case Gen of not_set -> var; _ -> Gen end,
    {Rem, Var} = tok_var(File, [X]),
    token(Rem, L, [{var,Var}|Result], GenNew, BsNl);

這些可以轉換為可用作守衛測試的抽象模式,

#tab() -> 9.
#space() -> 32.
#is_tab(#tab()) -> true.
#is_space(#space()) -> true.
#is_underline($_)) -> true.
#is_number(X) when X >= $0, X =< $9 -> true.
#is_upper(X)  when X >= $A, X =< $Z -> true.
#is_lower(X)  when X >= $a, X =< $z -> true.

token([X|File], L, Result, Gen, BsNl)
  when #is_upper(X) ->
    GenNew = case Gen of not_set -> var; _ -> Gen end,
    {Rem, Var} = tok_var(File, [X]),
    token(Rem, L, [{var,Var}|Result], GenNew, BsNl);
token([X|File], L, Result, Gen, BsNl)
  when #is_lower(X) ->
    GenNew = case Gen of not_set -> var; _ -> Gen end,
    {Rem, Var} = tok_var(File, [X]),
    token(Rem, L, [{var,Var}|Result], GenNew, BsNl);
token([X|File], L, Result, Gen, BsNl)
  when #is_underline(X) ->
    GenNew = case Gen of not_set -> var; _ -> Gen end,
    {Rem, Var} = tok_var(File, [X]),
    token(Rem, L, [{var,Var}|Result], GenNew, BsNl);

或轉換為可用作模式的抽象模式,

#tab() -> 9.
#space() -> 32.
#underline(X) when X == $_ -> X.
#number(X) when X >= $0, X =< $9 -> X.
#upper(X)  when X >= $A, X =< $Z -> X.
#lower(X)  when X >= $a, X =< $z -> X.

token([#upper(X)|File], L, Result, Gen, BsNl) ->
    GenNew = case Gen of not_set -> var; _ -> Gen end,
    {Rem, Var} = tok_var(File, [X]),
    token(Rem, L, [{var,Var}|Result], GenNew, BsNl);
token([#lower(X)|File], L, Result, Gen, BsNl) ->
    GenNew = case Gen of not_set -> var; _ -> Gen end,
    {Rem, Var} = tok_var(File, [X]),
    token(Rem, L, [{var,Var}|Result], GenNew, BsNl);
token([#underline(X)|File], L, Result, Gen, BsNl) ->
    GenNew = case Gen of not_set -> var; _ -> Gen end,
    {Rem, Var} = tok_var(File, [X]),
    token(Rem, L, [{var,Var}|Result], GenNew, BsNl);

當然,我們可以在抽象模式的守衛中使用分離。

#id_start(X) when X >= $A, X =< $Z
        ; X >= $a, X =< $z
        ; X == $_           -> X.

token([#is_start(X)|File], L, Result, Gen, BsNl) ->
    GenNew = case Gen of not_set -> var; _ -> Gen end,
    {Rem, Var} = tok_var(File, [X]),
    token(Rem, L, [{var,Var}|Result], GenNew, BsNl);

是的,原始的基於巨集的版本也可以做到同樣的事情。它來自 OTP 來源;別怪我。

除了取代模式和守衛(巨集無法做到)之外,模式優於巨集的最大優勢在於

  • 它們可以在定義點進行語法檢查,而巨集只能在使用點進行語法檢查;
  • 不存在變數名稱捕獲的問題,實際上也不可能存在;
  • 抽象模式是基於值,而不是基於權杖列表,因此運算子不會有問題。

考慮以下 OTP 巨集

-define(IC_FLAG_TEST(_F1, _I1), ((_F1 band _I1) == _I1)).

首先,作者顯然害怕與其他變數名稱發生意外衝突。其次,括號看起來像是為了防止運算子優先順序錯誤。

至少還有一個類似的,

-define(is_set(F, Bits), ((F) band (Bits)) == (F)).

(正確地)暗示第一個巨集沒有足夠的括號。抽象模式等效項,

#ic_flag_test(Flags, Mask) when Flags band Mask == Mask -> true.

沒有這些問題。

再一次,有些事情是抽象模式做不到的。例如,

-define(get_max(_X, _Y), if _X > _Y -> _X; true -> _Y end).
-define(get_min(_X, _Y), if _X > _Y -> _Y; true -> _X end).

這些不能是抽象模式,因為抽象模式不能包含「if」或「case」或任何其他控制結構。但是它們可以,而且應該是普通的內聯函式

-compile({inline,[{max,2},{min,2}]}).
max(X, Y) -> if X > Y -> X; true -> Y end.
min(X, Y) -> if X > Y -> Y; true -> X end.

抽象模式不需要做普通函式可以做的事情。以下是來自 OTP 來源的另一個範例。

-define(LOWER(Char),
    if
        Char >= $A, Char =< $Z ->
        Char - ($A - $a);
        true ->
        Char
    end).
tolower(Chars) ->
    [?LOWER(Char) || Char <- Chars].

這可以而且應該是一個普通的內聯函式。抽象模式不需要做普通函式可以做的事情。讓我們仔細檢查一下。假設我們有一個模式

Cl = #lower(Cx)

當用作普通函式時,它會將 $x$X 都轉換為 $x。然後,當用作模式 #lower(Cx) = $x 時,Cx 將會有兩個正確的答案。在沒有其他情況下,一個模式可能有多種匹配方式。抽象模式無法執行條件語句是使其可以用作模式的原因之一。

巨集有時用於模組名稱。

-define(SERVER,{rmod_random_impl,
        list_to_atom("babbis@" ++
    hd(tl(string:tokens(atom_to_list(node()),"@"))))}).

-define(CLIENTMOD,'rmod_random').

produce() -> ?CLIENTMOD:produce(?SERVER).

抽象模式也可以用於此,但是有一個錯誤可能會發生。

server() -> {rmod_random_impl,
        list_to_atom("babbis@" ++
    hd(tl(string:tokens(atom_to_list(node()),"@"))))}.

#client_mod() -> 'rmod_random'.

produce -> #client_mod():produce(server()).

風險是寫出 #client_mod:produce(server()),這是在第二階段中呼叫另一個模組中定義的抽象模式時所需的語法。巨集有一件事可以用於抽象模式,但您可能寧願不要這樣做。

發明抽象模式的另一個目的是取代至少部分記錄的使用。框架(或 Joe Armstrong 的結構,本質上是相同的)是一種更優越的方式來做到這一點。讓我們看一個簡單的案例。

-record(mark_params, {cell_id,
              virtual_col,
              virtual_row
             }).
...
MarkP = mark_params(),
...
NewMarkP = MarkP#mark_params{cell_id     = undefined,
                 virtual_col = undefined,
                 virtual_row = VirtualRow
                },

這會變成

% General
#mark_params(Cell, Row, Col) -> {mark_params, Cell, Row, Col}.
% Initial value
#mark_params() -> #mark_params(undefined, undefined, undefined).
% Recogniser
#is_mark_params({mark_params,_,_,_}) -> true.
% Cell extractor
#mark_params__cell(#mark_params(Cell,_,_)) -> Cell.
% Cell updater
#mark_params__cell(Cell, #mark_params(_,R,C)) ->
    #mark_params(Cell, R, C).
% Row extractor
#mark_params__row(#mark_params(_,Row,_)) -> Row.
% Row updater
#mark_params__row(Row, #mark_params(K,_,C)) ->
    #mark_params(K, Row, C).
% Col extractor
#mark_params__col(#mark_params(_,_,Col)) -> Col.
% Col updater
#mark_params__col(Col, #mark_params(K,R,_)) ->
    #mark_params(K, R, Col).
...
MarkP = #mark_params(),
...
NewMarkP = #mark_params__row(VirtualRow,
           #mark_params__col(undefined,
           #mark_params__cell(undefined, MarkP)))

擷取器和更新器模式可以自動衍生,這將在第 4 階段中實現。使用框架/結構,我們可能永遠不會去管它。

我一直很喜歡 Haskell 的一個功能。這就是所謂的「n+k 模式」,其中模式可能是 N+K,其中 N 是變數,K 是正整數。如果 V 是大於或等於 K 的整數,則會匹配 V,並將 N 綁定到 V - K。例如,

fib 0 = 1
fib 1 = 1
fib (n+2) = fib n + fib (n+1)

當然,這並不是實作 Fibonacci 函式的好方法。(在可達到 O(log N) 時,它需要 O(phi^N)。)Erlang 中沒有這種東西。但是透過抽象模式,我們可以編寫

#succ(M) when is_integer(N), N >= 1, M = N - 1 -> N.

fib(0) -> 1;
fib(1) -> 1;
fib(#succ(#succ(N)) -> fib(N) + fib(N+1).

有時我們需要三向分割

N = 1
N = 2k+0 (k >= 1)
N = 2k+1 (k >= 1)

我們也可以編寫

#one() -> 1.
#even(K)
when is_integer(N), (N band 1) == 0, N >= 2, K = N div 2
-> N.
#odd(K)
when is_integer(N), (N band 1) == 1, N >= 3, K = N div 2
-> N.

ruler(#one())   -> 0 ;
ruler(#even(K)) -> 1 + ruler(K);
ruler(#odd(K))  -> 1.

讓我們轉向抽象資料型態。有三種明顯的方式將關聯清單實作為單一資料結構

[{K1,V1}, ..., {Kn,Vn}]     % pairs
[K1,V1, ..., Kn,Vn]         % alternating
{K1,V1, ..., {Kn,Vn,[]}}    % triples

假設您無法決定哪一個更好。

#empty_alist() -> [].
-ifdef(PAIRS).
#non_empty_alist(K,V,R) -> [{K,V}|R].
-else.
-ifdef(TRIPLES).
#non_empty_alist(K,V,R) -> {K,V,R}.
-else.
#non_empty_alist(K,V,R) -> [K,V|R].
-endif.
-endif.

zip([K|Ks], [V|Vs]) ->
    #non_empty_alist(K, V, zip(Ks, Vs));
zip([], []) ->
    #empty_alist().

lookup(K, #non_empty_alist(K,V,_), _) ->
    V;
lookup(K, #non_empty_alist(_,_,R), D) ->
    lookup(K, R, D);
lookup(K, #empty_alist(), D) ->
    D.

現在,您可以透過切換單一前處理器開關,在三個實作之間切換,以進行測試和基準測試。

有時,在 Haskell 或 Clean 或 SML 或 CAML 中會是代數資料型態的東西,但在 Erlang 中,我們只需要使用各種元組。Erlang 原始程式碼的已剖析形式就是一個很好的例子。

lform({attribute,Line,Name,Arg}, Hook) ->
    lattribute({attribute,Line,Name,Arg}, Hook);
lform({function,Line,Name,Arity,Clauses}, Hook) ->
    lfunction({function,Line,Name,Arity,Clauses}, Hook);
lform({rule,Line,Name,Arity,Clauses}, Hook) ->
    lrule({rule,Line,Name,Arity,Clauses}, Hook);
%% These are specials to make it easier for the compiler.
lform({error,E}, _Hook) ->
    leaf(format("~p\n", [{error,E}]));
lform({warning,W}, _Hook) ->
    leaf(format("~p\n", [{warning,W}]));
lform({eof,_Line}, _Hook) ->
    $\n.

我們可以為這些定義抽象模式。

#attribute(L, N, A)    -> {attribute, L, N, A}.
#function( L, N, A, C) -> {function,  L, N, A, C}.
#rule(     L, N, A, C) -> {rule,      L, N, A, C}.
#eof(      L)          -> {eof,       L}.
#error(    E_          -> {error,     E}.
#warning(  W)          -> {warning,   W}.

#attribute()       -> #attribute(_,_,_).
#function()        -> #function(_,_,_,_).
#rule()            -> #rule(_,_,_,_).

lform(Form, Hook) ->
    case Form
      of #attribute() -> lattribute(Form, Hook)
       ; #function()  -> lfunction( Form, Hook)
       ; #rule()      -> lrule(     Form, Hook)
       ; #error(E)    -> leaf(format("~p\n", [{error,E}]))
       ; #warning(W)  -> leaf(format("~p\n", [{warning,W}]))
       ; #eof(_)      -> $\n
    end.

即使這些是它們唯一的出現,也幾乎值得定義這些模式,僅僅為了它們所允許的清晰度。但是這些模式會被重複使用。使用模式不僅可以使程式碼更短更清晰,還可以為我們提供兩種保護,防止資料表示法的變更。例如,假設我們決定將 Name/Arity 資訊以成對方式保留在「函式」和「規則」元組中,而不是作為單獨的欄位。那麼我們可以做到

-ifdef(OLD_DATA).
#function( L, N, A,  C) -> {function,  L, N, A, C}.
#rule(     L, N, A,  C) -> {rule,      L, N, A, C}.
#function( L, {N,A}, C) -> {function,  L, N, A, C}.
#rule(     L, {N,A}, C) -> {rule,      L, N, A, C}.
-else.
#function( L, N, A, C)  -> {function,  L, {N,A}, C}.
#rule(     L, N, A, C)  -> {rule,      L, {N,A}, C}.
#function( L, NA,   C)  -> {function,  L, NA,    C}.
#rule(     L, NA,   C)  -> {rule,      L, NA,    C}.
-endif.

其餘的程式碼將保持不變。這是一種保護措施。當我們需要新增案例時,這種保護措施就派不上用場了。這時,第二種保護措施就出現了。尋找 #function 比尋找 function 更能安全地引導我們找到相關的位置。

基本原理 #

抽象模式的概念不僅僅是此規範所描述的內容。以下是一個「路線圖」。

  • 階段 0

    允許在守衛條件中使用模式匹配。這是另一個 EEP 的主題,因為它本身就是理想的。這必須在實作階段 1 之前先實作,因為這正是我們希望可內聯模式呼叫擴展成的樣子。

  • 階段 1

    簡單的抽象模式受到限制,因此它們可以完全通過內聯擴展來實現。除了階段 0 所需的變更之外,這不需要對 VM 進行任何變更。

    模式的匯入/匯出可以使用前置處理器來偽造,以 -include 定義;這並不理想,但這是一個可接受的權宜之計。

  • 階段 2

    抽象函式是(成對的)真實函式,它們可以被 -exported 和 -imported,可以通過模組前綴呼叫,可以通過熱加載替換,應該是可追蹤、可除錯、可分析的,就像其他函式一樣。在階段 2 中,如果導出的抽象模式要內聯,則需要內聯宣告;其他模式將繼續內聯,除非在除錯模式下編譯。

    這需要對執行時系統進行相當大的變更。這裡最大的好處是,匯入的抽象模式可以通過熱加載替換,這與巨集不同。

  • 階段 3

      #fun [Module:]Name/Arity and
      #fun (P1, ..., Pn) when G -> B end
    

    引入了 forms 和 metacall

      #Var(E1,...,En) is added.
    

    這需要擴展 Erlang 詞項表示和 VM。這裡的好處是,FAQ「如何將模式作為參數傳遞」最終得到了一個安全的答案。例如,

      collect_messages(P) ->
          lists:reverse(collect_messages_loop(P, [])).
    
      collect_messages_loop(P, Ms) ->
          receive M = #P() -> collect_messages_loop([M|Ms])
            after 0        -> Ms
          end.
    

    收集目前信箱中所有符合作為參數傳遞的模式的訊息。

  • 階段 4

    <expression>#<pattern call> 欄位更新,如原始提案中所述。

  • 階段 5

    多子句抽象模式,如原始提案中所述。多子句抽象模式可以處理諸如 ?get_max?LOWER 之類的範例,這使得它們在守衛條件中更有用,但作為模式則有點可疑。

  • 階段 6

    「混合」抽象模式,其中在 #A/M+N 中,前 M 個引數始終是輸入,而只有最後 N 個是輸出。這實際上不是我的想法。範例

      #range(L, U, N)
      when is_integer(N), L =< N, N =< U
      -> N.
    

    來自郵件列表。我不太喜歡這個,並注意到在某些情況下,

      range(L, U) ->
          #fun(N) when is_integer(N), L =< N, N =< U
              -> N end.
    

    可以做同樣的工作。

我為此提案所做的是剝離所有不必要的部分。我們獲得了資料抽象、使用者定義的守衛測試和函式,以及許多巨集使用的替代方案,而無需執行時開銷,也無需對編譯器的前端進行任何變更,假設階段 0 先完成。

向後相容性 #

Erlang 目前使用井號來表示記錄語法。由於記錄語法使用花括號,而抽象模式使用圓括號,因此不應影響現有程式碼。

參考實作 #

上面已經草擬過。鑑於階段 0,此階段 1 在我的知識和能力範圍內,但我對 Erlang VM 的了解不足以完成階段 0。

版權 #

此文件已置於公共領域。