檢視原始碼 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:select/2
呼叫,其執行與 ets:foldr
相同
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
) 開頭,並且我們想要建立列表 [{<Old empno>,<New empno>}]
ets:select(emp_tab, ets:fun2ms(
fun(#emp{empno = [$0 | Rest] }) ->
{[$0|Rest],[$1|Rest]}
end)).
這個查詢會觸及 ordered_set
表格類型中部分綁定鍵的功能,因此不需要搜尋整個表格,只需要搜尋包含以 0
開頭的鍵的部分。
範例 5
fun 可以有多個子句。假設我們想要執行以下操作
- 如果員工在 1997 年之前開始工作,則返回元組
{inventory, <employee number>}
。 - 如果員工在 1997 年或之後開始工作,但在 2001 年之前開始工作,則返回
{rookie, <employee number>}
。 - 對於所有其他員工,返回
{newbie, <employee number>}
,除了名為Smith
的員工,因為他們會因任何其他標籤而感到冒犯,所以也會為他們的編號返回guru
標籤:{guru, <employee number>}
。
這是透過以下方式完成的
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 使用者指南中的匹配規格文件。但是,以下是當 fun 要由 ets:fun2ms/1
轉換為匹配規格時,您可以使用的最有用「內建函數」的簡要概述。無法呼叫匹配規格中允許的函數以外的其他函數。ets:fun2ms/1
轉換的 fun 無法執行任何「一般」Erlang 程式碼。fun 完全限於匹配規格的功能,這很遺憾,但這是您必須為 ets:select/2
與 ets:foldl/foldr
相比的執行速度而付出的代價。
fun 的頭部是一個頭部匹配(或不匹配)一個參數,即我們從中選取的表格的一個物件。該物件始終是一個單一變數(可以是 _
)或一個元組,因為 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 語意相同。
fun 的主體用於建構結果值。從表格中選擇時,通常會在這裡使用普通的 Erlang 詞彙建構來建構一個合適的詞彙,例如元組括號、列表括號,以及在標頭中匹配出的變數,可能會有一些常數。守衛中允許使用的任何表達式在這裡也允許使用,但除了 object
和 bindings
(詳見下文)之外,沒有特殊的函式存在,它們分別傳回整個匹配的物件和所有已知的變數綁定。
匹配規範的 dbg
變體對匹配規範主體採用命令式方法,而 ETS 方言則沒有。 ets:fun2ms/1
的 fun 主體會傳回結果,而不會產生副作用。由於不允許在匹配規範的主體中進行匹配(=
)(基於效能考量),所以剩下的或多或少只有詞彙建構。
使用 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}]}
如您所見,與 dbg:fun2ms/1
一起使用的 fun 會採用單一列表作為參數,而不是單一元組。該列表會匹配到追蹤函式的參數列表。也可以使用單一變數。fun 的主體會以更命令式的方式表達,如果 fun 標頭(和守衛)匹配,則要採取的動作。此處傳回 true
,只是因為 fun 的主體不能為空。傳回值會被捨棄。
在測試期間收到以下追蹤輸出
(<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
(帶有 arity 2
)是造成所有這些問題的原因。
此範例說明了 dbg
的匹配規範中最常用的呼叫。其他更深奧的呼叫會在 ERTS 使用者指南的Erlang 中的匹配規範中列出並說明,因為它們超出本說明範圍。
警告與限制
以下警告與限制適用於與 ets:fun2ms/1
和 dbg:fun2ms/1
一起使用的 fun。
警告
若要使用觸發轉換的虛擬函式,請確保在原始程式碼中包含標頭檔
ms_transform.hrl
。如果未這樣做,則可能會導致執行階段錯誤,而不是編譯時間錯誤,因為該表達式可以作為沒有轉換的純 Erlang 程式有效。
警告
fun 必須在虛擬函式的參數清單內以字面方式建構。fun 不能先繫結到變數,然後再傳遞給
ets:fun2ms/1
或dbg:fun2ms/1
。例如,ets:fun2ms(fun(A) -> A end)
可以運作,但F = fun(A) -> A end, ets:fun2ms(F)
則不行。如果包含標頭,後者會導致編譯時間錯誤,否則會導致執行階段錯誤。
許多限制適用於轉換為匹配規範的 fun。簡單來說:您不能在 fun 中使用在匹配規範中無法使用的任何內容。這表示,在其他限制中,以下限制適用於 fun 本身
無法呼叫以 Erlang 撰寫的函式,也無法呼叫本機函式、全域函式或真正的 fun。
所有寫為函式呼叫的內容都會轉換為對內建函式的匹配規範呼叫,因此呼叫
is_list(X)
會轉換為{'is_list', '$1'}
('$1'
只是一個範例,編號可能會有所不同)。如果嘗試呼叫不是匹配規範內建函式的函式,則會導致錯誤。fun 標頭中出現的變數會按出現順序替換為匹配規範變數,因此片段
fun({A,B,C})
會被{'$1', '$2', '$3'}
取代,依此類推。在匹配規範中每次出現這種變數都會以相同方式替換為匹配規範變數,因此 funfun({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']}]
無法在主體中使用
=
進行匹配。它只能在 fun 標頭的最上層使用。再次來自 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
傳遞的鍵值列表中。結果是一個正規化的 term,也就是說,不是抽象格式。
函式
-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
傳遞的鍵值列表中。結果是一個正規化的 term,也就是說,不是抽象格式。