Erlang 5.1 新增了在 guard 中使用 ‘andalso’、‘orelse’、‘and’ 和 ‘or’ 的功能。然而,‘andalso’ 和 ‘orelse’ 的語意與其他相關語言不同,造成混淆和效率低下。
我提議讓 ‘andalso’ 和 ‘orelse’ 的運作方式分別像 Lisp 的 AND 和 OR。
目前,(E1 andalso E2) 作為一個表達式的作用就像
case E1
of false -> false
; true -> case E2
of false -> false
; true -> true
end
end
除了在我的測試中,前者會引發 {badarg,NonBool}
異常,而後者會引發 {case_clause,NonBool}
異常。
應該將其更改為
case E1
of false -> false
; true -> E2
end.
目前,(E1 orelse E2) 作為一個表達式的作用就像
case E1
of true -> true
; false -> case E2
of true -> true
; false -> false
end
end
除了在我的測試中,前者會引發 {badarg,NonBool}
異常,而後者會引發 {case_clause,NonBool}
異常。
應該將其更改為
case E1
of true -> true
; false -> E2
end
顯然有一種民間傳說認為在 guard 中使用 ‘andalso’(或 ‘orelse’)會比使用 ‘,’(或 ‘;’)產生更好的程式碼。事實恰恰相反,你會得到相當糟糕的程式碼。請參閱「動機」中的範例。這應該改變。
guard ::= gconj {';' gconj}*
gconj ::= gtest {',' gtest}*
gtest ::= '(' guard ')' | ...
首先,我們允許使用括號來巢狀使用 ‘,’ 和 ‘;’。其次,我們規定,作為 guard 中的外部運算子,‘,’ 和 ‘andalso’ 之間的唯一區別在於優先順序,而 ‘;’ 和 ‘orelse’ 之間的唯一區別也在於優先順序。在像這樣的 guard 測試中
is_atom(X andalso Y)
‘andalso’ 不能被 ‘,’ 取代,但是只要一個可以被另一個取代,它們應該具有相同的效果。
Common Lisp
(defun member-p (X Xs)
(and (consp Xs)
(or (equal X (first Xs))
(member-p X (rest Xs)))))
Scheme
(define (member? X Xs)
(and (pair? Xs)
(or (equal? X (car Xs))
(member? X (cdr Xs)))))
Standard ML
fun is_member(x, xs) =
not (null xs) andalso (
x = hd xs orelse is_member(x, tl xs))
Haskell
x `is_member_of` xs =
not (null xs) && (x == head xs || x `is_member_of` tail xs)
Dylan
我不太了解 Dylan 的語法,無法完成這個範例,但我知道 Dylan 中的 ‘&’ 和 ‘|’ 與 Common Lisp 中的 AND 和 OR 完全相同,只是語法不同。(它們的文件說明允許右運算元傳回任何值,包括多個值。)
Python
def is_member(x, xs):
n = len(xs)
return n > 0 and (x == xs[0] or is_member(x, xs[1:n]))
我不太確定這一點,但參考手冊非常明確地指出 ‘and’ 或 ‘or’ 的第二個運算元可以是任何東西。
Smalltalk
在 Smalltalk 中以這種方式執行這個範例需要付出相當大的痛苦來違背 Smalltalk 的慣例,然而 Smalltalk 中的 ‘and:’ 和 ‘or:’ 選取器會檢查它們的第一個參數是否為布林值,並且不會檢查它們的第二個參數(的結果)。
在所有這些語言中,「and」和「or」運算的工作方式完全相同,並且在其實作支援尾遞迴的語言(Common Lisp、Scheme、Standard ML、Haskell)中,上面顯示的函數是尾遞迴的。(我可以將更多語言添加到列表中。)
Erlang 顯得格格不入。‘andalso’ 的行為令人驚訝,而且 ‘andalso’ 和 ‘orelse’ 會阻止尾遞迴的事實更是令人震驚。我完全贊成給程式設計師一些衝擊,讓他們學到一些有用的程式設計知識,但這並不是一個有用的教訓。測試 ‘and’ 和 ‘or’ 的兩個參數是有道理的,因為為這些運算子執行的程式碼總是會取得兩個運算元的值。但 ‘andalso’ 和 ‘orelse’ 只在某些時候測試它們的第二個運算元。
X = 1, X >= 0 andalso X % checked error
X = 1, X < 0 andalso X % unchecked error
在某些時候進行檢查似乎沒有太大意義,尤其是當它會做出像阻止尾遞迴這樣戲劇性的事情時。
至於 guard,這裡有一個小範例
f(X) when X >= 0, X < 1 -> math:sqrt(X).
這會編譯成以下相當明顯的程式碼
function, f, 1, 2}.
{label,1}.
{func_info,{atom,bar},{atom,f},1}.
{label,2}.
{test,is_ge,{f,1},[{x,0},{integer,0}]}.
{test,is_lt,{f,1},[{x,0},{integer,1}]}.
{call_ext_only,1,{extfunc,math,sqrt,1}}.
有些人期望 ‘andalso’ 能做得一樣好或更好。我期望它能做到相同的事情,而這個 EEP 要求它做到。這是原始碼
g(X) when X >= 0 andalso X < 1 -> math:sqrt(X).
這是 BEAM 指令
{function, g, 1, 4}.
{label,3}.
{func_info,{atom,bar},{atom,g},1}.
{label,4}.
{allocate,1,1}.
{move,{x,0},{y,0}}.
{test,is_ge,{f,5},[{x,0},{integer,0}]}.
{bif,'<',{f,7},[{x,0},{integer,1}],{x,0}}.
{jump,{f,6}}.
{label,5}.
{move,{atom,false},{x,0}}.
{label,6}.
{test,is_eq_exact,{f,7},[{x,0},{atom,true}]}.
{move,{y,0},{x,0}}.
{call_ext_last,1,{extfunc,math,sqrt,1},1}.
{label,7}.
{move,{y,0},{x,0}}.
{deallocate,1}.
{jump,{f,3}}.
它不僅做了更多的工作,甚至還分配了一個傳統程式碼沒有的堆疊框架。
有幾種方法可以處理 ‘andalso’ 和 ‘orelse’ 令人驚訝的行為。
保持現狀。
手冊應該新增許多警告,說明不要使用這些運算子,因為它們會阻止尾遞迴並且在 guard 中效率低下。
首先處理其他問題是合理的,但從長遠來看,這是不行的。你不必急著去包紮你遇到的每個人,但你也不應該在他們面前建造陷阱。
從語言中移除它們。
我會更喜歡這樣。對於 ‘and’ 和 ‘or’ 來說也是如此,它們似乎完全沒有意義,而且還會造成混淆。我不認為這在政治上是可行的。
新增具有合理語意的新運算子。
但我們該如何稱呼它們呢? ‘and’ 和 ‘or’ 已經被佔用,而 ‘|’ 和 ‘||’ 都用於其他用途。最重要的是,‘andalso’ 和 ‘orelse’ 仍然會存在,而且仍然會令人驚訝(以不好的方式)。我們已經有太多種拼寫「or」的方式了。
修正它們。
至於 ‘,’ 和 ‘;’ 應該巢狀使用的建議,我希望 Erlang 能夠簡單易懂。如果 ‘andalso’ 和 ‘orelse’ 在 guard 中的作用要像 ‘,’ 和 ‘;’ 一樣 – 我在上面已經論證過 – 那麼顯然 ‘,’ 和 ‘;’ 在 guard 中的作用應該像 ‘andalso’ 和 ‘orelse’ 一樣。
任何在執行時沒有引發異常的程式碼都會繼續產生相同的結果,只是執行速度更快。
過去引發異常的程式碼可能會在稍後的其他地方引發不同的異常,或可能會以意想不到的方式安靜地完成。我相信很少有人故意依賴 (E1 andelse 0) 引發異常。
之前因為這些運算子具有如此令人驚訝的行為而損壞的程式碼現在在更多情況下可以正常工作。
無。
本文檔已放入公共領域。