Pure Programmer
Blue Matrix


Cluster Map

Operators

Every programming language provides built-in operators for performing arithmetic, logic and bitwise operations. You will also find operators to compare values and assign value to a variable, subscript arrays and invoke functions. Depending on the language you may find referencing/dereferencing, member selection, pattern matching, range or type casting operators. Many of these operators will be explored in this section, but some will be deferred until later sections where their use becomes necessary.

Typecasting

In untyped languages like Rust we don't often need to convert a value of one type to a value of a different type. Often this is done automatically for us by the language. For example, converting an integer to a string or a string to an integer is done automatically based on the context.

One situation where we might need a typecast is when converting floating point values to an integer value. If we use the typecast to int we will get a truncation of the floating point value. Perhaps a better option is to use the floor() and ceil() math functions to either round down or round up as needed.

Arithmetic Operators

[[Arithmetic operations]] are one of the two major groups of operations that computers perform for us, the other being [[logical operations]]. Arithmetic operators include multiplication, division, remainder (modulus), addition, subtraction and negation (unary minus). Most of these operations should be familiar as they are the basic arithmetic operations taught in elementary school. Modulus is less familiar but it is simply the remainder after division. Remember those times studying long division where we ended up with a remainder? That remainder is the modulus.

The arithmetic operators are mostly similar to the symbols typically used in mathematics: addition (+), subtraction (-), division (/). Only multiplication (*) and modulus (%) differ from standard mathematical notation. Unlike standard mathematical notation where placing two symbols next to each other implies multiplication as in xyz (which means x × y × z), when writing code we must explicitly use the multiplication operator as in x * y * z.

Operators1.rs
fn main() {
	let a:isize = 3;
	let b:isize = 5;
	let c:isize = 2;
	let d:f64 = 3.1415926f64;
	let e:f64 = 4.0f64;

	println!("-b = {}", (-b));
	println!("a + b = {}", (a + b));
	println!("a + -b = {}", (a + -b));
	println!("-a + b = {}", (-a + b));
	println!("-a + -b = {}", (-a + -b));
	println!("a - b = {}", (a - b));
	println!("a - -b = {}", (a - -b));
	println!("-a - b = {}", (-a - b));
	println!("-a - -b = {}", (-a - -b));
	println!("b * c = {}", (b * c));
	println!("b * -c = {}", (b * -c));
	println!("-b * c = {}", (-b * c));
	println!("-b * -c = {}", (-b * -c));
	println!("b / c = {}", (f64::from(b) / f64::from(c)));
	println!("b / -c = {}", (f64::from(b) / f64::from(-c)));
	println!("-b / c = {}", (f64::from(-b) / f64::from(c)));
	println!("-b / -c = {}", (f64::from(-b) / f64::from(-c)));
	println!("b // c = {}", (b / c));
	println!("b // -c = {}", (b / -c));
	println!("-b // c = {}", (-b / c));
	println!("-b // -c = {}", (-b / -c));
	println!("b % a = {}", (b % a));
	println!("b % -a = {}", (b % -a));
	println!("-b % a = {}", (-b % a));
	println!("-b % -a = {}", (-b % -a));
	println!("d + e = {}", (d + e));
	println!("d - e = {}", (d - e));
	println!("d * e = {}", (d * e));
	println!("e / d = {}", (e / d));
	println!("e // d = {}", ((e / d) as isize));
}

