Operator Precedence Considered Harmful

Almost every ProgrammingLanguage supporting InfixNotation for expressions has extensive OperatorPrecedence levels. This outgrowth of the mathematical origins of programming languages has been the cause of countless bugs. The problem with operator precedence is that it's yet another thing a programmer has to memorize about the language he's using. And since the precedence levels for each operator vary from one language to another, it's easy to see why this can cause so much problems. Let's look at the precedence tables of two well-known languages:

CeeLanguage (from highest to lowest):

PascalLanguage (from highest to lowest):

Now, this comparison isn't entirely fair because C has many more operators than Pascal. Still, anyone coming from a Pascal background (now admittedly quite rare) is going to have trouble with eg. C's bitshift operators.

I'd like to argue for the abolishment of extensive operator precedence tables. Of course, some precedence is needed, but it should be limited to what SmalltalkLanguage provides (highest to lowest):

Really, that's all that is needed. All other ambiguity can be solved by using left-to-right associativity. Thus, "1+1*2" would be parsed as "(1+1)*2", and "1<<2+3" as "(1<<2)+3". Anyone can unambiguously parse that; just forget about precedence.

-- WouterCoene

"Of course, some precedence is needed, ..." No, not really. For example, LispLanguage gets along just fine without it. Well, OK, I suppose you could consider some macro characters to be unary operators with high precedence. :) -- DanMuller I'm obviously assuming a non-HomoiconicLanguage here :) -- WouterCoene

Just because an equation can be unambiguously parsed does not mean that it will be. Be kind to the people reading your code and include those unnecessary parenthesis. This also frees you from writing in an unusual order to maintain left to right sequencing. -- WayneMack

How does one express the calculation of a percentage (or anything requiring operations in the denominator) with only left to right sequencing? For example, A/(A+B+C) * 100 is not equivalent to (A/A) + B + C * 100.

"How does one express the calculation of a percentage (or anything requiring operations in the denominator) with only left to right sequencing?". Using parentheses of course. -- WouterCoene

... percentage ... operations in the denominator ... with only left to right sequencing


PerlSix is going to be even more of a precedence hell; see http://www.ozonehouse.com/mark/blog/code/PeriodicTable.html.


OperatorPrecedence is a good thing, as can be seen in mathematics. The problem is not its existence in programming, but its non-local use (as opposed to mathematics, where most texts state the rules in a preliminaries section). If precedence where defined and used locally it would pose no problem, because the programmer would have to explicitly import some operators and their precedences explicitly and otherwise have neither of them available. Think about C: I'd take shift and logical operations out of the core language, leaving only standard arithmetics with the usual prefix higher than */ higher than +- (which really is generally known). If you want to use shift operations, go ahead and import them, maybe with specifying their relative precedence to other operations. All precedences that are not explicitly given must be explicitly bracketed. Such a scheme would give the best of both worlds. Sadly, no language really supports such a thing. -- GunnarZarncke

So if I have an expression "1<<2+3" in source file A and I copy it to source file B it might mean something entirely different there? Please, ProgrammingIsNotMath?. For one, there's usually several orders of magnitude more program code in even simple programs than there's mathematics in a decently-sized maths paper. -- WouterCoene

The idea that programming code should grant a special exemption for math is just ridiculous. -- AnonymousDonor

there's usually several orders of magnitude more program code in even simple programs than there's mathematics in a decently-sized maths paper. Yes, but there's usually several orders of magnitude more math in the math paper. The largest fraction of programming is not concerned with math - and I have been speaking of the math part, not about conditions, loops and method calls. As for copying "1<<2+3" somewhere else: You should better copy it only where it has the same meaning, i.e. where it applies to arguments of the same type etc., otherwise all bets are off anyway. -- .gz

The same goes for existing code that must be maintained then. All bets are off regarding the precedence preferences of a particular programmer, so for each new set of source files, a maintenance programmer must again figure out what the precedence rules are. The problem with program code is that for even simple programs there's so much of it. It's really easy to get lost, especially for people not familiar with the program in question. -- WouterCoene

Isn't this really just a further extension of mathematical libraries at this point Gunnar? Are we needing to only provide the base operators in main programming and then in a MathClass?, define those additional operators (and thereby their precedence) for just the library. That route I would agree with. Then the basics would be just () + - * / and assignment. -- WyattMatthews

