Calculator
A pocket calculator shows "E" when something goes wrong and leaves you guessing. This one works on 32-bit integers and says exactly why it cannot give an answer: you divided by zero, pressed a key that is not an operator, or asked for a result too large for an int32.
It is the checkpoint for Part 9: Errors, and it uses each of the main tools from that part in the place where it fits best: fail to raise an error, ? to pass one on, match to take one apart, and catch to recover and carry on.
How it is put together
Every way a sum can go wrong is a case of one error variant, and each case carries the detail that explains it:
variant CalcError {
DivisionByZero,
UnknownOperator(char),
Overflow(int64)
}
Five small functions share the work, plus PrintTape, which only echoes a tape. Arrows show who calls whom, and what travels back:
flowchart LR
main(["Main"]) --> apply["Apply<br/>one sum"]
main --> run["Run<br/>a whole tape"]
run -- "each step, with ?" --> apply
apply -- "the answer, with ?" --> narrow["Narrow<br/>int64 → int32"]
main --> show["Show<br/>.Success or .Failure"]
show --> explain["Explain<br/>one line per case"]| Function | What it does with an error | Lessons it uses |
|---|---|---|
Narrow | Raises Overflow when the answer does not fit | Fail, Convert |
Apply | Raises the other two, passes Narrow's on | Fail, Match expression, Propagate |
Run | Passes the first failure of a tape on | Propagate, Tuple, Destructure |
Show | Opens the outcome | Outcome |
Explain | Turns each case into a sentence | Error variant, Variant match |
Main | Skips bad steps on the forgiving tape | Catch fallback |
Overflow is checked in a wider type
Adding or multiplying two int32s can overflow int32, but no sum, difference or product of two int32s can overflow int64. So Apply widens both operands, does the arithmetic there, and Narrow checks the answer on its way back down:
func Narrow(wide: int64) -> int32 ! CalcError {
if wide > 2147483647 || wide < -2147483648 {
fail CalcError::Overflow(wide);
}
return wide as int32;
}
That catches the case everyone expects, 2147483647 + 1, and one people usually miss: -2147483648 / -1. The answer, 2147483648, is one more than an int32 can hold, because the negative range reaches one further than the positive one.
A match arm that fails
Inside Apply, a match expression picks the operation. Its last arm does not produce a value at all — it leaves the function through the failure channel:
let wide = match op {
'+' => a + b,
'-' => a - b,
'*' => a * b,
'/' => a / b,
'%' => a % b,
else => fail CalcError::UnknownOperator(op)
};
return Narrow(wide)?;
Division by zero is tested before the match, so / and % are never reached with a zero divisor. And return Narrow(wide)?; passes an overflow straight to Apply's caller: ? keeps the success and hands any failure on unchanged.
A tape stops at the first bad key
A tape is a run of key presses, each applied to the answer so far — like a real pocket calculator, strictly left to right, with no precedence. Run walks it, and one ? is all it takes to make the first failure end the whole run:
for step in tape {
let (op, operand) = step;
total = Apply(total, op, operand)?;
}
Each step is a tuple (char, int32), and let (op, operand) = step; unpacks it. The bad tape fails at its second key, / 0, so the x key after it is never even looked at.
Forgiving instead of failing
The end of Main runs the same bad tape a second time, but with catch instead of ?. A step that fails leaves total as it was, and the tape carries on:
total = Apply(total, op, operand) catch { else => total };
/ 0 and x 2 are skipped, so the tape computes (0 + 10) * 7 - 6, which is 64. Which behaviour is right — stop at the first error or skip it — is a design decision, and the two loops show that it is decided by a single token.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Calculator: a pocket calculator that works on 32-bit integers and says exactly why when it
// cannot give an answer.
//
// A real calculator shows "E" and leaves you guessing. This one has an error variant with one case
// for each way a sum can go wrong, carrying whatever detail explains it: dividing by zero, a key
// that is not an operator, and an answer too large for an `int32`.
//
// The arithmetic happens in `int64`, where no two `int32` values can overflow, and the answer is
// then checked on its way back down. That catches the cases people expect, such as 2147483647 + 1,
// and one they usually do not: -2147483648 / -1, whose answer is one more than an `int32` holds.
//
// The program uses each of the main tools from the Errors part. `Apply` raises one with
// `fail`. `Run` passes one on with `?`, so a tape of key presses stops at the first bad step.
// `Show` and `Explain` take one apart with `match`. And the forgiving calculator at the end uses
// `catch` to skip a bad step and carry on.
import Io::{ Print, PrintLine };
variant CalcError {
DivisionByZero,
UnknownOperator(char),
Overflow(int64)
}
// Brings a wide answer back to `int32`, or fails if it does not fit.
func Narrow(wide: int64) -> int32 ! CalcError {
if wide > 2147483647 || wide < -2147483648 {
fail CalcError::Overflow(wide);
}
return wide as int32;
}
func Apply(left: int32, op: char, right: int32) -> int32 ! CalcError {
if (op == '/' || op == '%') && right == 0 {
fail CalcError::DivisionByZero;
}
let a = left as int64;
let b = right as int64;
// An arm may fail instead of producing a value: an unknown key ends the function right there.
let wide = match op {
'+' => a + b,
'-' => a - b,
'*' => a * b,
'/' => a / b,
'%' => a % b,
else => fail CalcError::UnknownOperator(op)
};
return Narrow(wide)?;
}
// Presses the keys of a tape one after another, starting from `start`. The first step that fails
// fails the whole run, and `?` is all it takes to say so.
func Run(start: int32, tape: (char, int32)[..]) -> int32 ! CalcError {
var total = start;
for step in tape {
let (op, operand) = step;
total = Apply(total, op, operand)?;
}
return total;
}
func Explain(error: CalcError) {
match error {
.DivisionByZero => PrintLine("error: division by zero"),
.UnknownOperator(op) => PrintLine("error: '{}' is not an operator", op),
.Overflow(wide) => PrintLine("error: {} does not fit in an int32", wide)
}
}
func Show(outcome: int32 ! CalcError) {
match outcome {
.Success(answer) => PrintLine("{}", answer),
.Failure(error) => Explain(error)
}
}
func PrintTape(start: int32, tape: (char, int32)[..]) {
Print("{}", start);
for step in tape {
let (op, operand) = step;
Print(" {} {}", op, operand);
}
Print(" = ");
}
func Main() -> int {
let sums: (int32, char, int32)[9] = [
(12, '+', 30),
(7, '*', 6),
(17, '%', 5),
(7, '/', 0),
(7, '%', 0),
(2, '^', 8),
(2147483647, '+', 1),
(-2147483648, '/', -1),
(65536, '*', 65536)
];
for sum in sums {
let (left, op, right) = sum;
Print("{} {} {} = ", left, op, right);
Show(Apply(left, op, right));
}
PrintLine();
// A tape is a run of key presses, each applied to the answer so far. Like a pocket calculator,
// it works strictly left to right, with no precedence: 1 + 2 * 3 is 9 on a tape, not 7.
let good: (char, int32)[4] = [('+', 10), ('*', 7), ('-', 6), ('/', 8)];
let bad: (char, int32)[5] = [('+', 10), ('/', 0), ('*', 7), ('x', 2), ('-', 6)];
PrintTape(0, good);
Show(Run(0, good));
PrintTape(0, bad);
Show(Run(0, bad));
// A forgiving calculator ignores a key it cannot use. `catch { else => total }` keeps the
// answer it already had, whatever went wrong, and the tape carries on past the bad steps.
var total: int32 = 0;
for step in bad {
let (op, operand) = step;
total = Apply(total, op, operand) catch { else => total };
}
PrintLine("skipping the bad keys instead: {}", total);
return 0;
}
Run it
cd Examples/Projects/Calculator
rux run
12 + 30 = 42
7 * 6 = 42
17 % 5 = 2
7 / 0 = error: division by zero
7 % 0 = error: division by zero
2 ^ 8 = error: '^' is not an operator
2147483647 + 1 = error: 2147483648 does not fit in an int32
-2147483648 / -1 = error: 2147483648 does not fit in an int32
65536 * 65536 = error: 4294967296 does not fit in an int32
0 + 10 * 7 - 6 / 8 = 8
0 + 10 / 0 * 7 x 2 - 6 = error: division by zero
skipping the bad keys instead: 64
Common mistakes
A line such as
Apply(1, '+', 2); on its own is refused: error: fallible result of type 'int32 ! CalcError' is discarded. A failure must be handled, passed on or discarded on purpose — see Discard.? in Run.Without it,
total = Apply(total, op, operand); tries to store the whole outcome in an int32: error: cannot assign 'int32 ! CalcError' to 'int32'. The ? is what unwraps the success.Delete the
.DivisionByZero arm from Explain and the compiler names what is missing: error: match on 'CalcError' is not exhaustive; missing CalcError::DivisionByZero. Adding a case to CalcError later makes every such match point at itself, which is exactly what you want.The arms of a
match are separated by commas, and the last one has none. Leaving one there fails with error: trailing comma is not allowed in match blocks.Try it yourself
- Add a
^key for powers, with2 ^ 8giving 256. Compute it with a loop inint64and letNarrowcatch a power that is too large. What should a negative exponent do — and does it need a new case inCalcError? - Add a case
NegativeRootand anrkey that takes an integer square root, failing for a negative operand. The compiler will show you everymatchthat needs a new arm. - Make the forgiving calculator report what it skipped. Replace the
catchwith amatchon the outcome: store the answer on.Success, and callExplainon.Failurewhiletotalstays as it was. - Change
Runso that it returns the number of the step that failed along with the error. Hint: a new error type that carries auintand aCalcError, and Error mapping to build it.
Learn more
- Error variant, Fail, Propagate and Catch fallback
- Error handling in the Rux Reference
- Next projects: Circle and Quadratic, which apply the same ideas to input a person types
25.4 Prime
Find every prime below 100 with the sieve of Eratosthenes, crossing out multiples in an inline array of flags. The limit itself is excluded.
25.6 Circle
Read a radius and print the circle's circumference and area, telling apart the end of input, a read error, text that is not a number, and a number that is not a valid radius.