Output
$ rustc Operators1.rs error[E0277]: the trait bound `f64: From<isize>` is not satisfied --> Operators1.rs:21:36 | 21 | println!("b / c = {}", (f64::from(b) / f64::from(c))); | --------- ^ the trait `From<isize>` is not implemented for `f64` | | | required by a bound introduced by this call | = help: the following other types implement trait `From<T>`: <f64 as From<bool>> <f64 as From<f32>> <f64 as From<i16>> <f64 as From<i32>> <f64 as From<i8>> <f64 as From<u16>> <f64 as From<u32>> <f64 as From<u8>> error[E0277]: the trait bound `f64: From<isize>` is not satisfied --> Operators1.rs:21:51 | 21 | println!("b / c = {}", (f64::from(b) / f64::from(c))); | --------- ^ the trait `From<isize>` is not implemented for `f64` | | | required by a bound introduced by this call | = help: the following other types implement trait `From<T>`: <f64 as From<bool>> <f64 as From<f32>> <f64 as From<i16>> <f64 as From<i32>> <f64 as From<i8>> <f64 as From<u16>> <f64 as From<u32>> <f64 as From<u8>> error[E0277]: the trait bound `f64: From<isize>` is not satisfied --> Operators1.rs:22:37 | 22 | println!("b / -c = {}", (f64::from(b) / f64::from(-c))); | --------- ^ the trait `From<isize>` is not implemented for `f64` | | | required by a bound introduced by this call | = help: the following other types implement trait `From<T>`: <f64 as From<bool>> <f64 as From<f32>> <f64 as From<i16>> <f64 as From<i32>> <f64 as From<i8>> <f64 as From<u16>> <f64 as From<u32>> <f64 as From<u8>> error[E0277]: the trait bound `f64: From<isize>` is not satisfied --> Operators1.rs:22:52 | 22 | println!("b / -c = {}", (f64::from(b) / f64::from(-c))); | --------- ^^ the trait `From<isize>` is not implemented for `f64` | | | required by a bound introduced by this call | = help: the following other types implement trait `From<T>`: <f64 as From<bool>> <f64 as From<f32>> <f64 as From<i16>> <f64 as From<i32>> <f64 as From<i8>> <f64 as From<u16>> <f64 as From<u32>> <f64 as From<u8>> error[E0277]: the trait bound `f64: From<isize>` is not satisfied --> Operators1.rs:23:37 | 23 | println!("-b / c = {}", (f64::from(-b) / f64::from(c))); | --------- ^^ the trait `From<isize>` is not implemented for `f64` | | | required by a bound introduced by this call | = help: the following other types implement trait `From<T>`: <f64 as From<bool>> <f64 as From<f32>> <f64 as From<i16>> <f64 as From<i32>> <f64 as From<i8>> <f64 as From<u16>> <f64 as From<u32>> <f64 as From<u8>> error[E0277]: the trait bound `f64: From<isize>` is not satisfied --> Operators1.rs:23:53 | 23 | println!("-b / c = {}", (f64::from(-b) / f64::from(c))); | --------- ^ the trait `From<isize>` is not implemented for `f64` | | | required by a bound introduced by this call | = help: the following other types implement trait `From<T>`: <f64 as From<bool>> <f64 as From<f32>> <f64 as From<i16>> <f64 as From<i32>> <f64 as From<i8>> <f64 as From<u16>> <f64 as From<u32>> <f64 as From<u8>> error[E0277]: the trait bound `f64: From<isize>` is not satisfied --> Operators1.rs:24:38 | 24 | println!("-b / -c = {}", (f64::from(-b) / f64::from(-c))); | --------- ^^ the trait `From<isize>` is not implemented for `f64` | | | required by a bound introduced by this call | = help: the following other types implement trait `From<T>`: <f64 as From<bool>> <f64 as From<f32>> <f64 as From<i16>> <f64 as From<i32>> <f64 as From<i8>> <f64 as From<u16>> <f64 as From<u32>> <f64 as From<u8>> error[E0277]: the trait bound `f64: From<isize>` is not satisfied --> Operators1.rs:24:54 | 24 | println!("-b / -c = {}", (f64::from(-b) / f64::from(-c))); | --------- ^^ the trait `From<isize>` is not implemented for `f64` | | | required by a bound introduced by this call | = help: the following other types implement trait `From<T>`: <f64 as From<bool>> <f64 as From<f32>> <f64 as From<i16>> <f64 as From<i32>> <f64 as From<i8>> <f64 as From<u16>> <f64 as From<u32>> <f64 as From<u8>> error: aborting due to 8 previous errors For more information about this error, try `rustc --explain E0277`.

String Concatenation

String concatenation is a special operator that is used to combine two strings into a single string. We use the plus (+) symbol to represent this special operation. For example:

Operators2.rs
fn main() {
	let a:&str = "Hello, ";
	let b:&str = "world";
	let c:&str = "Bruce";
	let d:&str = "Sheriff Brody";
	let e:&str = "!";
	println!("{}", String::from(a) + b + e);
	println!("{}", String::from(a) + c + e);
	println!("{}", String::from(a) + d + e);
}