Yes. The basics would be just that and everything else part of the library. I have no problem with operator precedences between && and || or between << and & - provided that they work on separate basetypes. It would be better anyway to have a separate bit-vector type (as e.g. VhdlLanguage has it) on which binary operations like shifting/masking work on and which requires explicit conversion to int/short. Shifting for arithmetic is a no-no anyway. Compilers are sufficiently smart since a long time to optimize multiplication and exponentiation into shifts where it is safe. -- .gz

I vote we settle the issue this way, we define the language entirely in terms of parse trees and create tools that can unparse them into whatever source form the user wishes. So that way, idiots can define whatever baroque Rube Golbergian system they wish without inflicting it upon innocent victims. Using parse trees directly has other benefits and only minor disadvantages which can be overcome. -- rk

This must be how LispLanguage's EssExpressions-based syntax (or lack of) came to being. :) -- WouterCoene

Yes, that was probably one motivation, though a bigger one was minimizing the syntax to make it easier for the language to be homoiconic. But that's assuming Lisps don't simply define the language in terms of parse trees, which may be true technically but I'm not sure is meaningful in practice.

One of the advantages of parse trees, the ability to stick a live object in the code without needing any kind of literal representation, is not present in any ... well, any language actually, that I know of. When a parse tree containing a live object got unparsed, the object wouldn't be unparsed but merely be represented non-literally. -- rk

(Technically we're mostly talking about abstract syntax trees, but the difference from parse trees/concrete syntax trees doesn't tend to matter in this kind of discussion. Tersely, given an expression like "(((1)))", a CST faithfully reflects every little detail of the parse, while an AST skips the irrelevant, reducing the example to simply "1".)

