为什么大多数主流语言不支持“ x <y <z”语法进行三路布尔比较?


34

如果我想比较两个数字(或其他排序良好的实体),则可以使用x < y。如果我想比较其中的三个,那位高中代数的学生会建议尝试x < y < z。然后,我中的程序员将回答“不,那是无效的,您必须这样做x < y && y < z”。

我遇到的大多数语言似乎都不支持这种语法,考虑到它在数学中的普遍性,这很奇怪。Python是一个明显的例外。JavaScript 看起来像是一个异常,但实际上,它只是运算符优先级和隐式转换的不幸产物。在node.js中,1 < 3 < 2评估为true,因为它确实是(1 < 3) < 2 === true < 2 === 1 < 2

所以,我的问题是这样的:为什么x < y < z具有预期语义的编程语言不常见?


1
这是语法文件,他们很方便地将其粘贴在Python文档中-我不认为这很困难:docs.python.org/reference/grammar.html
Aaron Hall

我不了解其他语言,也不了解Python,但是我可以说Python解释它的简单性。也许我应该回答。但是我不同意gnasher729关于它造成损害的结论。
亚伦·霍尔

@ErikEidt-需求是能够按照我们在高中(或更早)的教学方式来编写数学表达式。每个有数学倾向的人都知道$ a <b <c <d $的含义。仅仅因为功能存在并不意味着您必须使用它。那些不喜欢它的人总是可以制定个人或项目规则来禁止使用它。
David Hammen

2
我认为结果是,C#团队(例如)探索LINQ更好,并且在将来也许记录类型和模式匹配要比添加一些语法糖来节省人们4次击键,而不是真的添加任何表现力(您也可以编写辅助方法,例如,static bool IsInRange<T>(this T candidate, T lower, T upper) where T : IComparable<T>是否真的让您感到困扰&&
sara

1
SQL相当“主流”,您可以写“ 1到10之间的x”
JoelFan

Answers:


30

这些是二进制运算符,将它们链接起来后,通常会自然地生成一个抽象的语法树,例如:

二进制运算符的常规抽象语法树

进行评估(从头开始执行)时,这会从产生布尔结果x < y,然后会出现尝试执行的类型错误boolean < z。为了x < y < z如您所讨论的那样工作,您必须在编译器中创建一个特例以产生如下语法树:

特殊情况语法树

不是说不可能做到这一点。显然是这样,但是对于这种情况很少真正出现的情况,它为解析器增加了一些复杂性。您基本上是在创建一个符号,该符号有时像二进制运算符一样工作,有时又像三元运算符一样有效地工作,并带有错误处理的所有隐含含义。这就为出错的地方增加了很多空间,语言设计者宁愿避免这种情况。


1
“然后您会遇到尝试执行布尔值<z的类型错误”-如果编译器允许通过为z比较而就地评估y来允许链接,则不是这样。“这为出错的地方增加了很多空间,语言设计者宁愿避免这种情况。” 实际上,Python做到这一点没有问题,而且解析的逻辑仅限于一个函数:hg.python.org/cpython/file/tip/Python/ast.c#l1122-没有很多空间可以处理事情错误。“有时像二进制运算符,有时又像三进制运算符。”在Python中,整个比较链都是三元的。
亚伦·霍尔

2
我从来没有说过这是不可行的,只是额外的工作和额外的复杂性。其他语言不必只为处理其比较运算符而编写任何单独的代码。您可以通过其他二进制运算符免费获得它。您只需要指定它们的优先级即可。
Karl Bielefeldt

是的,但是......还有就是已经是三元运营商在很多的语言吗?
JensG

1
@JensG三元数的表示意味着它需要3个参数。在您的链接上下文中,它是一个三元条件运算符。对于似乎需要2却实际上需要3的运算符,显然是用术语“三进制”表示的。这个答案的主要问题是,它主要是FUD。
亚伦·霍尔

2
我是这个公认答案的拒绝者之一。(@JesseTG:请拒绝接受此答案。)此问题使x<y<z含义更混乱,或更重要的是,x<y<=z。此答案解释x<y<z为三元运算符。这就是不应该解释这种定义明确的数学表达式的方式。x<y<z相反是的简写(x<y)&&(y<z)。各个比较仍然是二进制的。
大卫·哈门

