作者
Richard carlsson <carlsson.richard(at)gmail(dot)com>
狀態
草稿
類型
標準追蹤
建立日期
2020-12-21
Erlang 版本
OTP-24.0
發佈歷史
2020-12-24

EEP 55:模式中的固定運算子 ^ #

摘要 #

此 EEP 建議新增一個新的單元運算子 ^,用於明確標記模式中已綁定的變數。這在 Elixir 中稱為「固定 (pinning)」 - 請參閱 Elixir 文件

例如

f(X, Y) ->
    case X of
        {a, Y} -> ok;
        _ -> error
    end.

可以更明確地寫成

f(X, Y) ->
    case X of
        {a, ^Y} -> ok;
        _ -> error
    end.

在 Elixir 中,這個運算子是嚴格必要的,才能在模式中引用已綁定變數的值,因為模式中的變數總是會被視為新的遮蔽實例(就像 Erlang 的 fun 子句頭一樣),除非明確固定。

在 Erlang 中,它們是可選的,但仍然是一個好主意,因為它們使程式在編輯和重構時更加穩健,而且允許在 fun 子句頭和 comprehension 生成器模式中使用固定變數。

規格 #

Erlang 中新增一個新的單元運算子 ^,稱為「固定運算子」。它只能在模式中使用,而且只能用於變數。它的含義是,「固定」的變數將在模式的封閉環境中解釋,並且其值將在模式中該位置使用。

在目前的 Erlang 中,如果變數已在封閉環境中綁定,則在普通匹配結構中會自動發生此行為。在以下範例中

f(X, Y) ->
    case X of
        {a, Y} -> {ok, Y};
        _ -> error
    end.

模式中 Y 的使用被視為參考函數參數 Y,而不是引入一個新的變數,而子句主體中的 Y 則是同一個參數。因此,在這種情況下,將模式變數註解為 ^Y 不會改變程式的行為,但會明確其意圖

f(X, Y) ->
    case X of
        {a, ^Y} -> {ok, Y};
        _ -> error
    end.

對於 fun 表達式和列表 comprehension 生成器模式,固定運算子使語言更具表現力。以下列 Erlang 程式碼為例

f(X, Y) ->
    F = fun ({a, Y}) -> {ok, Y};
            (_) -> error
        end,
    F(X).

在此,fun F 的子句頭中出現的 Y 是一個新的變數實例,會遮蔽 f(X, Y)Y 參數,而 fun 子句將會匹配該位置的任何值。子句主體中的 Y 是在子句頭中綁定的。但是,使用固定運算子,我們可以選擇性地匹配外部範圍中綁定的變數

f(X, Y) ->
    F = fun ({a, ^Y})  -> {ok, Y};
            (_) -> error
        end,
    F(X).

在這種情況下,不會有 Y 的新綁定,而且 fun 子句主體中 Y 的使用會參考函數參數。但也可以在同一個模式中組合固定和遮蔽

f(X, Y) ->
    F = fun ({a, ^Y, Y})  -> {ok, Y};
            (_) -> error
        end,
    F(X).

在這種情況下,固定的欄位會參考函數參數的值,但是也會將 Y 新遮蔽綁定到 tuple 的第三個欄位。fun 子句主體中的使用現在會參考遮蔽的實例。

列表 comprehension 或二元 comprehension 中的生成器模式遵循與 fun 子句頭相同的規則,因此透過固定,我們例如可以寫出以下程式碼

f(X, Y) ->
    [{b, Y} || {a, ^Y, Y} <- X].

其中 {b, Y} 中的 Y 是綁定到模式 tuple 的第三個元素的遮蔽實例。

最後,新增一個新的編譯器標誌 warn_unpinned_vars,預設為停用,如果啟用,則會使編譯器針對模式中所有未明確以 ^ 運算子註解的已綁定變數發出警告。這允許使用者逐個模組地將其程式碼遷移到在所有程式碼中使用明確的固定。如果固定成為 Erlang 的規範,則可以預設開啟此標誌,而且最終,固定運算子可能會變成嚴格要求,用於參考模式中已綁定的變數。

理由 #

