On operator precedences and missed opportunities

this is a bit of a historic investigation i want to share with you. not a pitch, or anything, i do not expect any changes to swift language in that area. this is an observation of what had happened and what could have happened.

have a look at SE-0077 Improved operator declarations.

there were three motivations listed there:

1. Problems with numeric definition of precedence
2. Problems with a single precedence hierarchy
3. Problems with current operator declaration syntax

let me start from the last one: "Current operator declaration syntax is basically an unstructured bag of words”:

infix operator <> { precedence 100 associativity left }

i view this bullet point as an “add-on” to the main topic of that proposal, for example we could have addressed just that bit without changing anything else:

infix operator <> { precedence: 100, associativity: left }

so let’s forget (3) and move to the next one: (2)

it turns out that at the end of the day swift didn’t change that aspect of the operator precedence.. we still have a single precedence hierarchy for built-in operators (see the operator table in operator_declarations of the language definition or command click on the “import Swift” too see for yourself).

Looking at the usage in custom operators — (my tool for this recently is an advanced search on github) — the owerhelming majority of those custom operators do not create custom precedence group at all, referencding the existing one. and the overhelming majority of those that do, base their precedence (using higherThan) in relation to one of the system precedence groups, thus putting their precedence group firmly inside the single system hierarchy of precedences.

given that observation my assumption here (welcome for your feedback if you are on the contrary) is this: we won’t lose too much if (2) was not there.

having said that, let us forget (2) and switch to the “main” one (1):

the reasoning here is clear and understandable. but let's consider this alternative: “use floating point numbers for precedence”. one can put an infinite number of real numbers between two different real numbers and even if we regard the 32/64 bit IEEE limitations the amount of numbers we can put between, say, 0.001 and 0.002 is wast and well beyond our needs to express precedence levels.

as i can see in the proposal — this alternative was not considered (*). i saw similar trend in other proposals as well - typically the authors are so focused on their preferred solution that they are not seriously considering anything else. (hint for the future language designers: do something about it).

(*) using Int32/64 and selecting the min/max precedence to 0 ... Int.max along with adjusting the existing levels, e.g. not "90" but "90_000_000" is not listed as considered either, even Int32 would be well more than enough and if not - there is Int64.

should we considered and then accepted the idea of floating / int32/64 precedence we would have a different language today in that area, smth like that:

enum Precedence: Float {
minimal = 0.0
bitwiseShift = 90.0
multiplication = 100.0
addition = 110.0
rangeFormation = 120.0
casting = 130.0
nilCoalescing = 140.0
comparison = 150.0
logicalConjunction = 160.0
logicalDisjunction = 170.0
ternary = 180.0
assignment = 190.0
maximum = 1000.0

init(…) { … }

}

infix operator <> { precedence: .comparison, associativity: left }

// custom:
extension Precedece {
static let wtf = Precedence(123.456)
}

infix operator @$#% { precedence: .wtf, associativity: left }

as i said, this is not a pitch. rather a historic observation of current affairs and missed opportunities. presented by a grumpy fella :-)

3 Likes

This is completely non-ergonomic. Developers would have to construct a floating-point literal that did not end up rounded to the same value of the literal used for some other operator. And there'd be no way to be certain, except by trial and error.

If you were going to introduce such a breaking change, it would make more sense to simply inflate the current values to dramatically increase the range between them. Integers are simple to reason about, and, if they are in range, literals are never modified upon conversion to a numeric. An integer also gives the full range of discrete values available in the binary representation, as no bit patterns are reserved for NaN or Inf.

4 Likes

yep:

Exponentiation precedence was renamed to bit shift precedence, the implication being that any future addition of exponentiation operators will involve bifurcating the hierarchy.

In general, we do not revisit what could have been for settled proposals without evidence of active harm. Swift has enough other areas that require attention.

2 Likes

Those who cannot remember the past are condemned to repeat it.
- George Santayana

You're misunderstanding what this does. Precedence groups are partially-ordered, not totally-ordered. Declaring A as higher than B makes A transitively higher than anything that B is itself higher than, but it doesn't order A with anything else that is higher than B. Precedence groups form a graph, not a continuum.

Custom operators often link themselves into the system hierarchy so that they're higher than operators like =, ??, and ==, but they often intentionally do not associate with the arithmetic operators, which is something that is expressible with Swift's system but not with a fractional numeric precedence score.

11 Likes

Echoing what John said, if we had this system from day 1, we probably would have been more aggressive about having operators that are unordered w.r.t. each other--for instance, it would make good sense to have bitwise operators have incomparable precedence vs. arithmetic operators on integers. It's a source-breaking change now, but we can at least avoid introducing more cases where operators have ordered precedence and we'd prefer that they not.

6 Likes

thank you both, i understand the reasoning now.

so, if we did it earlier, because it is not immediately obvious
whether: a + b & c
means: a+b & c
or: a + b&c

it is better to leave it intentionally undefined and force users to always put parens. without favouring one way or another (despite the fact that choosing such a default would allow avoiding parens in "half" of the cases).

it increases the number of () and makes the language a bit more complex than the old one (graph vs list) but if people find it useful - good.

1 Like

i take this back. found the proper doc one level below compared to where i was previously looking.

it also says swift precedence rules are simpler than those in C, which to me was not the case (graph vs list), but that's totally minor.

Which one?

In the sense that Swift has fewer precedence levels than C has (11 vs 15, IIRC), this is objectively true. But, there's certainly more than one way to interpret "simple".

1 Like