Kotlin Operator Precedence - Infix Functions

Jul 11th, 2022

16 min read

kotlinjavapythonc++

Last modified Aug 8th, 2022: C++ included as a comparison language.

There are a multitude of language-specific tutorials focussing on the order of operations used to evaluate mathematical expressions. Not to mention the array of mnemonics that are taught as a foundation in early school years (I was raised using BEDMAS).

This post won't dwell too long on the basics that have been covered time and again by other blogs. Instead, it will highlight a feature of Kotlin, infix functions, and where they rank in relation to other operators.

The Basics

Let's run through a few examples to make sure that we're on the same page.

Expand the Answers section when you're ready for more details.

1import kotlin.math.pow
2
3val ans1 = 10 + 2 * 3 / 2 - 3
4// Will ans1 = 10, 15, or 5?
5
6val ans2 = 100 / (10 + 15) - -4 * -1
7// Will ans2 = 0, -8, or 4?
8
9val ans3 = -(-2.0 * (-3.0).pow(2)).pow(2)
10// Will ans3 = -324.0, 324.0, or 1296.0?
11
12val ans4 = !(true || false) && !true || !false && true
13// Will ans4 = true or false?
14
15val ans5 = 4 * 3 > 5 * 2 || !(10 % 2 != 1 || 2 * 2 == 4) && !!false
16// Will ans5 = true or false?
kt
Answers
1val ans1 = 10 + 2 * 3 / 2 - 3
2// Equivalent to: 10 + (2 * 3 / 2) - 3
3println("10 + 2 * 3 / 2 - 3 = $ans1")
kt
10 + 2 * 3 / 2 - 3 = 10

Multiplicative operators have higher precedence than additive ones, so 2 * 3 / 2 is performed first. We move left to right to evaluate (2 * 3) / 2, but, due to the associative property, it doesn't matter if we choose to perform 2 * (3 / 2) instead. The same applies for the resulting (10 + 3) - 3 or 10 + (3 - 3).

1val ans2 = 100 / (10 + 15) - -4 * -1
2// Equivalent to: (100 / (10 + 15)) - (-4 * -1)
3println("100 / (10 + 15) - -4 * -1 = $ans2")
kt
100 / (10 + 15) - -4 * -1 = 0

Parentheses override convention so (10 + 15) is evaluated first. Unary operators are considered prefix operators, which have a higher precedence than arithmetic operators, so the negative integers are established before 100 / 25 and -4 * -1 are evaluated. 4 - 4 is performed last.

1val ans3 = -(-2.0 * (-3.0).pow(2)).pow(2)
2println("-(-2.0 * (-3.0).pow(2)).pow(2) = $ans3")
kt
-(-2.0 * (-3.0).pow(2)).pow(2) = -324.0

Kotlin does not have an exponent operator like Python, but pow() still has high priority because Kotlin considers any function dot notation to be a postfix operator and, therefore, of the highest precedence. So the pow() in parentheses is called first using the established negative double, then (-2.0 * 9.0) is evaluated. From the resulting -(-18.0).pow(2), exponentiation is executed before the unary negation outside the parentheses because postfix operators rank above prefix operators. To clarify, . ranks higher than unary - so the two negatives do not cancel before pow() is called on the value in the parentheses.

1val ans4 = !(true || false) && !true || !false && true
2println("!(true || false) && !true || !false && true = $ans4")
kt
!(true || false) && !true || !false && true = true

Boolean NOT ! is considered a prefix operator, which has high precedence (even higher than arithmetic operators). The other boolean operators rank much lower with conjunction && having higher precedence than disjunction ||. Parentheses break convention to evaluate (true || false) then all the boolean NOT operators are evaluated resulting in false && false || true && true. Conjunction takes next precedence resulting in false || true being evaluated last.

1val ans5 = 4 * 3 > 5 * 2 || !(10 % 2 != 1 || 2 * 2 == 4) && !!false
2println("4 * 3 > 5 * 2 || !(10 % 2 != 1 || 2 * 2 == 4) && !!false = $ans5")
kt
4 * 3 > 5 * 2 || !(10 % 2 != 1 || 2 * 2 == 4) && !!false = true