模式中變數的明確固定使程式碼更易讀,因為程式碼的意圖變得清楚。當已綁定的變數在 Erlang 中使用而沒有任何註解時,任何閱讀程式碼的人都必須先仔細研究,才能了解哪些變數會在模式的點綁定,然後才能判斷任何模式變數是新的綁定還是暗示相等斷言。即使對於經驗豐富的 Erlang 使用者來說,這也很容易在程式碼審查期間或嘗試了解一段註解不良的程式碼時被遺漏。

也許更重要的是,固定也使程式在編輯和重構時更加穩健。採用我們之前的範例,並新增一個 print 陳述式

f(X, Y) ->
    io:format("checking: ~p", [Y]),
    case X of
        {a, Y} -> {ok, Y};
        _ -> error
    end.

假設有人將函數參數從 Y 重新命名為 Z 並更新 print 陳述式,但忘記更新 case 子句中的使用。如果沒有明確的固定註解,則會悄悄地允許變更,但模式中的 Y 會被解釋為將匹配任何值的新變數,然後將在主體中使用。這會改變程式的行為。如果模式中的使用已註解為 ^Y,則編譯器會產生錯誤「Y 未綁定」,並且會捕獲錯誤。

當修改程式碼以新增功能或修正錯誤時,程式設計師可能想要為臨時結果引入一個新的變數。在長的函數主體中,這可能會引入新的錯誤。考慮以下情況

g(Stuff) ->
    ...
    Thing = case ... of
                {a, T} -> T;
                _ -> 0
            end,
    ...
    {ok, [Thing|Stuff]}.

在此,T 是一個新的變數,顯然只是打算作為用於提取 tuple 的第二個元素的臨時和局部變數。但是,假設有人在函數主體中更高的地方新增了名稱 T 的綁定,而沒有注意到該名稱已在使用

g(Stuff) ->
    ...
    T = q(Stuff) + 1,
    io:format("~p", [p(T)]),
    ...
    Thing = case ... of
                {a, T} -> T;
                _ -> 0
            end,
    ...
    {ok, [Thing|Stuff]}.

現在,只有當 tuple 的第二個元素具有與先前定義的 T 完全相同的值時,case 切換的第一個子句才會匹配。同樣,編譯器會悄悄地接受此變更,但如果指示編譯器警告模式中所有未註解的使用已綁定變數,則會偵測到此錯誤。

Funs 和 Comprehension 中的遮蔽 #

在 fun 和 comprehension 中,固定也讓我們可以執行其他需要額外暫時變數的事情。考慮以下程式碼

f(X, Y) ->
    F = fun ({a, Y}) -> {ok, Y};
            (_) -> error
        end,
    F(X).

由於 fun 的子句頭中的 Y 是一個新的遮蔽實例,因此該模式將匹配該位置的任何值。若要只匹配傳遞給 fY 值,必須新增子句保護,而且必須使用暫時變數來存取外部的 Y

f(X, Y) ->
    OuterY = Y,
    F = fun ({a, Y}) when Y =:= OuterY -> {ok, Y};
            (_) -> error
        end,
    F(X).

我們可以改為重新命名 Y 的內部使用,以避免遮蔽,但相等測試仍然必須寫成明確的保護

f(X, Y) ->
    F = fun ({a, Z}) when Z =:= Y -> {ok, Y};
            (_) -> error
        end,
    F(X).

借助固定運算子,這類事情不再是問題,我們可以簡單地寫成

f(X, Y) ->
    F = fun ({a, ^Y}) -> {ok, Y};
            (_) -> error
        end,
    F(X).

此外,在需要同時存取 Y 的周圍定義以及引入新的遮蔽綁定的奇數情況下,可以使用固定輕鬆地寫成

f(X, Y) ->
    F = fun ({a, ^Y, Y})  -> {ok, Y};
            (_) -> error
        end,
    F(X).

但在目前的 Erlang 中,需要兩個單獨的暫時變數

f(X, Y) ->
    OuterY = Y,
    F = fun ({a, Temp, Y}) when Temp =:= OuterY -> {ok, Y};
            (_) -> error
        end,
    F(X).

如先前所述,comprehension 的生成器中的模式也是如此。

向後相容性 #

新增一個新的且先前未使用的運算子 ^ 不會影響現有程式碼的含義,而且編譯器不會針對現有程式碼發出任何新的警告或錯誤,除非明確使用 warn_unpinned_vars 啟用。因此,此變更完全向後相容。

實作 #

可以在 PR #2951 中找到實作。

版權 #

本文檔已置於公有領域。