New Remainder Operators for Pyro

31st October 2024

The latest release of Pyro, my hobby programming language, adds support for two new remainder operators for integer division — rem and mod. These new operators replace the old % operator, which is now deprecated.

So what's the difference?

That explanation might not be immediately enlightening but I'll provide some concrete examples below which should clear up the distinction.

Before we dig in any further, do note that if both operands are positive, then the two operators will produce the same result — e.g. 5 rem 3 and 5 mod 3 both produce the same result, namely 2. We only have the potential for different results if one or other of the operands is negative.

Terminology

Let's start by defining some terminology.

When we calculate an integer division, we start with two values — the numerator and the denominator. In standard mathematical notation, we want to perform the operation numerator/denominator.

This operation has two outputs — the quotient (the count of how many times the denominator divides into the numerator), and the remainder (the bit that's left over).

Importantly, we always want this condition to be true:

assert quotient * denominator + remainder == numerator;

The complicating factor is that there are (at least) two different ways we can define the division operation. In programming languages, the important options are "truncated-division" and "floored-division".

Truncated Division

In truncated-division, the quotient is rounded toward zero. (This is Pyro's default kind of integer division — i.e. it's the kind of integer division performed by the // operator.)

Here are some examples using the std::math module's trunc_div() function which takes the numerator and denominator as arguments and returns a (quotient, remainder) tuple:

import std::math;

assert math:trunc_div(5, 3) == (1, 2);    # 1 * 3 + 2 == 5
assert math:trunc_div(5, -3) == (-1, 2);  # -1 * -3 + 2 == 5
assert math:trunc_div(-5, 3) == (-1, -2); # -1 * 3 - 2 == -5
assert math:trunc_div(-5, -3) == (1, -2); # 1 * -3 - 2 == -5

The remainder in this operation is the same as the output of the rem operator in Pyro and the same as the output of the % operator in C.1

Floored Division

In floored-division, the quotient is rounded toward negative infinity.

Here are some examples using the std::math module's floor_div() function which takes the numerator and denominator as arguments and returns a (quotient, remainder) tuple:

import std::math;

assert math:floor_div(5, 3) == (1, 2);    # 1 * 3 + 2 == 5
assert math:floor_div(5, -3) == (-2, -1); # -2 * -3 - 1 == 5
assert math:floor_div(-5, 3) == (-2, 1);  # -2 * 3 + 1 == -5
assert math:floor_div(-5, -3) == (1, -2); # 1 * -3 - 2 == -5

The remainder in this operation is the same as the output of the mod operator in Pyro and the same as the output of the % operator in Python.2

This remainder corresponds to the result of the modulo operation in mathematics.

(I don't know of any actual use-case for modulo with a negative denominator, but the result is at least internally consistent for this possibility.)

Notes

1

At least from C99 onwards. In older versions of C, the division and remainder operations for integer division were implementation-defined and could use either truncated-division or floored-division. From C99, the standard mandates the use of truncated-division.

2

See here for Guido's explanation of why Python works the way it does.