抽象模式是命名的模式/守衛組合,可用於
完整的提案共有六個階段,這只是第一階段。這個階段僅允許可由內聯替換處理的簡單抽象模式,因此不需要變更 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, ..., Hn
和 B
都必須是模式。
抽象模式不得直接或間接遞迴。
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。
此文件已置於公共領域。