Erlang中的if:讚美與非議

Hotlink發表於2022-04-24

本文是考古Erlang語言中關於if語句的討論。

語法

首先,我們該如何看待Erlang中的“if”語義呢?在Armstrong編寫的《Erlang程式設計》一書中,很明確地將"case和if表示式"一起放在了同一節,並且先介紹了case,後介紹了if。在介紹if時,給出的語法例子如下:

if
  Guard1 ->
    Expr_seq1;
  Guard2 ->
    Expr_seq2;
  ...
end

可以從這個句式很明顯地看出,整個if語句更像是一個case語句,通過一條條關卡(Guard)分割執行不同的子句。if語句會執行每一條關卡,如果結果為true則執行子表示式並結束,如果為false則依次向下匹配直到有一個關卡為true。

這裡需要注意的一點是,整個語句必須保證至少有一個關卡為true,否則整段語句就會丟擲異常。這樣的語句在特定條件下會是一個異常錯誤:

if
  A > 0 ->
    do_this()
end

除非你有意想讓錯誤發生。當然,一位熟悉編碼的工程師知道,任何隱藏的意圖與可能的失誤混淆在一起時,整段程式碼都會變得難以閱讀和理解,即使用註釋明確標註,在之後的演進中也會難以迭代。推薦的做法以及各類專案中的實踐是,在最後一個關卡中使用原子true,保證匹配最後一條子表示式。這就像Java中case最後的default一樣。

if
  Guard1 ->
    Expr_seq1;
  Guard2 ->
    Expr_seq2;
  ...
  true ->
    Expr_default
end

決策表?

檢視Erlang各類說明文件,stackoverflow中的例子,部落格,都會優先推薦case,而非if。這裡有一封歸檔郵件可以很清晰地說明if在開發者眼中的地位:http://erlang.org/pipermail/e... (也是Erlang編碼規範中援引的說明用例)。

簡單描述背景:Richard A. O'Keefe(直接搜尋名字就可以找到這位發表了不少計算機語言學研究論文的Otago大學研究員)支援“聰明”地使用if語句。他給出的郵件標題就是“讚美Erlang中的if”。說明中列舉了一篇論文,表明“結構化流程圖優於虛擬碼”的觀點,傳統的虛擬碼類似:

IF GREEN
   THEN
      IF CRISPY
         THEN
            STEAM
         ELSE
            CHOP
      ENDIF
   ELSE
      FRY
      IF LEAFY
         THEN
            IF HARD
               THEN
                  GRILL
               ELSE
                  BOIL
            ENDIF
         ELSE
            BAKE
      ENDIF
ENDIF

常見的巢狀if引發的邏輯混亂。在Erlang中,通過巧妙地利用Erlang語法,可以把這一段邏輯變化為以下模式:

if     Green,     Crispy                    -> steam()
 ;     Green, not Crispy                    -> chop()
 ; not Green,               Leafy,   Hard   -> fry(), grill()
 ; not Green,               Leafy, not Hard -> fry(), boil()
 ; not Green,           not Leafy           -> fry(), bake()
end

本質上是利用Erlang中的分號,逗號,空白符,創造出一張視覺上的“決策表”,能夠清晰地表明每個分支對應的條件。

不過這種寫法是不是讓你的神經感受到了某種“奇技淫巧”,直覺上我們的程式碼中應該規避所有這一類寫法取巧但難以理解/維護的程式碼,除非這段程式碼是至關重要的效能優化節點。而且,在編譯器發展成熟的今天,即使是你認為的“效能優化”往往到了編譯時會變得面目全非,也一定要經過效能測試才行,常常你做的這類優化根本無法比上編譯器做的優化。

迴歸簡樸

Anthony Ramine在回覆郵件中首先就指出了這種寫法奇怪的縮排給程式設計師帶來的困擾。

其次,分號和逗號的混用在這種方式下難以被注意到,甚至寫錯了也難以被自動檢測出來,例如他構建的以下例子(這裡第三個分支的逗號改為了分號):

if     Green,     Crispy                    -> steam()
 ;     Green, not Crispy                    -> chop()
 ; not Green;               Leafy,   Hard   -> fry(), grill()
 ; not Green,               Leafy, not Hard -> fry(), boil()
 ; not Green,           not Leafy           -> fry(), bake()
end

可以看到這類問題在Erlang開發中經常發生:https://github.com/rebar/reba...

對Erlang中if的評論甚至到了這種地步:https://stackoverflow.com/que...

"I have found that if you are relying on guards or case statements, you are probably 'doing it wrong' most of the time in Erlang."

為此,Anthony更希望去掉if語句中的關卡子句,甚至不再試用if子句。

順帶補充一下,在這個例子中,非綠色,沒有葉子,也不堅硬的蔬菜將因為無法烹飪而報錯。

編碼規範

在我們參考的編碼規範中,結合大家的開發經驗,也提出了少用/不用if語句的要求。

改造程式碼中的if語句,我們可以用case(更容易和其它語言一樣理解),而模式匹配是最好的選擇:

-module(no_if).
 
-export([bad/1, better/1, good/1]).
 
bad(Connection) ->
  {Transport, Version} = other_place:get_http_params(),
  if
    Transport =/= cowboy_spdy, Version =:= 'HTTP/1.1' ->
      [{<<"connection">>, utils:atom_to_connection(Connection)}];
    true ->
      []
  end.
 
 
better(Connection) ->
  {Transport, Version} = other_place:get_http_params(),
  case {Transport, Version} of
    {cowboy_spdy, 'HTTP/1.1'} ->
      [{<<"connection">>, utils:atom_to_connection(Connection)}];
    {_, _} ->
      []
  end.
  
 
good(Connection) ->
  {Transport, Version} = other_place:get_http_params(),
  connection_headers(Transport, Version, Connection).
   
connection_headers(cowboy_spdy, 'HTTP/1.1', Connection) ->
    [{<<"connection">>, utils:atom_to_connection(Connection)}];
connection_headers(_, _, _) ->
    [].

參考資料

相關文章