Output
$ rustc Operators2.rs $ ./Operators2 Hello, world! Hello, Bruce! Hello, Sheriff Brody!

Comparison Operators

The comparison operators less than, greater than, equal, not equal, etc. allow us to determine how two values compare to each other. They are fundamentally arithmetic in nature. This is because comparisons are subtraction operations that then look at whether the difference is negative, positive or zero.

The symbol to check equality is (==). Note that there are two equal signs not just one which has a different meaning, namely assignment. The symbol to check if two values are not equal is (!=). The symbols for less than (<) and greater than (>) are as we would expect. The symbols for less than or equal (<=) and greater than or equal (>=) add the equal sign to the less than or greater than symbols. For these symbols the equal sign always appears on the right.

Operators3.rs
fn main() {
	let a:isize = 3;
	let b:isize = 5;
	let c:f64 = 3.1415926f64;
	let d:f64 = 4.0f64;
	let e:&str = "apple";
	let f:&str = "pear";

	println!("a < b : {}", (a < b));
	println!("a <= b : {}", (a <= b));
	println!("a > b : {}", (a > b));
	println!("a >= b : {}", (a >= b));
	println!("a == b : {}", (a == b));
	println!("a != b : {}", (a != b));

	println!("c < d : {}", (c < d));
	println!("c <= d : {}", (c <= d));
	println!("c > d : {}", (c > d));
	println!("c >= d : {}", (c >= d));
	println!("c == d : {}", (c == d));
	println!("c != d : {}", (c != d));

	println!("e < f : {}", (e.to_string() < f.to_string()));
	println!("e <= f : {}", (e.to_string() <= f.to_string()));
	println!("e > f : {}", (e.to_string() > f.to_string()));
	println!("e >= f : {}", (e.to_string() >= f.to_string()));
	println!("e == f : {}", (e.to_string() == f.to_string()));
	println!("e != f : {}", (e.to_string() != f.to_string()));
}

Output
$ rustc Operators3.rs $ ./Operators3 a < b : true a <= b : true a > b : false a >= b : false a == b : false a != b : true c < d : true c <= d : true c > d : false c >= d : false c == d : false c != d : true e < f : true e <= f : true e > f : false e >= f : false e == f : false e != f : true

Logical Operators

The logical operators allow us to perform [[Boolean]] arithmetic. We have the logical operators AND (&&), OR (||) and NOT (!) that can be used to form any logical expression needed. The logical operators operate on and return the values false for falsity and true for truth.

ABA && B
falsefalsefalse
falsetruefalse
truefalsefalse
truetruetrue
Logical AND
 
ABA || B
falsefalsefalse
falsetruetrue
truefalsetrue
truetruetrue
Logical OR
 
A! A
falsetrue
truefalse
Logical NOT
Operators4.rs
fn main() {
	let a:bool = true;
	let b:bool = false;

	println!("a AND b : {}", (a && b));
	println!("a OR b : {}", (a || b));
	println!("NOT a : {}", (!a));
	println!("NOT b : {}", (!b));
}

Output
$ rustc Operators4.rs $ ./Operators4 a AND b : false a OR b : true NOT a : false NOT b : true

Bitwise Operators

We also have [[bitwise operators]] that work on the premise that each bit (0 or 1) in a value can be considered either false or true, respectively. Bitwise operators operate on integer values and perform their operation for each pair of bits (for binary operations) in the value. Some examples using 4-bit values are illustrated below.

ABA & B
000000000000
000001010000
001101010001
110001010100
111100110011
111111111111
Bitwise AND
 
ABA | B
000000000000
000001010101
001101010111
110001011101
111100111111
111111111111
Bitwise OR
 
ABA ^ B
000000000000
000001010101
001101010110
110001011001
111100111100
111111110000
Bitwise XOR
 
A~A
00001111
00111100
10100101
11110000
Compliment
Operators5.rs
fn main() {
	let a:isize = 0x5555AAAA;
	let b:isize = 0x36C36CF;

	println!("{}", format!("Bitwise a AND b : {0:08x}", (a & b)));
	println!("{}", format!("Bitwise a OR b : {0:08x}", (a | b)));
	println!("{}", format!("Bitwise a XOR b : {0:08x}", (a ^ b)));
	println!("{}", format!("Compliment a : {0:08x}", (!a)));
	println!("{}", format!("Compliment b : {0:08x}", (!b)));
}

