34
$\begingroup$

I actually drafted most of this question before the relevant stack overflow question but it's still relevant.

C has a famously confusing operator precedence order. It is divided into 15 levels and within the levels there is a clearly defined associativity. Most programmers don't fully remember the exact precedence rules when writing code citation needed, so to aid understandably, style guides usually recommend adding extra parenthesis even when not required by the compiler.

Most languages are the same. Python has a complete precedence table and defines associativity to always be left to right. Rust also has a complex list and also defines the associativity for each operator. APL defines all operators to associate right to left.

In all these languages, the exact rules are complex and people don't really know about them and just use parenthesis both for ease of writing and ease of reading evidence, thanks Michael Homer. It doesn't help that every language uses slightly different rules, further adding to the confusion especially when they are otherwise syntactically similar.

My question is basically, why do languages, including modern ones, bother to explicitly define the associativity for each operator instead of just erroring in cases more complex than PEMDAS. In some cases, the precedence and associativity is obvious but often it's not and I don't understand why languages make an arbitrary decision on those cases.

TLDR

  1. Precedence and associativity rules are complex, hard to implement and hard to use
  2. People use parenthesis anyways
  3. Why even pick some arbitrary order and allow it without parenthesis?
$\endgroup$
19
  • 4
    $\begingroup$ Fortress was a project of a programming language that chose to not have a full precedence table. Some (and there were many) operators could not be combined without parenthesis. $\endgroup$ Commented Jul 7 at 13:09
  • 16
    $\begingroup$ Citation for programmers not remembering precedence levels, and in some cases getting them wrong a majority of the time: Developer Beliefs about Binary Operator Precedence; there are also a lot of similar results about novices learning programming, and reports of real-world problems caused by misapplied operator precedence. $\endgroup$ Commented Jul 8 at 3:59
  • 18
    $\begingroup$ Just because different designers made different choices doesn't make their choices arbitrary. The operator precedence rules of C are famously goofy but that's not because Ritchie made arbitrary choices. He made principled choices that in hindsight we disagree with, because his priorities in the 1970s with dozens of C users are different than ours in the 2020s with millions of C-and-its-descendents users. $\endgroup$ Commented Jul 8 at 6:08
  • 5
    $\begingroup$ I'd also encourage you to think about possible "frame shift" answers. To my mind, the problem is not that precedence is hard to remember. It's that we've made a habit of designing programming languages where punctuation characters are granted special meanings unrelated to their historical usage, and only known to a priesthood of insiders. Why not focus effort on fixing that? $\endgroup$ Commented Jul 8 at 6:14
  • 7
    $\begingroup$ A point I don't think has been mentioned is that you're not just writing code for the compiler. You're also writing it for the programmer coming after you. If parentheses make the code more readable for a human, then they're useful. $\endgroup$ Commented Jul 8 at 14:20

6 Answers 6

33
$\begingroup$

To extend Simon Farnsworth's answer -- which I thoroughly agree with -- let me first push back somewhat on your question, and then give a bit more perspective typical of a mainstream line-of-business language design committee meeting for a major language.

Precedence and associativity rules are complex, hard to implement and hard to use

I'm not really convinced of that. An ordered list is not complex. Precedence is one of the easiest problems to solve when writing a recursive descent parser for a modern language. Millions of people are successful with these languages every day without causing precedence-related bugs. I think you're overstating this.

People use parenthesis [when not required to] anyways

Sure, but that's not actually evidence in support of the proposition that parentheses should be required. People also omit parentheses when they're not required.

Why even pick some arbitrary order and allow it without parenthesis?

One hopes the order is not arbitrary, but rather aligns with what most programmers find "natural". That this is open to debate is not a sign of arbitrariness; just the opposite in fact. It's a sign that there is room for many different perspectives to inform a design decision.

OK, that's my initial pushback on the question itself. If I were pitched this proposal on a language design committee for a new modern line of business language, what would I think about it?

I'd take it seriously as a proposal but would want evidence that the benefits are worth the costs. As Simon Farnsworth's answer points out, the feature isn't free, and the most expensive part is "then output a decent error."

Every time you create an error situation where the user wrote code that they think is perfectly sensible and you've got to tell them why it doesn't meet the compiler's high standards, it's fraught.

Some developers like it because it feels like the compiler is protecting them from having to fix their junior coworkers' dumb mistakes. I kid you not! I have had senior developers say exactly that to me when I was pitching them C# features.

Some feel like the compiler is telling them they're an idiot and it makes them angry. Worse, errors where the compiler knows what the fix is and can precisely tell you what the fix is makes a lot of those same users yell if you know exactly what I meant then why isn't this code already correct?.

