左结合的前缀运算符或右结合的后缀运算符是否存在?

8

这个页面说:“前缀运算符通常是右结合的,后缀运算符是左结合的”(我强调了一下)。

是否有左结合的前缀运算符或右结合的后缀运算符的真实示例?如果没有,一个假设的运算符会是什么样子,如何解析?


相关链接:https://dev59.com/HWcs5IYBdhLWcg3wTyNl - Steve Jessop
也许最简单的答案是最好的。 “左结合”不是“右结合”的唯一选择。另一个可能性是“非关联性”。请参见我的回答中“new”的示例。我认为这就是您应该解释原始引用的方式:“前缀运算符通常是右结合的,但有时它们不是关联的....” - rici
这回答了你的问题吗?一元运算符具有结合性是否有意义? - anonymous38653
6个回答

3
“左结合”和“右结合”的概念并不容易准确解释,因为它们与任何清晰的语法特征都没有直接的对应关系。尽管如此,我还是会尝试解释一下。
尽管缺乏数学排版,但我在这里插入了一个关于优先级关系的解释(链接:https://dev59.com/ZGzXa4cB1Zd3GeqPSlk0#13983288),这是我所能做到的最好的解释,因此我不会再重复。基本思想是,在给定运算符文法(即没有两个非终结符没有中间终结符的产生式的文法)的情况下,可以定义文法符号之间的优先级关系“⋖”、“≐”和“⋗”,然后将此关系扩展到终结符。
简单地说,如果“a”和“b”是两个终结符,则当存在某个产生式中,“a”后面跟随一个非终结符,并且该非终结符有一个推导(可能不是立即的),其中第一个终结符是“b”时,“a ⋖ b”成立。“a ⋗ b”成立,当存在某个产生式中,“b”跟随一个非终结符,并且该非终结符有一个推导,在该推导的最后一个终结符是“a”时。当存在某个产生式中,如果“a”和“b”要么是连续的,要么是由单个非终结符分隔开的,则“a ≐ b”成立。使用看起来像算术比较的符号是不幸的,因为通常的算术定律都不适用。事实上,“a ≐ a”并不必须是真的(事实上,这很少发生);“a ≐ b”不意味着“b ≐ a”,并且可能情况下既“a ⋖ b”又“a ⋗ b”都为真或都为假。
如果给定任何两个终结符“a”和“b”,如果最多只有一个关系成立(即“a ⋖ b”、“a ≐ b”和“a ⋗ b”中最多只有一个成立),则运算符文法就是一个运算符优先级文法。
如果一个语法是操作符优先级语法,可能可以找到一组整数分配给终端,使得优先关系或多或少对应于整数比较。精确的对应很少可能,因为a ≐ a很少出现。然而,通常可以找到两个函数f(t)g(t),使得如果f(a) < g(b),则a ⋖ b为真,如果f(a) > g(b),则a ⋗ b为真。(我们不需要担心"仅当",因为可能不存在ab之间的关系,并且通常a ≐ b使用不同的机制处理:实际上,它意味着完全不同的东西。) %left%right(yacc/bison/lemon/...声明)构造了函数fg。它们的做法非常简单。如果操作符(OP)是"左结合"的,那么这意味着expr1 OP expr2 OP expr3必须被解析为<expr1 OP expr2> OP expr3,在这种情况下,可以从推导中看出OP ⋗ OP。同样地,如果ROP是"右结合"的,那么expr1 ROP expr2 ROP expr3必须被解析为expr1 ROP <expr2 ROP expr3>,在这种情况下,可以从推导中看出ROP ⋖ ROP
由于fg是单独的函数,所以这是可以的:左结合的运算符将具有f(OP) > g(OP),而右结合的运算符将具有f(ROP) < g(ROP)。这可以通过为每个优先级级别使用两个连续的整数,并依次将它们分配给fg(如果运算符是右结合的),或者依次将它们分配给gf(如果运算符是左结合的)来轻松实现。(这个过程将保证f(T)永远不等于g(T)。在通常的表达式语法中,唯一的≐关系是在开放和关闭括号类型符号之间,这些通常不会产生歧义,因此在yacc派生语法中不需要为它们分配优先级值。在Floyd解析器中,它们将标记为。)
那么前缀和后缀运算符呢?前缀运算符总是在以下形式的产生式[1]中找到:
non-terminal-1: PREFIX non-terminal-2;

没有任何非终止符在PREFIX之前,因此不可能有任何东西是⋗ PREFIX(因为a ⋗ b的定义需要在b之前有一个非终止符)。因此,如果PREFIX具有结合性,则必须是右结合的。同样,后缀运算符对应于:

non-terminal-3: non-terminal-4 POSTFIX;

因此,如果 POSTFIX 是可关联的,则必须是左关联的。
运算符可以是语义上或语法上非关联的(意思是将运算符应用于相同运算符的应用的结果是未定义或不合法的)。例如,在 C++ 中,++ ++ a 在语义上是不正确的(除非 aoperator++() 已被以某种方式重新定义),但是它已被语法接受(在情况下 operator++() 已被重新定义)。另一方面,new new T 不是语法上正确的。因此,new 是语法上非关联的。

[1] 在 Floyd 语法中,所有非终结符都合并为单个非终结符类型,通常是 expression。然而,优先关系的定义并不需要这样做,因此我使用了不同的占位符来表示不同的非终结符类型。


2
原则上是可以的。例如,考虑一元加和减前缀运算符:假设+是恒等操作符,-是一个数值的取反操作。
它们通常是右结合的,这意味着+-1相当于+(-1),结果为负一。
假设它们是左结合的,则表达式+-1将等同于(+-)1
因此,语言必须赋予子表达式+-一个含义。语言通常不需要它具有含义并且不会给出含义,但您可以想象一个函数式语言,其中将恒等操作符应用于否定操作符的结果是一个具有与否定操作符完全相同效果的操作符/函数。然后对于这个例子,完整表达式的结果仍然是-1。
实际上,如果将函数/运算符并排定义为左到右顺序应用两者具有相同效果的函数/运算符,那么无论您如何关联它们,都不会对表达式的结果产生影响。这只是定义(f g)(x) == f(g(x))的两种不同方式。不过,如果您的语言将+-定义为与-不同的含义,则关联方向将很重要(我怀疑对于习惯于“通常”语言的人来说,这种语言将非常难懂...)。
另一方面,如果该语言不允许并置运算符/函数,则前缀运算符必须是右结合的,以允许表达式+-1。不允许并置运算是另一种方式,它表示(+-)没有含义。

如果这些运算符是左结合的,那么你就是错的,因为+-1将等同于-(+1)。你混淆了运算符的应用和对运算符的解析代码的概念。 - SergeyS
简而言之,“左结合性”的含义是+-1等同于(+-)1。它并不代表其他任何意思。如果在您所描述的那种假想语言中,(+-)1等同于-(+1),那也没关系。但这不是“左结合性”的逻辑结果。 - Steve Jessop
根据你的逻辑,--1应该等于1,但在C#/C++中它等于0。因为解析器将--视为单独的运算符。解析器不会贪婪地从右侧结合。但表达式树对于这个例子会贪婪地从右侧结合。 - SergeyS
@SergeyS:从我的逻辑中并没有这样的事情。正如你所说,--是C#和C++中的一个独立运算符,它不会被解析为两个一元-的实例。因此,我们对“结合性”的任何讨论都与--1的词法分析或语法分析无关。实际上,它被分解为两个标记,--1。由于只有一个标记是运算符,因此结合性完全无关紧要。 - Steve Jessop
请查看我的答案,并告诉我您的想法。 - Rei Miyasaka
显示剩余3条评论

2
我不知道在真正的语言(例如,至少被十几个人使用过的语言)中是否存在这样的事情。我猜“通常”只是因为证明否定是几乎不可能的,所以避免争论琐事更容易通过不做绝对陈述来实现。
至于理论上如何做到这一点,似乎有两种可能性。假设你有两个前缀运算符@和#,你将把它们视为左结合,你可以将@#解析为等价于#(@(a))。至少对我来说,这似乎是一个非常糟糕的想法-理论上可能,但没有人希望使用的语言。
另一种可能性是@#a将被解析为(@#)a。在这种情况下,我们基本上将@和#组合成单个运算符,然后将其应用于a。
在大多数典型的语言中,这可能不会非常有趣(基本上与如果它们是右结合的含义相同)。另一方面,我可以想象一个面向多线程编程的语言,它规定单个运算符的应用始终是原子的-当您将两个运算符组合成具有左结合解析的单个运算符时,生成的融合运算符仍然是单个原子操作,而仅连续应用它们不会(必要)。
老实说,即使这也是有点牵强的,但我至少可以想象它是一种可能性。

我认为这个运算符并不完全相同。通常的定义允许我们说 */% 作为一组具有相同优先级的运算符是左结合的。但是,用 +- 的方向关联性来谈论它们的方向关联性是没有意义的。因此,我认为语法术语与优先级关系密切相关。(这与数学意义上的结合律不同;% 在那种意义下根本不是结合的)。 - rici
请看我的回答,了解前缀和后缀的结合性。 - SergeyS
注意:我已经重新撰写了这个答案。如果您想查看先前的评论,请参阅编辑历史记录。 - Jerry Coffin

1

我不想否定我自己提出的问题,但是在看了另外两个答案之后,我是否无意中提出了一个主观性问题,并且实际上左关联前缀和右关联后缀的解释只是undefined

记住,即使像表达式这样普遍的符号表示法也是建立在一些约定俗成的基础上,如果有一种边缘情况是这些约定从未考虑过的,那么也许,在某个标准委员会制定定义之前,最好假装它不存在。


我提供了一个带有左结合前缀运算符的假想语言的清晰示例。它有什么问题? - SergeyS
@SergeyS 这并不是错的,只是似乎它不是唯一正确的答案 - 也就是说,没有一个确定的答案。 - Rei Miyasaka

0
假设的例子。一种语言具有相同优先级的前缀运算符@和后缀运算符#。如果两个运算符都是左结合的,那么表达式@x#将等于(@x)#;如果两个运算符都是右结合的,则表达式@x#将等于@(x#)

如果运算符具有相反的结合性怎么办?由于不能允许这种情况,在(您所谓的)左结合情况下,该级别的所有前缀运算符都将绑定在所有后缀或中缀运算符之前(右结合情况相反),因此更容易通过_拆分_优先级级别来描述这一点,使这些前缀运算符具有更高的优先级。这确实是C/C++中的做法,我想大多数具有前缀和后缀运算符的语言也是如此。 - Marc van Leeuwen

0

我不记得有任何左相关的前缀运算符或右相关的后缀运算符。但我可以想象两者都很容易存在。它们不常见,因为人们对运算符的自然看法是:离身体更近的优先应用

C#/C++语言的简单例子:

~-3 等于 2,但 -~3 等于 4

这是因为这些前缀运算符是右相关的,对于 ~-3 来说,它意味着首先应用了 - 运算符,然后应用了 ~ 运算符到前面的结果。这将导致整个表达式的值等于 2

假设你可以想象,如果这些运算符是左相关的,那么对于 ~-3 来说,首先应用最左边的运算符 ~,然后再应用 - 到前面的结果。这将导致整个表达式的值等于 4

[编辑] 回答 Steve Jessop 的问题:

Steve说:“左结合的意思是+-1等同于(+-)1”

我不同意这个观点,认为它完全是错误的。为了更好地理解左结合,请考虑以下示例:

假设我有一种假想的编程语言,其中前缀运算符是左结合的:

@ - 将操作数乘以3

# - 将7添加到操作数

那么在我的语言中,以下构造@#5将等于(5*3)+7 == 22。如果我的语言是右结合的(像大多数常见的语言一样),那么我将得到(5+7)*3 == 36

如果您有任何问题,请告诉我。


我认为你的编辑并没有解释任何东西。它只是重申了你已经陈述过的“左结合”的含义。我不同意这个含义,因为在其他地方我所遇到的结合性含义并不是这样的。 - Steve Jessop
那么,你认为在我的例子中,我的运算符确实是右结合的吗? - SergeyS
不,根据你的例子,它们是左关联的。但是你已经为子表达式 @# 的含义做出了选择。该含义不是左关联的必然结果,你本可以做出另一种选择。 - Steve Jessop
请查看我的答案,并告诉我您的想法。 - Rei Miyasaka
1
不,不,不。结合律是关于隐式括号插入的位置,而不是像您所做的那样从源代码中非相邻的部分组装公式的排列。 (在您的最终示例中的@5)。也许预处理器操作可以做到这种事情,但公式的结合律规则不能。如果您认为它们可以,请考虑一下如果@是右结合的,但#是左结合的,则如何处理@#5 - Marc van Leeuwen
@MarcvanLeeuwen,在出现平局的情况下,我们应该始终使用从左到右的原则。无论如何,我不能假装自己在数学术语方面像数学教授那样熟练,所以我相信你的意见。顺便说一句,如果存在像我假设的例子中那样语法的语言,这个运算符的属性会被称为什么? - SergeyS

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接