Output
$ rustc Operators5.rs $ ./Operators5 Bitwise a AND b : 0144228a Bitwise a OR b : 577dbeef Bitwise a XOR b : 56399c65 Compliment a : ffffffffaaaa5555 Compliment b : fffffffffc93c930

Increment and Decrement

The increment (++) and decrement (--)operators modify a variable by adding or subtracting one respectively. These unary operators can be applied in front of (prefix) or behind (postfix) the variable symbol. The prefix operators return the new value of the variable whereas the postfix form returns the original value of the variable. In this ways the increment and decrement operators can be used to modify a variable but also can also be used in expressions. It can be tricky to use them correctly so it is generally best to only use a variable once in an expression if it has increment or decrement applied to it.

Assignment Operators

Assignment operators allow us to change the value of a variable. The assignment operator is represented by a single equal sign (=). It should not be confused with the equivalence comparison operator which is represented by a double equal sign (==). In addition to simple assignment we also have variations on the assignment operator that perform an operation before assignment. These shorthand assignment operators are listed below showing the equivalent using only simple assignment.

Assignment Operators
OperatorEquivalent
A *= BA = A * B
A /= BA = A / B
A %= BA = A % B
A += BA = A + B
A -= BA = A - B
A &= BA = A & B
A |= BA = A | B
A ^= BA = A ^ B
A <<= BA = A << B
A >>= BA = A >> B
Operators6.rs
fn main() {
	let mut a:isize = 3;

	a += 1;
	println!("++a: {}", a);
	a -= 1;
	println!("--a: {}", a);

	a += 2;
	println!("a += 2 : {}", a);
	a *= 3;
	println!("a *= 3 : {}", a);
	a -= 4;
	println!("a -= 4 : {}", a);
	a /= 5;
	println!("a /= 5 : {}", a);
}

Output
$ rustc Operators6.rs $ ./Operators6 ++a: 4 --a: 3 a += 2 : 5 a *= 3 : 15 a -= 4 : 11 a /= 5 : 2

Conditional Operator

The only ternary operator (takes three operands) is the conditional operator. Based on the truth of the first operator, it will yield either the value of the second or third operator. The expression A ? B : C yields B if A is true and C if A is false.

Operators7.rs
fn main() {
	let a:isize = 3;
	let b:isize = 5;

	println!("a < b ? -1 : 1 => {}", (if a < b { -1 } else { 1 }));
	println!("b < a ? -1 : 1 => {}", (if b < a { -1 } else { 1 }));
	println!("a < b ? 'less' : 'greater' => {}", (if a < b { "less" } else { "greater" }));
	println!("b < a ? 'less' : 'greater' => {}", (if b < a { "less" } else { "greater" }));
}

Output
$ rustc Operators7.rs $ ./Operators7 a < b ? -1 : 1 => -1 b < a ? -1 : 1 => 1 a < b ? 'less' : 'greater' => less b < a ? 'less' : 'greater' => greater
Operators8.rs
mod utils;

fn main() {
	let sFloat:&str = "3.1415926";
	let sInt:&str = "3";
	let sLong:&str = "123456789123456789";
	let a:f64 = Utils.stodWithDefault(sFloat, 0.0f64);
	let b:isize = Utils.stoiWithDefault(sInt, 0);
	let c:i64 = Utils.stoiWithDefault(sLong, 0);
	let strA:String = a.to_string();
	let strB:String = b.to_string();
	let strC:String = c.to_string();

	println!("{}", format!("sFloat as double: {0:f}", a));
	println!("{}", format!("sInt as int: {0:d}", b));
	println!("{}", format!("sLong as long: {0:d}", c));
	println!("{}", format!("a as string: {0:s}", strA));
	println!("{}", format!("b as string: {0:s}", strB));
	println!("{}", format!("c as string: {0:s}", strC));
}