37

为什么x < y < z在编程语言中通常不可用?

在这个答案中,我得出结论:

  • 尽管这种构造对于用语言的语法实现来说是微不足道的,并且可以为语言用户创造价值,
  • 大多数语言中不存在该语言的主要原因是由于它相对于其他功能的重要性以及语言管理机构不愿意使用任何一种语言
    • 可能会给用户带来重大变化而使用户不满
    • 移动以实现该功能(即懒惰)。

介绍

我可以从Pythonist的角度谈这个问题。我是具有此功能的语言的用户,并且我想研究该语言的实现细节。除此之外,我对更改C和C ++等语言(ISO标准由委员会管理并按年份进行版本化)的过程有些熟悉,并且我已经看到Ruby和Python都实现了重大更改。

Python的文档和实现

从文档/语法中,我们看到可以使用比较运算符链接任意数量的表达式:

comparison    ::=  or_expr ( comp_operator or_expr )*
comp_operator ::=  "<" | ">" | "==" | ">=" | "<=" | "!="
                   | "is" ["not"] | ["not"] "in"

并且文档进一步指出:

比较可以任意链接,例如,x <y <= z等效于x <y和y <= z,除了y仅被评估一次(但是在两种情况下,当x <y被发现时,z都不被评估。是假的)。

逻辑对等

所以

result = (x < y <= z)

在评估,和时在逻辑上是等效x,但两次评估除外:yzy

x_lessthan_y = (x < y)
if x_lessthan_y:       # z is evaluated contingent on x < y being True
    y_lessthan_z = (y <= z)
    result = y_lessthan_z
else:
    result = x_lessthan_y

同样,区别在于y仅用评估一次(x < y <= z)

(请注意,括号是完全不必要和多余的,但是我使用它们是出于其他语言的好处,并且上面的代码是合法的Python。)

检查解析的抽象语法树

我们可以检查Python如何解析链式比较运算符:

>>> import ast
>>> node_obj = ast.parse('"foo" < "bar" <= "baz"')
>>> ast.dump(node_obj)
"Module(body=[Expr(value=Compare(left=Str(s='foo'), ops=[Lt(), LtE()],
 comparators=[Str(s='bar'), Str(s='baz')]))])"

因此,我们可以看到,对于Python或任何其他语言而言,解析它确实并不困难。

>>> ast.dump(node_obj, annotate_fields=False)
"Module([Expr(Compare(Str('foo'), [Lt(), LtE()], [Str('bar'), Str('baz')]))])"
>>> ast.dump(ast.parse("'foo' < 'bar' <= 'baz' >= 'quux'"), annotate_fields=False)
"Module([Expr(Compare(Str('foo'), [Lt(), LtE(), GtE()], [Str('bar'), Str('baz'), Str('quux')]))])"

与当前接受的答案相反,三元运算是一种通用比较运算,它采用第一个表达式,特定比较的可迭代项和表达式节点的可迭代项进行必要的评估。简单。

关于Python的结论

我个人发现范围语义非常优雅,并且我认识的大多数Python专业人士都鼓励使用该功能,而不是考虑它具有破坏性-语义已在声誉卓著的文档中明确说明(如上所述)。

请注意,读取的代码远比编写的要多。应该接受能够提高代码的可读性的更改,而不应通过提高恐惧,不确定性和怀疑等通用特性来加以接受。

那么为什么x <y <z在编程语言中通常不可用?

我认为,有多种原因合在一起,围绕该功能的相对重要性以及语言的总督所允许的相对变化动量/惯性。

关于其他更重要的语言功能,可以问类似的问题

为什么Java或C#中没有多重继承?这两个问题都没有好的答案。正如鲍勃·马丁(Bob Martin)所说,也许开发人员太懒了,给出的理由仅仅是借口。多重继承是计算机科学中一个相当大的话题。当然,它比操作员链接更重要。

存在简单的解决方法

比较运算符链接很优雅,但绝不比多重继承重要。就像Java和C#具有接口作为解决方法一样,每种语言都可以进行多次比较-您只需将比较与布尔“和”链接起来即可,这很容易工作。