Evaluating the expression in the parentheses first, arithmetic operators have higher precedence than equality operators, so the expression becomes (0 != 1 || 4 == 4). Equality operators have higher precedence than the disjunction operator, so this leaves (true || true) to be evaluated. All the boolean NOT operators are evaluated to result in 4 * 3 > 5 * 2 || false && false. Note that multiple ! together are evaluated from right to left so !!false == !(!false).

Arithmetic operators take precedence over comparison operators so the expression becomes 12 > 10 || false && false. Comparison operators rank above both && and ||, resulting in true || false && false, which is evaluated as true || (false && false) due to conjunction having a higher precedence than disjunction.

Infix Functions

Infix functions rank around the middle of Kotlin's order of operations, lower than arithmetic operators and higher than boolean operators.

Even if you can't immediately think of examples of these types of functions, chances are you've already been using them and even naturally adhering to the order of operations.

For example, every time you create a tuple of type Pair using to, you are using an infix function (specifically the infix notation for to()):

1val pair1 = 7 + 3 to 4 * 3
2println(pair1) // (10, 12)
3
4val pair2 = "Teens" to 11..29 - 10
5println(pair2) // (Teens, 11..19)
kt

Line 1: As stated previously, arithmetic operators have higher precedence and are evaluated first, resulting in 10 to 12.

Line 4: .. is the symbolic representation of the operator function rangeTo(), which ranks higher than infix functions but lower than arithmetic operators. We could use parentheses or remove whitespace to either reduce ambiguity or increase code readability, but this line would not cause a compile error. 29 - 10 is evaluated first, then the range 11..19 is created before it is used to initialize a new Pair as the second component.

Infix functions have higher precedence than named checks, comparison, and equality operators. If the component of a Pair needs to be the result of an expression with these operators, parentheses will be necessary to avoid getting a bunch of red squiggly lines in your IDE.

1/* none of these will work
2val pair1 = "A" to "a" in "aeiou"
3val pair2 = 100 > 10 to 1
4val pair3 = "Case" to "text".lowercase() == "text"
5*/
6
7// but these will
8val pair1 = "A" to ("a" in "aeiou")
9val pair2 = (100 > 10) to 1
10val pair3 = "Case" to ("text".lowercase() == "text")
11
12// this statement works too
13val bool = 1 to 2 in listOf(1 to 1, 1 to 2, 1 to 3)
kt

Line 8: in is the symbolic representation of the operator function contains(), which ranks below infix functions. The same operator precedence applies for other named checks !in is !is.

until() is another common infix function:

1val isSpecial = 19 in 90 / 9 until 99 * 9 / 19
2println(isSpecial) // true
kt

Line 1: The arithmetic expressions to the left and right of until are evaluated first so that an IntRange can be initialized before it is searched for the value 19.

Let's see one more example, using zip(), before we get into the fun stuff:

1val set1 = setOf(1, 2, 3, 4, 5)
2val set2 = setOf(1, 2, 3)
3val set3 = setOf(2, 5)
4
5val messy = 4 to 'd' in 1..5 zip 'a'..'e' && 2 to 3 in set2 zip set1 - set3
6println(messy) // true
kt

Line 5: The sole additive expression set1 - set3 is evaluated first, then both range operations, followed by all the infix functions from left to right (two Pair initializations and two zip operations). Both named checks in are then evaluated, with the conjunction && being evaluated last.

Other infix functions exist (for example, downTo, matches and Set functions intersect subtract union), but we're going to move on to a special subset that I've avoided so far, the bitwise infix functions.

Bitwise Operators

If you're coming from a background in Java, you'll know that the bitwise AND, XOR, and OR operators are on the lower end of Java's order of operations, only having a higher precedence than boolean and assignment operators.

This is different from Kotlin, which does not have bitwise operators and instead relies on infix functions to perform bitwise operations. This means that, unlike Java, Kotlin's bitwise functions and() xor() or() have higher precedence than comparison and equality operators. Python has a similar operator precedence to Kotlin where it concerns bitwise operators, whereas C++ is more similar to Java.