Output
$ rustc Operators8.rs error: unknown format trait `f` --> Operators8.rs:14:47 | 14 | println!("{}", format!("sFloat as double: {0:f}", a)); | ^ | = note: the only appropriate formatting traits are: - ``, which uses the `Display` trait - `?`, which uses the `Debug` trait - `e`, which uses the `LowerExp` trait - `E`, which uses the `UpperExp` trait - `o`, which uses the `Octal` trait - `p`, which uses the `Pointer` trait - `b`, which uses the `Binary` trait - `x`, which uses the `LowerHex` trait - `X`, which uses the `UpperHex` trait error: unknown format trait `d` --> Operators8.rs:15:42 | 15 | println!("{}", format!("sInt as int: {0:d}", b)); | ^ | = note: the only appropriate formatting traits are: - ``, which uses the `Display` trait - `?`, which uses the `Debug` trait - `e`, which uses the `LowerExp` trait - `E`, which uses the `UpperExp` trait - `o`, which uses the `Octal` trait - `p`, which uses the `Pointer` trait - `b`, which uses the `Binary` trait - `x`, which uses the `LowerHex` trait - `X`, which uses the `UpperHex` trait error: unknown format trait `d` --> Operators8.rs:16:44 | 16 | println!("{}", format!("sLong as long: {0:d}", c)); | ^ | = note: the only appropriate formatting traits are: - ``, which uses the `Display` trait - `?`, which uses the `Debug` trait - `e`, which uses the `LowerExp` trait - `E`, which uses the `UpperExp` trait - `o`, which uses the `Octal` trait - `p`, which uses the `Pointer` trait - `b`, which uses the `Binary` trait - `x`, which uses the `LowerHex` trait - `X`, which uses the `UpperHex` trait error: unknown format trait `s` --> Operators8.rs:17:42 | 17 | println!("{}", format!("a as string: {0:s}", strA)); | ^ | = note: the only appropriate formatting traits are: - ``, which uses the `Display` trait - `?`, which uses the `Debug` trait - `e`, which uses the `LowerExp` trait - `E`, which uses the `UpperExp` trait - `o`, which uses the `Octal` trait - `p`, which uses the `Pointer` trait - `b`, which uses the `Binary` trait - `x`, which uses the `LowerHex` trait - `X`, which uses the `UpperHex` trait error: unknown format trait `s` --> Operators8.rs:18:42 | 18 | println!("{}", format!("b as string: {0:s}", strB)); | ^ | = note: the only appropriate formatting traits are: - ``, which uses the `Display` trait - `?`, which uses the `Debug` trait - `e`, which uses the `LowerExp` trait - `E`, which uses the `UpperExp` trait - `o`, which uses the `Octal` trait - `p`, which uses the `Pointer` trait - `b`, which uses the `Binary` trait - `x`, which uses the `LowerHex` trait - `X`, which uses the `UpperHex` trait error: unknown format trait `s` --> Operators8.rs:19:42 | 19 | println!("{}", format!("c as string: {0:s}", strC)); | ^ | = note: the only appropriate formatting traits are: - ``, which uses the `Display` trait - `?`, which uses the `Debug` trait - `e`, which uses the `LowerExp` trait - `E`, which uses the `UpperExp` trait - `o`, which uses the `Octal` trait - `p`, which uses the `Pointer` trait - `b`, which uses the `Binary` trait - `x`, which uses the `LowerHex` trait - `X`, which uses the `UpperHex` trait error[E0425]: cannot find value `Utils` in this scope --> Operators8.rs:7:14 | 7 | let a:f64 = Utils.stodWithDefault(sFloat, 0.0f64); | ^^^^^ not found in this scope error[E0425]: cannot find value `Utils` in this scope --> Operators8.rs:8:16 | 8 | let b:isize = Utils.stoiWithDefault(sInt, 0); | ^^^^^ not found in this scope error[E0425]: cannot find value `Utils` in this scope --> Operators8.rs:9:14 | 9 | let c:i64 = Utils.stoiWithDefault(sLong, 0); | ^^^^^ not found in this scope error: aborting due to 9 previous errors For more information about this error, try `rustc --explain E0425`.

Precedence