大多数语言受委员会管辖

大多数语言都是由委员会发展而来的(而不是像Python那样拥有明智的人生独裁者)。我推测这个问题尚未得到足够的支持,无法使其脱离其各自的委员会。

不提供此功能的语言可以更改吗?

如果一种语言允许x < y < z没有预期的数学语义,那将是一个重大的改变。如果一开始不允许这样做,那么添加它几乎是微不足道的。

重大变化

关于具有重大更改的语言:我们确实具有重大更改来更新语言-但是用户往往不喜欢这种行为,尤其是那些可能已破坏功能的用户。如果用户依赖的先前行为x < y < z,则他们可能会大声抗议。而且由于大多数语言都是由委员会管理的,所以我怀疑我们是否会获得支持这种改变的政治意愿。


坦白地说,我对链接比较操作(例如x <y <z)的语言所提供的语义没有任何疑问,但对于开发人员而言,在思维上映射到并不x < y < z容易(x < y) && (y < z)。采摘尼特,链式比较的心理模型是通用数学。一般而言,经典比较不是数学,而是布尔逻辑。x < y产生二元答案{0}y < z产生二元答案{1}{0} && {1}产生描述性答案。逻辑是组合的,不是幼稚的链接。
K. Alan Bates

为了更好地沟通,我在回答的开头加了一个句子,直接总结了整个内容。这句话很长,所以我把它分解成子弹。
亚伦·霍尔

2
很少有语言实现此功能的主要原因是,在Guido之前,没有人考虑过它。从C继承的语言现在无法获得“正确”(在数学上正确),这主要是因为40年前C的开发人员将其“错误”(在数学上错误)。那里有很多代码,取决于这些语言如何解释的违反直觉的本质x<y<z。一门语言只有一次获得这种权利的机会,而这是一门诞生之初。
David Hammen

1
@ K.AlanBates您有2点:1)运算符链接很草率,2)语法糖没有价值。首先:我已经证明了运算符链接是100%确定性的,不是吗?也许有些程序员在精神上懒得扩展自己的理解能力?第二点:在我看来,您是在直接反对可读性?当语法糖提高可读性时,它通常不是好东西吗?如果以这种方式思考是正常的,那么程序员为什么不希望如此交流呢?代码应该被阅读,不是吗?
亚伦·霍尔

2
I have watched both Ruby and Python implement breaking changes.对于那些谁是好奇,这里是在C#5.0中的重大更改,涉及循环变量和封锁:blogs.msdn.microsoft.com/ericlippert/2009/11/12/...
user2023861

13

计算机语言尝试定义最小的单位,然后将它们组合在一起。可能的最小单位是“ x <y”,它给出布尔结果。

您可能会要求三元运算符。一个示例是x <y <z。现在,我们允许哪些运算符组合?显然,x> y> z或x> = y> = z或x> y> = z或应该允许x == y == z。那么x <y> z呢?x!= y!= z?最后一个是x!= y和y!= z是什么意思,或者这三个都不同?

现在参数提升:在C或C ++中,参数将提升为通用类型。那么x <y <z的x是什么意思,但是x和y是long long int?所有三个提升为两倍?还是y被取一次为double,而另一时间取为long long int?如果在C ++中一个或两个运算符都重载了,会发生什么?

最后,您允许任何数量的操作数吗?像a <b> c <d> e <f> g?

好吧,这一切都变得非常复杂。现在我不介意的是x <y <z会产生语法错误。因为与不了解x <y <z实际作用的初学者造成的损害相比,它的用处很小。


4
简而言之,这只是设计良好的一项难点。
乔恩·普迪

2
这实际上不是解释为什么没有众所周知的语言包含此功能的原因。实际上,以明确定义的方式将其包含在语言中非常容易。只是将其视为由类似类型的运算符连接的列表,而不是每个运算符都是二进制的列表。可以对sum进行相同的操作x + y + z,唯一的区别是,这并不意味着任何语义上的区别。因此,只是没有众所周知的语言愿意这样做。
cmaster

