作者
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 中的表达式必须是

  • 模式中的模式
  • 保护中其他地方的保护表达式
  • 普通表达式中其他地方的任何表达式。

理解抽象模式的语义有两种方式:作为函数调用和作为内联替换。

如果将其视为函数,则第 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。

版权 #

本文档已放入公共领域。