This is really a fundamental decision in language design. We designed C# to lean towards "protect the developer from mistakes with rich error messages for ambiguous code", and VB to lean towards "figure out what the developer plausibly meant and do it". It's about understanding the expectations and attitudes of the developer community you're targeting with your design.

So, the main design tradeoff of this feature proposal is: on one hand we have the pain and cost of all the bugs caused by developers' incorrect understanding of precedence leading them to write programs that look plausible but do the wrong thing. On the other hand we have the pain of user dissatisfaction from a compiler that keeps telling them that code they know to be correct isn't clear enough. Which pain do you want? :)

But I've buried the lede. That's not the actual question faced by the design committee.

The actual question is "we have three hours for this meeting, the Distinguished Engineer in the room makes thousands of dollars an hour, and how can we be spending their incredibly valuable time getting the best possible return on that investment?"

Opportunity costs for modern language designers are very, very real. Modern line of business languages that have millions of users are expensive to design, build and maintain, and so we try to spend our time in committee meetings focusing on the biggest possible bang for buck features with the highest possible impact on developers, organizations, and the industry as a whole.

This feature proposal likely wouldn't get a lot of time. It's a "nice to have" at best, and the list of "nice to haves" that are on a committee's back burner is literally longer than your arm.

$\endgroup$
19
  • 3
    $\begingroup$ "Which pain do you want?" I've noticed over the years that people with simultaneously believe that certain ways of writing code are always wrong but are adamantly against the compiler preventing those same things. It doesn't seem very rational. Side note: there's no reason to use the spelling 'lede'. There's very little chance anyone will be confused and think you are talking about Pb. $\endgroup$ Commented Jul 8 at 16:13
  • 3
    $\begingroup$ I noticed this myself when first learning Python many years ago. I was at first repulsed by the idea of syntactically significant indentation but after while, I realized that at some level, it eliminated the need to police indentation. In other words, what value does allowing 'bad' indention provide? In industrial design, there's a principle that it shouldn't be possible to put things together incorrectly i.e. you should not support incorrect configurations. $\endgroup$ Commented Jul 8 at 17:41
  • 4
    $\begingroup$ "But of course you also want to give people sharp power tools if they can use them safely, not just kid scissors." I get what you are saying but I think this is the area where people sometimes fall for a false dichotomy that effectiveness and safety are in opposition. A table saw that retracts its blade when it touches flesh can can be just effective (or better) at cutting wood as any other table saw but it's much less effective at cutting off fingers. But because we generally don't want to cut our fingers off with a table saw, that ineffectiveness isn't a limitation, it's an advantage. $\endgroup$ Commented Jul 8 at 21:12
  • 5
    $\begingroup$ "we have three hours for this meeting". As a language designer myself (XSLT), I find that an extraordinary answer. Yes, getting the design right is incredibly time-consuming (which is why we advance so slowly), but the cost to the user community of getting it wrong is many orders of magnitude greater than the cost of getting it right. $\endgroup$ Commented Jul 9 at 12:01
  • 3
    $\begingroup$ @MasonWheeler: My go-to example of that is when we were designing C# 3 we took a poll about var x = 2, y = 3.5; Should that mean the same as double x = 2, y = 3.5;, that is, infer the best type to substitute for var, or should it mean int x = 2; double y = 3.5; that is, infer the types of both variables independently. 50% of respondents said that the first option was obviously correct, and 50% said the second was obviously correct, so we made it an error. $\endgroup$ Commented Jul 16 at 18:31
21
$\begingroup$

It makes implementation easier.

You would simplify implementation if you had no precedence or associativity rules at all; but once you've implemented handling of PEMDAS, adding more operators to the precedence and associativity rules is simple; it's two more pieces of data to attach to the operator (precedence level, and left/right associative). Indeed, in Haskell, you can define precedence and associativity for user-defined operators.