1val a = 10 and 1 > 2 or 3
2println(a) // false
kt
1public static void main(String args[]) {
2 // causes compile error: bad operand types for binary operator '&'
3 // boolean a = 10 & 1 > 2 | 3;
4
5 // this works
6 boolean a = (10 & 1) > (2 | 3);
7 System.out.println(a); // false
8}
java
1a = 10 & 1 > 2 | 3
2print(a) # False
py
1#include <iostream>
2
3int main()
4{
5 bool a = 10 & 1 > 2 | 3;
6 std::cout << std::boolalpha << a << std::endl; // true
7 a = (10 & 1) > (2 | 3);
8 std::cout << a << std::endl; // false
9
10 return 0;
11}
cpp

The same expression in Java does not compile because 1 > 2 is evaluated first, then 10 & false is attempted. In C++, the same expression does compile but evaluates in the same order as Java to produce an unwanted result (remember that C++ Boolean values evaluate to integers). Parentheses have to be used in both Java and C++ to get the same result that Kotlin and Python produce.

Another important difference is precedence among these three bitwise operators. Java clearly ranks them from highest to lowest as & ^ |. Kotlin documentation just groups all infix functions together and playing around with expressions shows that operations of the same precedence are evaluated from left to right. In this matter, Python and C++ are more similar to Java than Kotlin by also having a clear distinction among the three.

1val a = 8 or 1 and 4
2println(a) // 0
3val b = (8 or 1) and 4
4println(b) // 0
5val c = 8 or (1 and 4)
6println(c) // 8
kt
1public static void main(String args[]) {
2 int a = 8 | 1 & 4;
3 System.out.println(a); // 8
4 int b = (8 | 1) & 4;
5 System.out.println(b); // 0
6 int c = 8 | (1 & 4);
7 System.out.println(c); // 8
8}
java
1a = 8 | 1 & 4
2print(a) # 8
3b = (8 | 1) & 4
4print(b) # 0
5c = 8 | (1 & 4)
6print(c) # 8
py
1#include <iostream>
2
3int main()
4{
5 int a = 8 | 1 & 4;
6 std::cout << a << std::endl; // 8
7 int b = (8 | 1) & 4;
8 std::cout << b << std::endl; // 0
9 int c = 8 | (1 & 4);
10 std::cout << c << std::endl; // 8
11
12 return 0;
13}
cpp

In Kotlin, bitwise shift operations also rely on infix functions that follow the same left to right precedence when up against other bitwise functions. This is, once again, different from Java, which ranks its bitwise shift operators at a higher precedence than its other bitwise operators, and even higher than relational and equality operators.

1val a = 9 or 1 shl 2 and 12
2println(a) // 4
kt
1public static void main(String args[]) {
2 int a = 9 | 1 << 2 & 12;
3 System.out.println(a); // 13
4}
java
1a = 9 | 1 << 2 & 12
2print(a) # 13
py
1#include <iostream>
2
3int main()
4{
5 int a = 9 | 1 << 2 & 12;
6 std::cout << a << std::endl; // 13
7
8 return 0;
9}
cpp

The Java example produces a different result because 1 << 2 is evaluated first, then 4 & 12, with the resulting bitwise OR expression being evaluated last. Python and C++ again produce the same result as Java, whereas Kotlin evaluates the bitwise expressions in the order they are written from left to right.

One commonality among the three languages is that bitwise NOT does have higher precedence than all other bitwise operators. This is because Java, Python, and C++ consider ~ to be a high precedence unary operator and Kotlin's corresponding inv() requires dot notation to be invoked, which is a postfix operator (the highest precedence).

1val a = 15 xor 11.inv()
2println(a) // -5
kt
1public static void main(String args[]) {
2 int a = 15 ^ ~11;
3 System.out.println(a); // -5
4}
java
1a = 15 ^ ~11
2print(a) # -5
py
1#include <iostream>
2
3int main()
4{
5 int a = 15 ^ ~11;
6 std::cout << a << std::endl; // -5
7
8 return 0;
9}
cpp