1
我认为在Python中这是一种优化(x < y < z相当于((x < y) and (y < z))y只评估了一次),我想编译语言会优化它们的处理方式。“由于与无法弄清楚x <y <z实际作用的初学者造成的损害相比,它的用处很小。” 我确实认为这很有用。大概是-1 ...
Aaron Hall

如果一个人的目标是设计一种语言,以消除可能会使最愚蠢的程序员感到困惑的所有事物,那么这种语言已经存在:COBOL。我宁愿自己使用python,a < b > c < d > e < f > g意思是“显而易见” ,在这里确实可以写(a < b) and (b > c) and (c < d) and (d > e) and (e < f) and (f > g)。仅仅因为您可以写那并不意味着您应该这样做。消除这种怪异是代码审查的职责。另一方面,0 < x < 8用python 编写具有明显的名称(没有吓人的引号),这意味着x位于0到8之间(不包括在内)。
大卫·哈门

@DavidHammen,具有讽刺意味的是,COBOL确实允许<b <c
JoelFan

10

在许多编程语言中,x < y是一个二进制表达式,它接受两个操作数并求值为单个布尔结果。 因此,如果链接多个表情,true < zfalse < z将没有任何意义,如果这些表达式成功评价,他们很可能会产生错误的结果。

将其x < y视为带有两个参数并产生单个布尔结果的函数调用要容易得多。实际上,这就是多少语言在后台实现它。它是可组合的,易于编译,并且可以正常工作。

x < y < z情况要复杂得多。现在,实际上,编译器必须构造三个函数:x < yy < z和,并将这两个值的结果并在一起,所有这些都可以在一个模棱两可的语言语法中进行

他们为什么这样做呢?因为它是明确的语法,所以更容易实现,而且更容易正确。


2
如果您正在设计语言,则有机会使其正确
JesseTG '16

2
当然,它回答了这个问题。如果问题确实是为什么,答案是“因为这是语言设计师选择的”。如果您能找到比这更好的答案,那就去吧。请注意,Gnasher在回答的第一段中基本上说了同样的话。
罗伯特·哈维

3
再次,您要分开头发。程序员倾向于这样做。“你想拿垃圾吗?” “没有。” “你会把垃圾拿出来吗?” “是。”
罗伯特·哈维

2
我也对最后一段提出质疑。Python支持链比较,其解析器为LL(1)。定义和实现语义也不一定很困难:Python只是说这e1 op1 e2 op2 e3 op3 ...等同于e1 op e2 and e2 op2 e3 and ...每个表达式只被评估一次。(顺便说一句,这个简单的规则具有令人困惑的副作用,即a == b is True不再像声明那样具有预期的效果。)

2
@RobertHarvey re:answer这就是我对主要问题发表意见后立即想到的地方。我不认为支持为x < y < z语言语义添加任何特定的值。 (x < y) && (y < z)得到更广泛的支持,更明确,更富有表现力,更容易被其成分消化,更易组合,更合乎逻辑,更易于重构。
K. Alan Bates

6

大多数主流语言(至少部分地)是面向对象的。从根本上说,OO的基本原理是对象将消息发送到其他对象(或它们自己),并且该消息的接收者完全控制如何响应该消息。

现在,让我们看看如何实现类似

a < b < c

我们可以严格从左到右(左关联)进行评估:

a.__lt__(b).__lt__(c)

但现在我们调用__lt__的结果a.__lt__(b),即Boolean。这是没有意义的。

让我们尝试右关联:

a.__lt__(b.__lt__(c))

