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) 引发异常。
以前由于这些运算符具有如此令人惊讶的行为而损坏的代码现在将在更多情况下工作。
无。
本文档已置于公共领域。