An advantage of Lisp S-Expressions is that they are the same thing as Lisp parse trees, at least to a good first approximation, whereas most other languages tend to need more attributes per node and therefore to find it natural to use more complex structures per node of their parse trees. (Which is not to say that S-Expressions can't represent parse trees for arbitrary languages, merely that they may get hairy rather than remaining simple.)

I'm not sure what you mean by "without needing any kind of literal representation", since every actual abstraction needs an actual literal representation in the real world...

...but this simplicity of Lisp means that it is well-suited to building parse trees.

My most recent Lisp implementation has a little syntactic trick to allow infix notation, since I like it for simple 3-level operator precedence, and this syntactic sugar translates completely trivially into simple prefix S-expressions. It didn't change the nature of Lisp at all to do this; it was pure and simple SyntacticSugar. -- DougMerritt

What's the literal representation of the Object metaclass? If you think about it, you'll realize there isn't one. Just like there isn't any kind of literal representation of the first TCP connection that's open right now. These things are unique and well-defined, but there is no syntax that specifies them in any way, shape or form. IOW, they have no literal representation.

The problem with using indirection to access objects without literal representations should be clear. By the time your code is evaluated, the object may have been dereferenced & garbage collected or otherwise destroyed. It's the same problem with symbolic links and any other type of indirection, you get dangling pointers.

Currently, the only way to store a live object in code is to assign it a global variable. This is wrong on so many different levels including scoping, naming conflicts and the fact that it's a two-step process. It shouldn't be necessary to go through this rigmarole to do something perfectly sensible like sticking an arbitrary handcrafted object into code. Note that recreating the object, even programmatically, is a non-solution that merely works around the problem in a particularly annoying way (by forcing the user to meta).

I have here a perfectly working CD player which I made step by step using my own two hands, including smelting the iron that's in it and pumping out the petrol that went into the case. Now I want to test these mass-produced laser diodes off an assembly line with my perfectly working CD player. I don't want to create more CD players, I want to use the one that's RIGHT HERE that I made myself. In the physical world, you can certainly do this. Why can't you do it in software? -- RK

Taking one issue at a time: I'm not sure that we are both using the term "literal representation" in the same way.

Consider that Smalltalk and Smalltalk-influenced languages always provide a way to take any Object and turn it into a stream representation and/or archive it and/or pass a representation of it to some remote system, a.k.a. "pickle"/"shelve"/"archive" in certain languages (I can't remember what Smalltalk and Objective-C call this, but you know what I mean).

The representation that is thus used to store and restore objects is, in my terminology, a "literal representation", which has a syntax (albeit usually a very simple one).

How is that the same, or how is it different, than what you mean by related terminology? -- Doug

It's not a literal representation of the object because it's not the same object. It's a way of creating a duplicate, that's all. There is nothing in Smalltalk or any derived language that allows you to literally store and unstore an object. If Smalltalk provided a way to store an object that happened to be in an instance variable then unstoring it would restore the object in the instance variable in the context it was stored from. You would need to store a reference to the context the object is in.

If you store a person in a prison or in a cryostasis cell, their relationships to other things in the world don't magically disappear. And when you unstore them, the relationships that were suppressed during the storage period are restored along with the person. In contrast, if you "store" a Smalltalk object, it is cut off from everything in the world. And if it isn't so cut off then "unstoring it" will create a clone with no connections to anything in the world. The identity of an object includes its relationships with everything around it.

The word "store" is ambiguous and counter-productive. We need to distinguish between cloning, imprisoning, transplanting, transporting, and excising. Transporting is what a transparent distribution package like OpenTalk? does. Cloning is what "storing" does. Imprisoning would block access to an object (implementation: create a placeholder and have it swap places with the object). Excising (surgically removing) is similar except the placeholder oneWayBecomes nil after being swapped for the object. Transplanting surgically removes an object from its milieu and reconnects it to homologous objects in a new milieu.

I've been dealing with the problem of identity of objects for a long time now so I'll give you a taste of it. Consider that even in the case of joke operating systems like Windows and Unix, copying an object and linking to it are not the same thing. It gets even worse if you add versioning because then copying an object requires read access to all its past versions. And even then you still wouldn't be able to reproduce it because you can't forge timestamps in a serious OS. And after you've reproduced the object, what does that give you if nobody else has access to it? In a serious OS, you should be able to go into the past and fork an object from a past version with everyone having equal access to both branches.

Addressing a completely different issue, it would be somewhat okay for Smalltalk to require objects to be named before they can be accessed. The only problem with that is that Smalltalk does not in fact require naming of objects before they are used. When you evaluate a method in a workspace, you use this method without ever having named it. So we see that Smalltalk is actually inconsistent in this matter. -- RK

Actually, that method is run as the method named doIt in UndefinedObject, so it's not exactly unnamed.

Semantics.

Despite my attempt to take it one topic at a time, your racing mind inserted a slew of topics, so for the moment let me just say that part of what you are saying sounds like what Phil-the-Botany-prof said in his 1999 paper cited at page bottom of AboutCells.

The question of "identity" is, FYI, a large one that has been considered for centuries, philosophically; you're not the first to think hard about it! .... and attacked quite seriously in terms of many computer areas as well, so basically it is too large a topic to say anything about as long as you're zipping around and touching on dozens of subjects. Whereas if we could take just one thing at a time... -- DougMerritt


"OperatorPrecedence is a good thing, as can be seen in mathematics".

SingleLetterVariableNames? are a good thing, as can be seen in mathematics.

Consider the PrincipleOfLeastSurprise - if a programmer comes to a programming language after learning mathematics, s/he will expect infix operators to follow the same precedence as they do in math. Having 2+3x4 return 20 will be rude and evil. This does not apply to a language that does math with prefix or postfix operators, since they are not represented in the same way as the infix operators are. -- PeteHardie

Partly this is a problem with maths teaching. Operator precedence is simply a notational convention, not a mathematical axiom, and should be taught that way. In the context of a programming language, operator precedence causes serious difficulties: it is very hard to cleanly add new operators, for example. Look at the mess in Haskell or Prolog, where you have to give each operator a magic precedence number.

Mixing concepts from maths with those from programming causes all sorts of confusion. For example, the never-ending discussions over whether a circle is an ellipse in object oriented languages comes down to trying to apply simple maths where they don't apply.

When using a programming language you are not working in mathematical notation so you should expect some differences.

I agree that the conventional mathematics precedence is just that, a convention (see the commutative property, however). However it is a universal one, and violating that in a context that is almost identical is a BadThing. When one is performing a mathematical calculation in any programming language using infix notation, one expects the result of 2 + 3 x 4 to match that of the mathematical expression written the same way.

The fact that it may make adding operators more difficult is orthogonal - it is the responsibility of the developer (language developer in this case) to make things easy for the user (programmer in this case), not for the user to conform to non-standard usage simply to make things easier for the developer. If language developers want to avoid oddities of operator precedence, they have the simple option of using a pre/postfix notation, which removes the source of confusion.

As for the circle/ellipse problem, that's more an issue of presentation. Most people are taught the definition of a circle first, so they favor that definition, which is simpler to model, to the more general one of a circle as a special case of ellipse. IMNSHO, Occam's Razor would favor defining only ellipses as graphic objects, with a special mode to "lock" foci together for circles.

-- PeteHardie


See IssuesForLanguageDesigners ConsideredHarmful

OctoberZeroFive

somewhat CategoryMath


EditText of this page (last edited March 22, 2006) or FindPage with title or text search