不,那也没有道理。现在,我们有了a < (something that's a Boolean)

好的,将其作为语法糖处理。让我们通过n个<比较的链发送n-1个消息。这可能意味着,我们将消息发送__lt__a,传递bc作为参数:

a.__lt__(b, c)

好的,这行得通,但是这里有一个奇怪的不对称性:a决定它是否小于b。但b没有得到决定是否低于c,而不是决定也是由制作a

将其解释为发送给n元的消息该this怎么办?

this.__lt__(a, b, c)

最后!这可以工作。但是,这意味着对象的排序不再是对象的属性(例如,是否a小于b既不是对象的属性a也不是对象的属性b),而是上下文的属性(即this)。

从主流的角度看,这似乎很奇怪。但是,例如在Haskell中,这是正常的。Ord例如,类型类可以有多种不同的实现,并且是否a小于b,取决于哪个类型类实例恰好在范围内。

但实际上,它不是那个奇怪了!Java(Comparator)和.NET(IComparer)都具有允许您将自己的排序关系注入例如排序算法的接口。因此,他们完全承认排序不是固定于类型的东西,而是取决于上下文。

据我所知,目前尚无任何语言可以执行这种翻译。但是,有一个先例:IokeSeph都具有其设计者所称的“三元运算符”- 语法上为二进制,而语义上为三进制的运算符。特别是,

a = b

解释为将所述消息发送=a传递b作为参数,而是作为发送该消息=的“当前地”(类似但不相同的一个概念this)使ab作为参数。所以,a = b被解释为

=(a, b)

并不是

a =(b)

这很容易推广到n元运算符。

请注意,这确实是面向对象语言所特有的。在OO中,我们始终只有一个对象,该对象最终负责解释消息发送,并且正如我们所看到的,对于像a < b < c应该是哪个对象这样的事物,并不是立即显而易见的。

但是,这不适用于过程或功能语言。例如,在SchemeCommon LispClojure中,该<函数为n元,可以使用任意数量的参数调用。

具体而言,<确实不是意味着“小于”,而这些功能都稍有不同的解释:

(<  a b c d) ; the sequence a, b, c, d is monotonically increasing
(>  a b c d) ; the sequence a, b, c, d is monotonically decreasing
(<= a b c d) ; the sequence a, b, c, d is monotonically non-decreasing
(>= a b c d) ; the sequence a, b, c, d is monotonically non-increasing

3

仅仅是因为语言设计师没有想到它,或者认为这不是一个好主意。Python按照您描述的简单(几乎)LL(1)语法进行操作。


1
在几乎所有主流语言中,这仍将与普通语法一起解析。只是由于@RobertHarvey给出的原因而无法正确理解。
梅森惠勒

@MasonWheeler不,不一定。如果编写规则以使比较可以与其他运算符互换(例如,因为它们具有相同的优先级),那么您将不会获得正确的行为。Python将所有比较都放在一个层次上的事实使它可以将序列视为合取。
Neil G

1
并不是的。放入1 < 2 < 3Java或C#,操作符优先级就没有问题;您有无效类型的问题。问题在于,它仍将按照您编写的方式进行完全解析,但是您需要在编译器中使用特殊情况的逻辑,才能将其从一系列单独比较转换为链式比较。
梅森惠勒

2
@MasonWheeler我的意思是必须设计语言才能使其正常工作。其中一部分是正确的语法。(如果编写规则以使比较与其他运算符可以互换,例如,因为它们具有相同的优先级,那么您将不会获得正确的行为。)其中的另一部分是将AST解释为连接,C ++不做。我的回答的重点是这是语言设计师的决定。
Neil G

@MasonWheeler我认为我们同意。我只是强调指出,为此语法不难。只需预先确定您希望它以这种方式工作即可。
Neil G

2

即使警告设置为最高级别(-Weverything),以下C ++程序也可以从clang窥视一眼:

#include <iostream>
int main () { std::cout << (1 < 3 < 2) << '\n'; }

另一方面,gnu编译器套件很好地警告了我这一点comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses]

所以,我的问题是:为什么x <y <z在具有预期语义的编程语言中不常见?

答案很简单:向后兼容。在野外有大量代码使用等价的代码,1<3<2并期望结果是真实的。

语言设计师只有一个机会来实现这种“正确”,这就是该语言最初设计的时间点。最初将它理解为“错误”意味着其他程序员将相当快地利用这种“错误”行为。第二次“正确”实现将破坏现有的代码库。


+1是因为该程序输出“ 1”作为表达式的结果,该表达式在数学上显然是错误的。尽管有人为,但添加了此语言功能后,带有难以理解的变量名的实际示例将成为调试的噩梦。
塞斯·巴丁

@SethBattin-这不是Python中的调试噩梦。Python唯一的问题是if x == y is True : ...我的看法:编写这种代码的人应该受到某种特殊的折磨,这种折磨(如果他现在还活着的话)会使Torquemada自己晕倒。
David Hammen
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.