抽象模式是命名的模式/保护组合,可用于
完整提案有六个阶段,这是第一阶段。此阶段仅允许可以通过内联替换处理的简单抽象模式,因此不需要更改 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 中的表达式必须是
理解抽象模式的语义有两种方式:作为函数调用和作为内联替换。
如果将其视为函数,则第 1 阶段的抽象模式对应于两个函数。给定我们的示意规则,我们得到
'#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())
,这是我们将在第 2 阶段中用于调用在另一个模块中定义的抽象模式的语法。宏有一种用法,抽象模式可以使用,但您可能宁愿不使用。
抽象模式的发明也是为了替换至少一些记录的用法。帧(或 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)
当然,这不是实现斐波那契函数的最佳方式。(当可达到 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.
即使这些是它们唯一的出现,仅仅为了它们允许的清晰度,也几乎值得定义这些模式。但是这些模式会一次又一次地使用。使用这些模式不仅使代码更短更清晰,而且还为我们提供了两种针对数据表示更改的保护。例如,假设我们决定将 “function” 和 “rule” 元组中的 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 阶段
简单的抽象模式被限制为只能通过内联展开来实现。这不需要对虚拟机进行任何更改,除了 Stage 0 所需的更改之外。
模式的导入/导出可以使用预处理器通过 -include 定义来伪造;这虽然不是理想的,但作为一个权宜之计是可以接受的。
第二阶段
抽象函数是(成对的)真实函数,它们可以被 -exported 和 -imported,可以使用模块前缀调用,可以通过热加载替换,应该像其他函数一样可追踪、可调试、可分析等等。在第二阶段,如果要内联导出抽象模式,则需要内联声明;其他模式将继续内联,除非在调试模式下编译。
这需要对运行时系统进行相当大的更改。这里的好处是,与宏不同,导入的抽象模式可以通过热加载替换。
第三阶段
#fun [Module:]Name/Arity and
#fun (P1, ..., Pn) when G -> B end
引入了形式 (forms) 和元调用 (metacall)
#Var(E1,...,En) is added.
这需要扩展 Erlang 的项表示和虚拟机。这里的好处是 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.
收集邮箱中所有匹配作为参数传递的模式的消息。
第四阶段
<表达式>#<模式调用>
字段更新,如原始提案中所述。
第五阶段
多子句抽象模式,如原始提案中所述。多子句抽象模式可以处理像 ?get_max
和 ?LOWER
这样的例子,这使得它们在 guard 中更有用,但作为模式则有点可疑。
第六阶段
“混合”抽象模式,其中在 #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.
可以完成相同的工作。
我为这个提案所做的是去除了一切不必要的东西。我们获得了数据抽象、用户定义的 guard 测试和函数,以及许多宏的替代方案,无需运行时开销,并且除了编译器前端之外无需更改任何内容,假设首先完成 Stage 0。
Erlang 目前使用井号 (sharp sign) 表示记录语法。由于记录语法使用花括号,而抽象模式使用圆括号,因此不应影响任何现有代码。
如上所述。考虑到 stage 0,这个 stage 1 在我的知识和能力范围内,但我不太了解 Erlang VM,无法完成 stage 0。
本文档已放入公共领域。