Erroring, on the other hand, is extra code; you have to detect that this is a case that would be ambiguous (you don't want an error for a << b, even if you do want one for a << b + c), then determine that you don't have a precedence and associativity to choose, then output a decent error.

Linters, which do more work looking for possible errors than compilers or interpreters, often do warn you when it's potentially unclear; for example Rust's Clippy linter has a warn-by-default lint for unclear precedence, and a separate one for unclear bitwise masking that's allow-by-default, while PC-Lint Plus has a variety of lint messages relating to operator precedence, like messages 514, 9050 and 9113.

$\endgroup$
7
  • 10
    $\begingroup$ It's not easier to implement as a lint than as part of the language - it's just that it's common for linters to pick up on more things than the language itself does, and this is one where linters do pick up the slack. $\endgroup$ Commented Jul 7 at 14:47
  • 4
    $\begingroup$ I'm not convinced it actually makes implementation easier in all cases. Perhaps if you've already implemented a complex user-definable-precedence system. But if you're using a parser with a prespecified formal grammar, you simply split your parenthesized-expression term into distinct subcases for each operator. a << b + c is then a automatic parse error because left-shift-term is not supported as an operand in an addition-term expression. Yes, mixed addition/multiplication complicates things, but you avoid having to nest/deconvolute all operators in a similar fashion. $\endgroup$ Commented Jul 7 at 22:33
  • 3
    $\begingroup$ I agree with @r.m. -- I'm skeptical that it adds parsing complexity to allow for cases with no precedence (or analogously no associativity, which I think may even already be an option for Haskell's user-defined operators.) I do grant that it might take some effort on producing good error messages. $\endgroup$ Commented Jul 8 at 5:00
  • 4
    $\begingroup$ @R.M. Once you have a complex enough parser to support PEMDAS, you have backtracking; that means that you need to either specify the parse output for a << b + c, or ensure that both possible parses (yours, plus "addition-term is not supported as an operand in a left-shift expression") are errors, and that neither parse returns a "better" error than the other. It's the error handling that makes this more complex than just defining the one valid parse and leaving it there. $\endgroup$ Commented Jul 8 at 8:07
  • 2
    $\begingroup$ @mousetail Two main reasons for separate lint. (1) A full lint+compile just wouldn't fit in 1970s hardware. Even without that, a C compile ran as about five phases. (2) Lint was generally only useful with a fresh code, or before a final release. $\endgroup$ Commented Jul 8 at 9:41
10
$\begingroup$

I suspect the explanation is historical. Fortran, or FORTRAN as we used to spell it, used operator precedence because users wanted Formulae translated into something the computer could execute. Formulae in textbooks followed the BODMAS, so Fortran adopted that. FORTRAN II, the first version of the language that I encountered, didn't have many precedence rules, and, as I said they followed BODMAS, so it wasn't too onerous.

Other languages added more operators, a few at a time, so the number of operators and levels wasn't immediately as alarming as in your list. Many programming languages in the late 60s early 70s were influenced by Fortran. The designers generally tried to improve on things that were, in their opinion, weaknesses of Fortran, but I don't recall operator precedence being one of them. (I've only dabbled in compiler writing, but I recall that the algorithms for handling operator precedence seemed cool when I learned them).

You mention the absence of operator precedence in APL. I liked APL at the time, but was disturbed by the difficulty of translating formulae from the textbook to the computer.

So the short answer to your question is: most languages have complete operator precedence because most of the previous languages have complete operator precedence.

$\endgroup$
4
  • 3
    $\begingroup$ I remember being confused for about 5 minutes, then thoroughly enjoying the very simple operator precedence rules in Smalltalk. (Simple, as in, there are none.) $\endgroup$ Commented Jul 7 at 22:21
  • 3
    $\begingroup$ @JörgWMittag Seems to me that "There are no precedence rules" is a precedence rule. $\endgroup$ Commented Jul 8 at 9:35
  • $\begingroup$ @Paul_Pedant I'd say it's a rule about precedence rules, but not a precedence rule itself, to avoid the paradox. :P $\endgroup$ Commented Jul 8 at 15:01
  • 1
    $\begingroup$ @Idran Rules and Meta-rules in a much-loved Canadian comedy television series... :-) $\endgroup$ Commented Jul 9 at 7:51
8
$\begingroup$

I don't think most programmers have a problem with precedence levels in their daily use programming language. Sure, there may be dark areas of a programmer language a given programmer is less familiar with, such as bitwise operators. But in general I think most programmers have internalized the intuition of the precedence rules in most cases. After all, the precedence rules are usually intended to be intuitive by their designers.

PEMDAS (3 levels) is for simple arithmetic, but that excludes a lot of operators programmers often use, such as:

  • assignment operators (1 more level)
  • comparison operators (equality, less than, greater than; 1 more level)
  • locical operators (not, and, or; 3 more levels)
  • bitwise operators (not, and, xor, or, left/right shift; 5 more levels).

So any programming language aiming to solve the same problems as C (with 15 levels), probably ought to have about PEMDAS+assignment+logical+comparison+bitwise: 3+1+1+3+5 = 13 levels of precedence.