So we've covered how infix functions rank against other common operators, as well as some key language comparisons, but how are they evaluated against other infix functions?

Infix vs Infix

As mentioned in the previous section, when a Kotlin expression includes multiple infix functions, the rule that governs their order of operations is simply evaluation from left to right. This means that parentheses will most likely be necessary to avoid errors when structuring code that primarily uses infix expressions.

1val x = 0b1000 to (1 shl 3)
2println(x) // (8, 8)
3
4val y = 0 or 1 until (25 xor 19) step (1 shl 1)
5println(y) // 1..9 step 2
6
7val z = 1 shl 6 downTo 0 step (1 shl 4) zip (9 and 4 until (128 shr 1) + 1 step 16)
8println(z)
9// [(64, 0), (48, 16), (32, 32), (16, 48), (0, 64)]
kt

For example, in line 1, the expression could not be written as 0b1000 to 1 shl 3 since 0b1000 to 1 would be evaluated first. This would result in (8, 1) shl 3 causing a receiver type mismatch compile error.

If you recall a very messy expression that I previously used as an example in the first section, you'll be able to see how this left to right evaluation may cause logic errors if the rules are forgotten. In this example we replace the higher precedence arithmetic operator - with a lower precedence infix function subtract(), which changes the result if parentheses are not used.

1val set1 = setOf(1, 2, 3, 4, 5)
2val set2 = setOf(1, 2, 3)
3val set3 = setOf(2, 5)
4
5val messyOG = 4 to 'd' in 1..5 zip 'a'..'e' && 2 to 3 in set2 zip set1 - set3
6println(messyOG) // true
7
8val messy = 4 to 'd' in 1..5 zip 'a'..'e' && 2 to 3 in set2 zip set1 subtract set3
9println(messy) // false
10
11val messyFixed = 4 to 'd' in 1..5 zip 'a'..'e' && 2 to 3 in set2 zip (set1 subtract set3)
12println(messyFixed) // true
kt

In the second variable messy, set2 zip set1 is evaluated first resulting in [(1, 1), (2, 2), (3, 3)]. Since none of these elements are present in set3, subtracting the latter from the former results in a set of elements identical to the untouched list, which does not contain the pair (2, 3), thereby changing the final result. This unwanted result is avoided if parentheses are used appropriately, as in messyFixed.

Summary

The following table summarizes the order of operations in Kotlin, from highest to lowest precedence, with comparison columns for Java, Python, and C++:

KOTLINJAVAPYTHONC++
Postfix ++ -- .⬇️✔️1❌⬇️✔️1,2⬇️✔️1
Prefix - + ++ -- !✔️❌⬇️✔️2,3✔️
Multiplicative * / %✔️✔️✔️
Additive + -✔️✔️✔️
Range ..
Infix functions❌⬇️✔️4❌️✔️5❌⬇️✔️4
Named checks in !in✔️6
Comparison < > <= >=✔️✔️6✔️
Equality == !=✔️✔️6✔️
Conjunction &&✔️✔️✔️
Disjunction ||✔️✔️✔️
Assignment = += -= *= /= %=✔️✔️✔️
1 Kotlin's bitwise NOT operator is inv() so it ranks at the highest level as a dot notation call. The bitwise NOT operator ~ in Java, Python, and C++ ranks just below at the prefix/unary level.
2 Python has no increment or decrement operators, either in postfix or prefix.
3 Python's boolean NOT operator not is not a prefix operator and has much lower precedence, ranking just above the conjunction operator.
4 The bitwise shift operators in Java and C++ rank at this level but their bitwise operators & ^ | have lower precedence, between equality operators and the conjunction operator.
5 All Python's bitwise operators rank at the same level as Kotlin's infix functions; however, unlike Kotlin, the order of operations among the bitwise operators is ranked internally as << >>, &, ^, |.
6 Technically, Python's membership test, comparison, and equality operators all rank on the same level.

⚠️ Note that this table is not exhaustive and language-specific operators that have not been mentioned in this post have been excluded.

To see a full summary of each language's order of operations, please visit the official documentation:


Thanks for reading!🏝️🥚🐣🐤🐔

|© 2023 bog-walk