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.pow23val ans1 = 10 + 2 * 3 / 2 - 34// Will ans1 = 10, 15, or 5?56val ans2 = 100 / (10 + 15) - -4 * -17// Will ans2 = 0, -8, or 4?89val ans3 = -(-2.0 * (-3.0).pow(2)).pow(2)10// Will ans3 = -324.0, 324.0, or 1296.0?1112val ans4 = !(true || false) && !true || !false && true13// Will ans4 = true or false?1415val ans5 = 4 * 3 > 5 * 2 || !(10 % 2 != 1 || 2 * 2 == 4) && !!false16// Will ans5 = true or false?
Answers
1val ans1 = 10 + 2 * 3 / 2 - 32// Equivalent to: 10 + (2 * 3 / 2) - 33println("10 + 2 * 3 / 2 - 3 = $ans1")
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 * -12// Equivalent to: (100 / (10 + 15)) - (-4 * -1)3println("100 / (10 + 15) - -4 * -1 = $ans2")
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")
-(-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 && true2println("!(true || false) && !true || !false && true = $ans4")
!(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) && !!false2println("4 * 3 > 5 * 2 || !(10 % 2 != 1 || 2 * 2 == 4) && !!false = $ans5")
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 * 32println(pair1) // (10, 12)34val pair2 = "Teens" to 11..29 - 105println(pair2) // (Teens, 11..19)
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 work2val pair1 = "A" to "a" in "aeiou"3val pair2 = 100 > 10 to 14val pair3 = "Case" to "text".lowercase() == "text"5*/67// but these will8val pair1 = "A" to ("a" in "aeiou")9val pair2 = (100 > 10) to 110val pair3 = "Case" to ("text".lowercase() == "text")1112// this statement works too13val bool = 1 to 2 in listOf(1 to 1, 1 to 2, 1 to 3)
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 / 192println(isSpecial) // true
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)45val messy = 4 to 'd' in 1..5 zip 'a'..'e' && 2 to 3 in set2 zip set1 - set36println(messy) // true
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 32println(a) // false
1public static void main(String args[]) {2 // causes compile error: bad operand types for binary operator '&'3 // boolean a = 10 & 1 > 2 | 3;45 // this works6 boolean a = (10 & 1) > (2 | 3);7 System.out.println(a); // false8}
1a = 10 & 1 > 2 | 32print(a) # False
1#include <iostream>23int main()4{5 bool a = 10 & 1 > 2 | 3;6 std::cout << std::boolalpha << a << std::endl; // true7 a = (10 & 1) > (2 | 3);8 std::cout << a << std::endl; // false910 return 0;11}
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 42println(a) // 03val b = (8 or 1) and 44println(b) // 05val c = 8 or (1 and 4)6println(c) // 8
1public static void main(String args[]) {2 int a = 8 | 1 & 4;3 System.out.println(a); // 84 int b = (8 | 1) & 4;5 System.out.println(b); // 06 int c = 8 | (1 & 4);7 System.out.println(c); // 88}
1a = 8 | 1 & 42print(a) # 83b = (8 | 1) & 44print(b) # 05c = 8 | (1 & 4)6print(c) # 8
1#include <iostream>23int main()4{5 int a = 8 | 1 & 4;6 std::cout << a << std::endl; // 87 int b = (8 | 1) & 4;8 std::cout << b << std::endl; // 09 int c = 8 | (1 & 4);10 std::cout << c << std::endl; // 81112 return 0;13}
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 122println(a) // 4
1public static void main(String args[]) {2 int a = 9 | 1 << 2 & 12;3 System.out.println(a); // 134}
1a = 9 | 1 << 2 & 122print(a) # 13
1#include <iostream>23int main()4{5 int a = 9 | 1 << 2 & 12;6 std::cout << a << std::endl; // 1378 return 0;9}
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
1public static void main(String args[]) {2 int a = 15 ^ ~11;3 System.out.println(a); // -54}
1a = 15 ^ ~112print(a) # -5
1#include <iostream>23int main()4{5 int a = 15 ^ ~11;6 std::cout << a << std::endl; // -578 return 0;9}
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)34val y = 0 or 1 until (25 xor 19) step (1 shl 1)5println(y) // 1..9 step 267val 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)]
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)45val messyOG = 4 to 'd' in 1..5 zip 'a'..'e' && 2 to 3 in set2 zip set1 - set36println(messyOG) // true78val messy = 4 to 'd' in 1..5 zip 'a'..'e' && 2 to 3 in set2 zip set1 subtract set39println(messy) // false1011val messyFixed = 4 to 'd' in 1..5 zip 'a'..'e' && 2 to 3 in set2 zip (set1 subtract set3)12println(messyFixed) // true
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++:
KOTLIN | JAVA | PYTHON | C++ |
---|---|---|---|
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!🏝️🥚🐣🐤🐔