For simple expressions that contain only one operator, it is easy to see how it will be evaluated. But more complex expressions that contain multiple operators could lead to ambiguity and unpredictable results unless we had some rules in place to help us out. These rules to eliminate ambiguity are the rules of precedence illustrated below. In this table, operators which have higher precedence appear closer to the top. Operators with lower precedence appear closer to the bottom. In a situation where two operators are potentially operating on a value, the operator that is higher in the table takes precedence over a lower operator. If the two operators are on the same level of precedence then the associativity of the operators helps us to determine which takes precedence.

For example, the expression 5 * 3 + 1 could be ambiguous. Without rules of precedence it wouldn't be clear weather the multiplication or the addition should be performed first. If the multiplication takes precedence we would have (5 * 3) + 1 == 16 whereas if the addition takes precedence we would have 5 * (3 + 1) == 20. But we do have rules of precedence so there is only one correct way to interpret that expression. Because multiplication appears in the table on a level above addition, we know that the first interpretation yielding 16 is the correct one. If we instead wanted the expression to be evaluated the second way to yield 20, we would have to use parenthesis to explicitly override the rules of precedence. Many of the rules of precedence come from standard arithmetic and logical notation so they should seem familiar.

Rules of precedence don't guarantee any particular order of evaluation, but they do help resolve issues when two operators are applied to the same value. This subtlty is not obvious because in most expressions there is only one order of evaluation that follows the rules of precedence. Consider (10 - 6) - (3 - 1). At first glance it would appear that the parenthesis cause this expression to be evaluated in only one way. But it turns out that there are two ways to evaluate this expression without violating the rules of precedence, namely to compute (10 - 6) first or to compute (3 - 1) first. So there are two ways to order the evaluation of this expression without violating the rules of precedence. In this case as in most cases, the end result will be the same, but it illustrates that the rules of precedence still leave some leeway in how the computer carries out the evaluation of the expression.

Where we get into trouble is with the operators that change a variable, namely the increment (++), decrement (--) and assignment operators. When these operators are used in an expression, leeway in when that operation is performed can cause the entire expression to yield different results. Let us assume that we have a variable A that is set to 1. Then we consider the expression ++A - ++A. According to the rules of precedence there is no ambiguity in this expression. The first A has a prefix increment (++) applied on the left and subtraction (-) on the right. Since unary operators take precedence over subtraction we know that the increment must be applied before the addition. The second A only has one operator applied, the prefix increment so that must be applied before its result is added to the result of the first A after it is incremented. So here is the problem, we need to know when the first A is incremented to know what value is represented by the second A. The computer is free to evalutate the first ++A yielding 2 then the second ++A yielding 3. Equally it is free to evaluate the second ++A first yielding 2 then the first yielding 3. This would give us either 2 - 3 or 3 - 2 neither of which violate any of the rules of precedence. So in this case there is a reliance on knowing the order of evaluation which leads to ambiguity. Just because it evaluates one way doesn't mean that it will always evaluate that way. A new version of the language tools can cause the expression to change how it is evaluated leading to a difficult bug to track down. So to avoid this problem we have a rule of thumb to never use a variable more than once in an expression if it has had an operator applied that produces the side effect of changing that variable.

Consider the expression 10 * 6 - 4 / 2. Two of the values have operators on both the left and the right side requiring us to use the rules of precedence. Two values do not. Use parenthesis to show how the rules of precedence require us to interpret this expression.
Answer: ((10 * 6) - (4 / 2)) == (60 - 2) == 58
Note: It is equally correct for (10 * 6) to be evaluated first or for (4 / 2) to be evaluated first. Because the expressions do not have any side effects, the result will be the same either way.

Consider the expression A + B << 2 * 3 & 0xff. Two of the values have operators on both the left and the right side requiring us to use the rules of precedence. Two values do not. Use parenthesis to show how the rules of precedence require us to interpret this expression.
Answer: (((A + B) << (2 * 3)) & 0xff)

Rust Operator Rules of Precedence
Operator Description Associativity

As we have seen, we can use parenthesis to change how an expression is to be interpreted. Using parenthesis can also make an expression easier to read. Adding parenthesis to an equation doesn't incur any performance penalty even if the expression would have been interpreted the desired way without the parenthesis.

TODO: Precision and rounding errors in operations 1/3

Questions

Projects

More ★'s indicate higher difficulty level.

References