C has logical and bitwise not at the same level, so it's saving one level from the above discussion, and adds three more levels, but they are for weird stuff:

  • one level is for function calls, array subscription, or member access
  • one level is for the comma operator.
  • and one level is for the ternary operator

Python and Rust has even more levels. APL is just weird.

Beginner programmers might have problems with learning this, but hey, school children also have problems with PEMDAS. You've got to learn your stuff.

It is very hard to reduce the number of levels in this hierarchy, without confusing users of the programming language. Most peoples intuition say that x := 3 + 5 should mean that you assign to x the result of adding 3 and 5; not that you assign 3 to x and then add 5 to whatever value this assignment returns. Such a language would probably not be very popular, at least not for that reason (unless it got something else right)

The are only two sane alternatives to a huge precedence level hierarchy:

  • put parentheses around everything. (But last time I checked most programmers still dislike Lisp (I don't)).
  • limit functionality (also not very popular)
$\endgroup$
4
  • 2
    $\begingroup$ TBH, it seems kinda hard for there to be ambiguity on the precedence between unary prefix operators like logical and binary "not". If you come up with a reason to combine them, you have either !~a or ~!a and both seem they should be unambiguous. $\endgroup$ Commented Jul 8 at 7:51
  • $\begingroup$ The designers of C certainly agree! $\endgroup$ Commented Jul 8 at 7:54
  • 3
    $\begingroup$ Regarding only two sane alternatives, I see a third possible option. We could group the C operators into arithmetic (+ - * /), bitwise (& ^ |), logical boolean (|| &&) and various other operators. Then the language could let us mix operators within a group without parentheses but not when you use operators from multiple groups. So we could write 1 + 2 * 3 but would have to write (1 + 2) | 3. (The programmer still has to learn the precedence within a group but that is much easier. The problems usually come from mixing groups.) $\endgroup$ Commented Jul 20 at 13:09
  • $\begingroup$ I agree. That's a reasonable (and somewhat novel) alternative. $\endgroup$ Commented Jul 21 at 15:20
1
$\begingroup$

TL;DR: both options have different benefits wrt. expressiveness.

EDIT: the answer is mostly about the language-defined vs. user-defined precedence part. The why define it at all part is essentially usability and defined semantics and, hence, not covered at length.

Actually, it makes language definition easier. Implementation is more code and often also slightly more complex assuming that languages have a common set of operators similar to Java, C, C++, etc. My experience here is that language-defined operators are about 4x as much code even if user-defined operators have special rules for assignments and type checks.

The true downside of allowing users to define operators in the language is that it requires counterparts for everything that a language designer wants to express. However, the common approach for user-defined operators is to have them as syntactic sugar for function calls. So, one cannot define &&, || or ?: (guess why ?: cant't be overloaded in C++).

The reason why Tyr can express them (see) is that Tyr pioneers the idea of passing fully compiled Blocks as arguments into another function. Thus, a call of the user defined && in Tyr results in the same code/CFG as in Java. Wrapping everything in closures really isn't an alternative here because that would be a disaster wrt. performance.

Another option to overcome this issue is to just not define such operators in the code and add special handling for them in the compiler, e.g. Scala. However, with that approach, I would assume that implementation becomes about as complex for both options. Note that such a hook or a dedicated IR node is required in the backend of the compiler anyway to use correct instructions for, e.g. integer operators, but that aspect is transparent to the comparison here because language-defined operators also need it.

Something that is much easier to express with language-defined operators is ordering and name binding, if terms can introduce variables that can be used by other terms in the same operator tree. For instance in Java you can write:

 int x = 7; Object ref = x; if((ref instanceof Integer r) && 7 == r) System.out.println("ok"); 

The point with this example is that name binding of r in the third line happens after evaluating the left operand of &&. With user-defined operators, name binding would usually happen before looking into at operators. Thus, r would bind in the context of the if node and might bind to an embracing field or something similar. Especially in this example, I do not know and also cannot imagine an algorithm that would allow r to bind to the definition on the left and have a semantics that feels natural in general.

$\endgroup$
-2
$\begingroup$

Swift has a generic syntax for character sequences that could be operators. For example +-+-+ could be an operator. On the lexical level it is decided if an operator is unary or binary operator. +x is unary, x+y or x + y are binary. And every operator has a precedence and an associativity, set in source code or by the standard library. For example, the FORTRAN ** (power) operator is left, not right associative.

The parser scans an expression as a sequence of operands separated by operators. Then it combines operators with the same higher priority and same associativity. If two operators have equal highest priority but different associativity then pardon fails.

$\endgroup$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.