Operators · Lesson 2.1

Arithmetic

Source
Calculate with + - * / %, and see why 17 / 5 is 3 for integers but 3.4 for floats.

Arithmetic is the first thing most programs do with their values: add up a bill, split it between friends, work out what is left over. Rux has five arithmetic operators, and they look the way they do in maths. The one surprise is division — dividing two whole numbers gives a whole number, and the fraction is quietly dropped.

The five operators

OperatorMeaning17 and 5 give
+Addition22
-Subtraction12
*Multiplication85
/Division3
%Remainder2

Each one takes a value on either side and produces a new value. Neither operand changes — a + b reads a and b and hands back their sum:

let a: int32 = 17;
let b: int32 = 5;
PrintLine("{} + {} = {}", a, b, a + b);

Integer division drops the fraction

On paper, 17 divided by 5 is 3.4. Two integers cannot hold 3.4, so integer division truncates: it keeps the whole part and throws the rest away. The % operator gives back the part that was dropped, the remainder:

PrintLine("{} / {} = {}", a, b, a / b);
PrintLine("{} % {} = {}", a, b, a % b);

17 is three fives and two left over, so / says 3 and % says 2. The pair is useful far beyond maths homework — % is how a program asks "is this number even?" (n % 2 == 0) or "which minute of the hour is it?" (seconds / 60 % 60).

Truncation goes towards zero, so −3.4 becomes −3 rather than −4. The remainder keeps the sign of the number being divided:

PrintLine("{} / {} = {}", -a, b, -a / b);
PrintLine("{} % {} = {}", -a, b, -a % b);

Floats keep the fraction

Divide two floats and the fraction stays. % works on floats too, and gives what is left after taking out as many whole fives as fit:

let x: float64 = 17.0;
let y: float64 = 5.0;
PrintLine("{} / {} = {}", x, y, x / y);
PrintLine("{} % {} = {}", x, y, x % y);

What / does depends only on the types of its two operands:

flowchart LR
    div["left / right"] --> q{"What are the<br/>two operand types?"}
    q -- "both integers" --> int["Whole number:<br/>17 / 5 is 3"]
    q -- "both floats" --> fl["Keeps the fraction:<br/>17.0 / 5.0 is 3.4"]
    q -- "one of each" --> err["error: operator '/' cannot combine<br/>left operand 'int32' with<br/>right operand 'float64'"]
    err --> fix["Convert one side with as,<br/>then divide"]

Both sides must have the same type

Rux never mixes an int32 and a float64 on its own, because either choice could lose something. To divide two integers and keep the fraction, convert them first with as — the operator from Convert:

PrintLine("{} / {} as floats = {}", a, b, (a as float64) / (b as float64));

Converting after the division is too late: (a / b) as float64 is 3.0, because the fraction was already gone. Precedence looks at that case again.

Overflow wraps around

Every integer type has a largest value. A uint8 holds 0 to 255, and a sum that goes past the top wraps around and starts again from 0 — with no warning:

let level: uint8 = 250;
PrintLine("uint8 {} + 10 = {}", level, level + 10);

250 + 10 is 260, and 260 − 256 lands on 4. The 10 is a uint8 here because an unsuffixed literal takes the type of the value beside it, as Literal showed. Choosing a type wide enough for your numbers is the everyday fix; Wrapping arithmetic and Checked arithmetic cover the tools for when it is not enough.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// The five arithmetic operators: `+`, `-`, `*`, `/` and `%`. They work on
// integers and floats alike, but division means something different for each:
// two integers give a whole-number answer and throw the fraction away, while two
// floats keep it.
import Io::PrintLine;

func Main() -> int {
    let a: int32 = 17;
    let b: int32 = 5;
    PrintLine("{} + {} = {}", a, b, a + b);
    PrintLine("{} - {} = {}", a, b, a - b);
    PrintLine("{} * {} = {}", a, b, a * b);

    // Integer division truncates: 17 / 5 is 3, not 3.4. The `%` operator gives
    // back the part that division dropped, the remainder.
    PrintLine("{} / {} = {}", a, b, a / b);
    PrintLine("{} % {} = {}", a, b, a % b);

    // Truncation goes towards zero, so -3.4 becomes -3 rather than -4, and the
    // remainder keeps the sign of the number being divided.
    PrintLine("{} / {} = {}", -a, b, -a / b);
    PrintLine("{} % {} = {}", -a, b, -a % b);

    // Floats keep the fraction. `%` works on them too.
    let x: float64 = 17.0;
    let y: float64 = 5.0;
    PrintLine("{} / {} = {}", x, y, x / y);
    PrintLine("{} % {} = {}", x, y, x % y);

    // Both operands must have the same type: `a / y` is rejected, an `int32`
    // divided by a `float64`. To divide integers and keep the fraction, convert
    // them first with `as`.
    PrintLine("{} / {} as floats = {}", a, b, (a as float64) / (b as float64));

    // An integer that outgrows its type wraps around without any warning. A
    // `uint8` holds 0 to 255, so 250 + 10 lands on 4.
    let level: uint8 = 250;
    PrintLine("uint8 {} + 10 = {}", level, level + 10);
    return 0;
}

Run it

cd Examples/Operators/Arithmetic
rux run
17 + 5 = 22
17 - 5 = 12
17 * 5 = 85
17 / 5 = 3
17 % 5 = 2
-17 / 5 = -3
-17 % 5 = -2
17.0 / 5.0 = 3.4
17.0 % 5.0 = 2.0
17 / 5 as floats = 3.4
uint8 250 + 10 = 4

Common mistakes

Mixing an integer and a float.
When a is an int32 and y is a float64, a / y fails with error: operator '/' cannot combine left operand 'int32' with right operand 'float64'. Convert one side with as so both have the same type.
Expecting a fraction from two integers.
17 / 5 is 3, not 3.4, and (a / b) as float64 is 3.0. Convert the operands before dividing, not the result after.
Dividing an integer by zero.
The compiler does not catch it. The program stops while running with Panic: division by zero and the line it happened on. When the divisor might be zero, test it first — Logical shows a neat way to do that.

Try it yourself

  1. Turn 1000 seconds into minutes and seconds using / and %, and print a line like 16 min 40 s.
  2. Change a and b to -17 and -5. Predict both / and % before you run.
  3. Work out the average of three int32 scores, 7, 8 and 10, once as an integer and once as a float64.
  4. Change the uint8 example to level - 251. Which number does it wrap around to?

Learn more