查看源代码 ms_transform (stdlib v6.2)
一个将 fun 语法转换为匹配规范的解析转换。
此模块提供解析转换,使得对 ets
和 dbg:fun2ms/1
的调用转换为字面匹配规范。它还为从 Erlang shell 调用时相同的函数提供后端。
从 fun 到匹配规范的转换通过两个“伪函数” ets:fun2ms/1
和 dbg:fun2ms/1
进行访问。
由于每个尝试使用 ets:select/2
或 dbg
的人都似乎最终会阅读此手册页,因此此描述是对匹配规范概念的介绍。
如果这是您第一次使用转换,请阅读整个手册页。
匹配规范或多或少用作过滤器。它们类似于列表推导式或与 lists:foldl/3
等一起使用的 fun 中常见的 Erlang 匹配。但是,纯匹配规范的语法很笨拙,因为它们完全由 Erlang 项组成,并且该语言没有语法使匹配规范更具可读性。
由于匹配规范的执行和结构类似于 fun,因此使用熟悉的 fun 语法来编写它,并将其自动转换为匹配规范更为直接。真正的 fun 显然比匹配规范允许的更强大,但是考虑到匹配规范,以及它们可以做什么,将其全部写成 fun 仍然更方便。此模块包含将 fun 语法转换为匹配规范项的代码。
示例 1
使用 ets:select/2
和匹配规范,可以过滤出表的行,并构造一个包含这些行中相关部分数据的元组列表。可以使用 ets:foldl/3
代替,但是 ets:select/2
调用效率更高。如果没有 ms_transform
提供的转换,就必须努力编写匹配规范项以适应这种情况。
考虑一个简单的员工表
-record(emp, {empno, %Employee number as a string, the key
surname, %Surname of the employee
givenname, %Given name of employee
dept, %Department, one of {dev,sales,prod,adm}
empyear}). %Year the employee was employed
我们使用以下代码创建表
ets:new(emp_tab, [{keypos,#emp.empno},named_table,ordered_set]).
我们用随机选择的数据填充表
[{emp,"011103","Black","Alfred",sales,2000},
{emp,"041231","Doe","John",prod,2001},
{emp,"052341","Smith","John",dev,1997},
{emp,"076324","Smith","Ella",sales,1995},
{emp,"122334","Weston","Anna",prod,2002},
{emp,"535216","Chalker","Samuel",adm,1998},
{emp,"789789","Harrysson","Joe",adm,1996},
{emp,"963721","Scott","Juliana",dev,2003},
{emp,"989891","Brown","Gabriel",prod,1999}]
假设我们需要销售部门所有员工的员工编号,有几种方法可以实现。
可以使用 ets:match/2
1> ets:match(emp_tab, {'_', '$1', '_', '_', sales, '_'}).
[["011103"],["076324"]]
ets:match/2
使用一种更简单的匹配规范类型,但它仍然难以阅读,并且对返回的结果几乎没有控制权。它始终是一个列表的列表。
可以使用 ets:foldl/3
或 ets:foldr/3
来避免嵌套列表
ets:foldr(fun(#emp{empno = E, dept = sales},Acc) -> [E | Acc];
(_,Acc) -> Acc
end,
[],
emp_tab).
结果为 ["011103","076324"]
。fun 很简单,因此唯一的问题是必须将表中的所有数据从表传输到调用进程进行筛选。与 ets:match/2
调用相比,这是低效的,后者可以在仿真器“内部”完成筛选,并且仅将结果传输到进程。
考虑一个执行 ets:foldr
操作的“纯” ets:select/2
调用
ets:select(emp_tab, [{#emp{empno = '$1', dept = sales, _='_'},[],['$1']}]).
尽管使用了记录语法,但仍然难以阅读,甚至更难编写。元组的第一个元素 #emp{empno = '$1', dept = sales, _='_'}
说明要匹配什么。不匹配此元素的元素不会返回,如在 ets:match/2
示例中一样。第二个元素(空列表)是守卫表达式的列表,我们不需要它。第三个元素是构造返回值的表达式列表(在 ETS 中,这几乎总是包含一个单项的列表)。在我们的例子中,'$1'
绑定到头部(元组的第一个元素)中的员工编号,因此返回员工编号。结果为 ["011103","076324"]
,与 ets:foldr/3
示例中一样,但在执行速度和内存消耗方面,结果的检索效率更高。
使用 ets:fun2ms/1
,我们可以结合使用 ets:foldr/3
的易用性和纯 ets:select/2
示例的效率
-include_lib("stdlib/include/ms_transform.hrl").
ets:select(emp_tab, ets:fun2ms(
fun(#emp{empno = E, dept = sales}) ->
E
end)).
此示例不需要任何特殊的匹配规范知识即可理解。fun 的头部匹配您要筛选的内容,主体返回您要返回的内容。只要 fun 可以保持在匹配规范的限制范围内,就不需要像 ets:foldr/3
示例中那样将所有表数据传输到进程进行筛选。它比 ets:foldr/3
示例更容易阅读,因为 select 调用本身会丢弃任何不匹配的内容,而 ets:foldr/3
调用的 fun 需要处理匹配和不匹配的元素。
在上面的 ets:fun2ms/1
示例中,需要在源代码中包含 ms_transform.hrl
,因为这是触发将 ets:fun2ms/1
调用解析转换为有效匹配规范的解析转换。这也意味着转换是在编译时完成的(从 shell 调用时除外),因此在运行时不占用任何资源。也就是说,尽管您使用更直观的 fun 语法,但它在运行时与手动编写匹配规范一样高效。
示例 2
假设我们要获取 2000 年之前雇用的所有员工的员工编号。使用 ets:match/2
在这里不是一个选择,因为关系运算符无法在此表达。再一次,ets:foldr/3
可以做到(速度较慢,但正确)
ets:foldr(fun(#emp{empno = E, empyear = Y},Acc) when Y < 2000 -> [E | Acc];
(_,Acc) -> Acc
end,
[],
emp_tab).
结果是 ["052341","076324","535216","789789","989891"]
,正如预期的那样。使用手写匹配规范的等效表达式如下所示
ets:select(emp_tab, [{#emp{empno = '$1', empyear = '$2', _='_'},
[{'<', '$2', 2000}],
['$1']}]).
这会产生相同的结果。 [{'<', '$2', 2000}]
在守卫部分,因此会丢弃任何没有 empyear
(在头部绑定到 '$2'
)小于 2000 的内容,如 foldr/3
示例中的守卫。
我们使用 ets:fun2ms/1
编写它
-include_lib("stdlib/include/ms_transform.hrl").
ets:select(emp_tab, ets:fun2ms(
fun(#emp{empno = E, empyear = Y}) when Y < 2000 ->
E
end)).
示例 3
假设我们想要匹配整个对象,而不是仅一个元素。一种替代方法是为记录的每个部分分配一个变量,并在 fun 的主体中再次构建它,但是以下方法更简单
ets:select(emp_tab, ets:fun2ms(
fun(Obj = #emp{empno = E, empyear = Y})
when Y < 2000 ->
Obj
end)).
与普通的 Erlang 匹配一样,您可以使用“匹配内部的匹配”(即 =
)将变量绑定到整个匹配的对象。不幸的是,在转换为匹配规范的 fun 中,只允许在“顶层”执行此操作,也就是说,将到达要匹配的整个对象匹配到一个单独的变量中。如果您习惯手动编写匹配规范,我们会提到变量 A 只是转换为 '$_'。或者,伪函数 object/0
也返回整个匹配的对象,请参阅 警告和限制 部分。
示例 4
此示例涉及 fun 的主体。假设所有以零 (0
) 开头的员工编号都必须更改为以一 (1
) 开头,并且我们要创建列表 [{<旧员工编号>,<新员工编号>}]
ets:select(emp_tab, ets:fun2ms(
fun(#emp{empno = [$0 | Rest] }) ->
{[$0|Rest],[$1|Rest]}
end)).
此查询命中了表类型 ordered_set
中部分绑定键的功能,因此不需要搜索整个表,只需要查找包含以 0
开头的键的部分即可。
示例 5
fun 可以有多个子句。假设我们要执行以下操作
- 如果员工在 1997 年之前开始工作,则返回元组
{inventory, <员工编号>}
。 - 如果员工在 1997 年或之后,但在 2001 年之前开始工作,则返回
{rookie, <员工编号>}
。 - 对于所有其他员工,返回
{newbie, <员工编号>}
,但名为Smith
的员工除外,因为他们会对除标签guru
之外的任何标签感到冒犯,这也是为他们的编号返回的内容:{guru, <员工编号>}
。
这是通过以下方式完成的
ets:select(emp_tab, ets:fun2ms(
fun(#emp{empno = E, surname = "Smith" }) ->
{guru,E};
(#emp{empno = E, empyear = Y}) when Y < 1997 ->
{inventory, E};
(#emp{empno = E, empyear = Y}) when Y > 2001 ->
{newbie, E};
(#emp{empno = E, empyear = Y}) -> % 1997 -- 2001
{rookie, E}
end)).
结果如下
[{rookie,"011103"},
{rookie,"041231"},
{guru,"052341"},
{guru,"076324"},
{newbie,"122334"},
{rookie,"535216"},
{inventory,"789789"},
{newbie,"963721"},
{rookie,"989891"}]
有用的 BIF
你还能做什么?一个简单的答案是:查看 ERTS 用户指南中 匹配规范的文档。不过,以下是对最常用的“内置函数”的简要概述,当函数通过 ets:fun2ms/1
转换为匹配规范时可以使用这些函数。除了匹配规范中允许的函数外,无法调用其他函数。由 ets:fun2ms/1
转换的函数不能执行任何“通常的”Erlang 代码。该函数严格限制在匹配规范的能力范围内,这很不幸,但这正是 ets:select/2
相对于 ets:foldl/foldr
的执行速度所必须付出的代价。
该函数的头部是匹配(或不匹配)一个参数,即我们从中选择的表的一个对象。该对象始终是一个变量(可以是 _
)或一个元组,因为 ETS、Dets 和 Mnesia 表都包含这些。 ets:fun2ms/1
返回的匹配规范可以与 dets:select/2
和 mnesia:select/2
以及 ets:select/2
一起使用。允许(并鼓励)在顶层使用 =
。
保护部分可以包含任何 Erlang 的保护表达式。以下是 BIF 和表达式的列表
- 类型测试:
is_atom
、is_float
、is_integer
、is_list
、is_number
、is_pid
、is_port
、is_reference
、is_tuple
、is_binary
、is_function
、is_record
- 布尔运算符:
not
、and
、or
、andalso
、orelse
- 关系运算符:
>
、>=
、<
、=<
、=:=
、==
、=/=
、/=
- 算术:
+
、-
、*
、div
、rem
- 按位运算符:
band
、bor
、bxor
、bnot
、bsl
、bsr
- 保护 BIF:
abs
、element
、hd
、length
、node
、round
、size
、byte_size
、tl
、trunc
、binary_part
、self
与“手写”匹配规范的事实相反,is_record
保护的作用与普通 Erlang 代码中的作用相同。
保护中的分号 (;
) 是允许的,其结果(正如预期的那样)是保护的每个以分号分隔的部分都对应一个“匹配规范子句”。其语义与 Erlang 语义相同。
函数的正文用于构造结果值。从表中选择时,通常在此处使用普通的 Erlang 项构造(如元组括号、列表括号)以及头部中匹配出的变量,并可能带有偶尔的常量,来构造一个合适的项。保护中允许的任何表达式在此处也允许,但除了 object
和 bindings
(详见下文)之外,不存在特殊函数,它们分别返回整个匹配对象和所有已知的变量绑定。
匹配规范的 dbg
变体对匹配规范正文采用命令式方法,而 ETS 方言则没有。 ets:fun2ms/1
的函数正文返回结果,而不会产生副作用。由于(出于性能原因)不允许在匹配规范的正文中进行匹配 (=
),所以剩下的几乎只有项构造。
dbg 示例
本节介绍由 dbg:fun2ms/1
转换的略有不同的匹配规范。
使用解析转换的相同原因也适用于 dbg
,甚至可能更多,因为在跟踪时使用 Erlang 代码进行过滤不是一个好主意(除非事后,如果跟踪到文件)。该概念与 ets:fun2ms/1
的概念类似,只是通常直接从 shell 中使用它(也可以通过 ets:fun2ms/1
来完成)。
以下是一个用于跟踪的示例模块
-module(toy).
-export([start/1, store/2, retrieve/1]).
start(Args) ->
toy_table = ets:new(toy_table, Args).
store(Key, Value) ->
ets:insert(toy_table, {Key,Value}).
retrieve(Key) ->
[{Key, Value}] = ets:lookup(toy_table, Key),
Value.
在模型测试期间,第一个测试在 {toy,start,1}
中导致 {badmatch,16}
,为什么?
我们怀疑 ets:new/2
调用,因为我们对返回值进行了硬匹配,但只想特别调用第一个参数为 toy_table
的 new/2
。因此,我们在节点上启动默认跟踪器
1> dbg:tracer().
{ok,<0.88.0>}
我们为所有进程启用调用跟踪,我们想创建一个非常严格的跟踪模式,因此没有必要仅跟踪少数几个进程(通常不是这样)
2> dbg:p(all,call).
{ok,[{matched,nonode@nohost,25}]}
我们指定过滤器,我们想查看类似于 ets:new(toy_table, <something>)
的调用
3> dbg:tp(ets,new,dbg:fun2ms(fun([toy_table,_]) -> true end)).
{ok,[{matched,nonode@nohost,1},{saved,1}]}
可以看出,与 ets:fun2ms/1
一起使用的函数采用单个列表作为参数,而不是单个元组。该列表匹配被跟踪函数的参数列表。也可以使用单个变量。函数正文以更命令式的方式表达如果函数头部(和保护)匹配应采取的操作。此处返回 true
,仅仅因为函数正文不能为空。返回值将被丢弃。
在测试期间收到以下跟踪输出
(<0.86.0>) call ets:new(toy_table, [ordered_set])
假设我们尚未发现问题,并且想查看 ets:new/2
返回的内容。我们使用略有不同的跟踪模式
4> dbg:tp(ets,new,dbg:fun2ms(fun([toy_table,_]) -> return_trace() end)).
在测试期间收到以下跟踪输出
(<0.86.0>) call ets:new(toy_table,[ordered_set])
(<0.86.0>) returned from ets:new/2 -> 24
调用 return_trace
会在函数返回时生成一条跟踪消息。它仅适用于触发匹配规范(并匹配匹配规范的头部/保护)的特定函数调用。到目前为止,这是 dbg
匹配规范正文中最为常见的调用。
现在测试失败,出现 {badmatch,24}
,因为原子 toy_table
与为未命名表返回的数字不匹配。因此,问题已经找到,该表应被命名,并且测试程序提供的参数不包括 named_table
。我们重写 start 函数
start(Args) ->
toy_table = ets:new(toy_table, [named_table|Args]).
启用相同的跟踪后,会收到以下跟踪输出
(<0.86.0>) call ets:new(toy_table,[named_table,ordered_set])
(<0.86.0>) returned from ets:new/2 -> toy_table
假设该模块现在通过了所有测试并进入系统。过了一段时间,发现表 toy_table
在系统运行时不断增长,并且有许多元素的键是原子。我们只期望整数键,系统的其余部分也是如此,但显然不是整个系统。我们启用调用跟踪,并尝试查看以原子作为键的模块调用
1> dbg:tracer().
{ok,<0.88.0>}
2> dbg:p(all,call).
{ok,[{matched,nonode@nohost,25}]}
3> dbg:tpl(toy,store,dbg:fun2ms(fun([A,_]) when is_atom(A) -> true end)).
{ok,[{matched,nonode@nohost,1},{saved,1}]}
我们使用 dbg:tpl/3
来确保捕获本地调用(假设该模块自较小版本以来已增长,并且我们不确定是否在本地完成此原子插入)。如有疑问,请始终使用本地调用跟踪。
假设以这种方式跟踪时没有任何反应。永远不会使用这些参数调用该函数。我们得出结论,是其他人(其他模块)在执行此操作,并且意识到我们必须跟踪 ets:insert/2
,并想查看调用函数。可以使用匹配规范函数 caller
来检索调用函数。为了将其放入跟踪消息中,必须使用匹配规范函数 message
。过滤器调用如下所示(查找对 ets:insert/2
的调用)
4> dbg:tpl(ets,insert,dbg:fun2ms(fun([toy_table,{A,_}]) when is_atom(A) ->
message(caller())
end)).
{ok,[{matched,nonode@nohost,1},{saved,2}]}
现在,调用方显示在跟踪输出的“附加消息”部分中,一段时间后显示以下内容
(<0.86.0>) call ets:insert(toy_table,{garbage,can}) ({evil_mod,evil_fun,2})
您已经意识到 evil_mod
模块的函数 evil_fun
(参数数量为 2
)造成了所有这些麻烦。
此示例说明了 dbg
的匹配规范中最常用的调用。其他更深奥的调用在 ERTS 用户指南中的 Erlang 中的匹配规范 中列出并进行了解释,因为它们超出了本描述的范围。
警告和限制
以下警告和限制适用于与 ets:fun2ms/1
和 dbg:fun2ms/1
一起使用的函数。
警告
要使用触发转换的伪函数,请确保在源代码中包含头文件
ms_transform.hrl
。不这样做可能会导致运行时错误,而不是编译时错误,因为该表达式作为未经转换的普通 Erlang 程序是有效的。
警告
函数必须在伪函数的参数列表中以字面方式构造。该函数不能首先绑定到变量,然后再传递给
ets:fun2ms/1
或dbg:fun2ms/1
。例如,ets:fun2ms(fun(A) -> A end)
可以工作,但是F = fun(A) -> A end, ets:fun2ms(F)
则不行。如果包含标头,则后者会导致编译时错误,否则会导致运行时错误。
对转换为匹配规范的函数有很多限制。简而言之:您不能在函数中使用任何您不能在匹配规范中使用的内容。这意味着,除其他外,以下限制适用于函数本身
无法调用用 Erlang 编写的函数,也无法调用局部函数、全局函数或真正的函数。
编写为函数调用的所有内容都会转换为对内置函数的匹配规范调用,因此调用
is_list(X)
会转换为{'is_list', '$1'}
('$1'
只是一个示例,编号可能会有所不同)。如果尝试调用不是匹配规范内置的函数,则会导致错误。函数头中出现的变量按出现顺序替换为匹配规范变量,因此片段
fun({A,B,C})
被替换为{'$1', '$2', '$3'}
,依此类推。匹配规范中每次出现此类变量都以相同的方式替换为匹配规范变量,因此函数fun({A,B}) when is_atom(A) -> B end
转换为[{{'$1','$2'},[{is_atom,'$1'}],['$2']}]
。未包含在头部的变量从环境中导入,并转换为匹配规范
const
表达式。来自 shell 的示例1> X = 25. 25 2> ets:fun2ms(fun({A,B}) when A > X -> B end). [{{'$1','$2'},[{'>','$1',{const,25}}],['$2']}]
不能在正文中使用与
=
的匹配。它只能在函数头的顶层使用。再次来自 shell 的示例1> ets:fun2ms(fun({A,[B|C]} = D) when A > B -> D end). [{{'$1',['$2'|'$3']},[{'>','$1','$2'}],['$_']}] 2> ets:fun2ms(fun({A,[B|C]=D}) when A > B -> D end). Error: fun with head matching ('=' in head) cannot be translated into match_spec {error,transform_error} 3> ets:fun2ms(fun({A,[B|C]}) when A > B -> D = [B|C], D end). Error: fun with body matching ('=' in body) is illegal as match_spec {error,transform_error}
所有变量都在匹配规范的头部绑定,因此转换器不允许进行多个绑定。在顶层完成匹配的特殊情况下,变量会绑定到生成的匹配规范中的
'$_'
。这是为了允许更自然地访问整个匹配对象。可以使用伪函数object()
代替,请参见下文。以下表达式被同等地转换
ets:fun2ms(fun({a,_} = A) -> A end). ets:fun2ms(fun({a,_}) -> object() end).
特殊的匹配规范变量
'$_'
和'$*'
可以通过伪函数object()
(用于'$_'
)和bindings()
(用于'$*'
)访问。例如,可以将以下ets:match_object/2
调用转换为ets:select/2
调用ets:match_object(Table, {'$1',test,'$2'}).
这与以下代码相同:
ets:select(Table, ets:fun2ms(fun({A,test,B}) -> object() end)).
在这个简单的例子中,前一种表达方式在可读性方面可能更可取。
在生成的代码中,
ets:select/2
调用在概念上看起来像这样:ets:select(Table, [{{'$1',test,'$2'},[],['$_']}]).
在 fun 的顶层进行匹配可能是访问
'$_'
的更自然的方式,如上所述。术语构造/字面量会尽可能地被转换为有效的匹配规范。这样,元组会被转换为匹配规范元组构造(包含元组的单元素元组),并且在从环境中导入变量时会使用常量表达式。记录也会转换为普通元组构造、对 element 的调用等等。保护测试
is_record/2
会使用内置于匹配规范的三参数版本转换为匹配规范代码,因此,如果记录类型t
的记录大小为 5,则is_record(A,t)
会被转换为{is_record,'$1',t,5}
。匹配规范中不存在的语言结构,如
case
、if
和catch
是不允许的。如果未包含头文件
ms_transform.hrl
,则不会转换 fun,这可能会导致运行时错误(取决于 fun 在纯 Erlang 上下文中是否有效)。在使用
ets
和编译代码中的dbg:fun2ms/1
时,请确保包含此头文件。如果触发转换的伪函数是
ets:fun2ms/1
,则 fun 的头部必须包含单个变量或单个元组。如果伪函数是dbg:fun2ms/1
,则 fun 的头部必须包含单个变量或单个列表。
从 fun 到匹配规范的转换是在编译时完成的,因此使用这些伪函数不会影响运行时性能。
有关匹配规范的更多信息,请参阅 ERTS 用户指南中的 Erlang 中的匹配规范。
摘要
函数
接受模块中其他函数返回的错误代码,并创建错误的文本描述。
在编译时实现转换。如果源文件中包含头文件 ms_transform.hrl
,则此函数由编译器调用以进行源代码转换。
当从 shell 调用 fun2ms/1
函数时实现转换。在这种情况下,抽象形式用于单个 fun(由 Erlang shell 解析)。所有导入的变量都应在作为 BoundEnvironment
传递的键值列表中。结果是一个规范化的术语,即不是抽象形式的。
函数
-spec format_error(Error) -> Chars when Error :: {error, module(), term()}, Chars :: io_lib:chars().
接受模块中其他函数返回的错误代码,并创建错误的文本描述。
-spec parse_transform(Forms, Options) -> Forms2 | Errors | Warnings when Forms :: [erl_parse:abstract_form() | erl_parse:form_info()], Forms2 :: [erl_parse:abstract_form() | erl_parse:form_info()], Options :: term(), Errors :: {error, ErrInfo :: [tuple()], WarnInfo :: []}, Warnings :: {warning, Forms2, WarnInfo :: [tuple()]}.
在编译时实现转换。如果源文件中包含头文件 ms_transform.hrl
,则此函数由编译器调用以进行源代码转换。
有关如何使用此解析转换的信息,请参阅 ets
和 dbg:fun2ms/1
。
有关匹配规范的描述,请参阅 ERTS 用户指南中 Erlang 中的匹配规范 部分。
-spec transform_from_shell(Dialect, Clauses, BoundEnvironment) -> term() when Dialect :: ets | dbg, Clauses :: [erl_parse:abstract_clause()], BoundEnvironment :: erl_eval:binding_struct().
当从 shell 调用 fun2ms/1
函数时实现转换。在这种情况下,抽象形式用于单个 fun(由 Erlang shell 解析)。所有导入的变量都应在作为 BoundEnvironment
传递的键值列表中。结果是一个规范化的术语,即不是抽象形式的。