Numbers · Lesson 16.11

Math

Source
Tour the Math package — roots, powers, logarithms, trigonometry and rounding — with the domain of each and the limits of floating-point answers.

The arithmetic operators stop at the four operations. Square roots, powers, logarithms, angles and rounding live in the Math package, as functions on float64 — with float32 versions of each, chosen by the type of the argument.

Two things are true of all of them, and both matter more than any single function. Every one has a domain — the inputs it has an answer for — and outside it the answer is a special value, not an error. And every answer is a float: the nearest float to the true answer, not the answer itself.

The program imports what it uses, and lists Math as a dependency:

import Math::{ Abs, Cbrt, Ceil, Cos, DegToRad, Exp, Floor, Log, Log10, Log2, Pi, Pow, Round, Sin,
               Sqrt, Trunc };

Roots and powers

PrintLine("Sqrt(2)        {}", Sqrt(2.0));
PrintLine("Cbrt(-8)       {}", Cbrt(-8.0));
PrintLine("Pow(2, 10)     {}", Pow(2.0, 10.0));
PrintLine("Sqrt(-1)       {}", Sqrt(-1.0));

Sqrt wants a number that is not negative; Cbrt, the cube root, takes any sign, so Cbrt(-8.0) is -2.0. Pow(x, y) is x to the power y. And Sqrt(-1.0) is outside the square root's domain: the answer is NaN, as in Float special, and the program carries on.

Notice the arguments are written 2.0, not 2. The functions take floats, and an integer is not converted on its own.

Logarithms

PrintLine("Log(Exp(1))    {}", Log(Exp(1.0)));
PrintLine("Log2(1024)     {}", Log2(1024.0));
PrintLine("Log10(0.001)   {}", Log10(0.001));
PrintLine("Log(0)         {}", Log(0.0));

Log is the natural logarithm, base e, and the inverse of Exp; the others say their base in their name. Log2(1024.0) is 10.0 because 2¹⁰ = 1024. Logarithms want a number greater than zero, and Log(0.0) is -Inf.

FunctionWantsOutside the domain
Sqrtx ≥ 0NaN
Cbrtany x—
Log, Log2, Log10x > 0-Inf at 0, NaN below

Check the input first when it might fall outside — a NaN produced deep inside a calculation is much harder to trace than an if before it.

Angles

Trigonometry works in radians, where a half turn is Pi. DegToRad converts from degrees:

PrintLine("Sin(30 deg)    {}", Sin(DegToRad(30.0)));
PrintLine("Cos(60 deg)    {}", Cos(DegToRad(60.0)));
PrintLine("Sin(Pi)        {}", Sin(Pi));

On paper, sin 30° and cos 60° are both exactly ½, and sin π is exactly 0. The program prints 0.49999999999999994, 0.5000000000000001 and 1.2246467991473532e-16. Nothing is wrong: Pi is the nearest float64 to π, not π itself, and every step rounds to the nearest float.

So a float result is compared within a tolerance, never with ==:

let tolerance = 1e-9;
PrintLine("Sin(Pi) is 0?  {}", Abs(Sin(Pi)) < tolerance);

Rounding, in four senses

"Round" can mean four different things. They agree on most positive numbers and part ways on negative ones — which is where a program usually finds out it chose the wrong one:

let value = -2.5;
PrintLine("Floor(-2.5)    {}   towards minus infinity", Floor(value));
PrintLine("Ceil(-2.5)     {}   towards plus infinity", Ceil(value));
PrintLine("Trunc(-2.5)    {}   towards zero", Trunc(value));
PrintLine("Round(-2.5)    {}   to nearest, halves away from zero", Round(value));
FunctionRounds2.5−2.5
Floordown, towards minus infinity2.0−3.0
Ceilup, towards plus infinity3.0−2.0
Trunctowards zero2.0−2.0
Roundto nearest, halves away from zero3.0−3.0

All four return a float. Trunc is what as does when it converts a float to an integer; to round to the nearest whole number, Round first and convert after.

The program

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

Src/Main.rux
// The arithmetic operators stop at the four operations. Roots, powers, logarithms, angles and
// rounding live in the `Math` package, as functions on `float64` (with `float32` versions too).
//
// Every one of them has a domain, the inputs it has an answer for, and outside it the answer is
// one of the special values from FloatSpecial rather than an error: `Sqrt(-1)` is NaN, `Log(0)` is
// minus infinity. Check the input first when it might fall outside.
//
// And every answer is a float, so it is the nearest float to the true answer, not the answer
// itself. `Sin(Pi)` is not 0, because `Pi` is not exactly pi. Compare results with a tolerance,
// never with `==`.
import Io::PrintLine;
import Math::{ Abs, Cbrt, Ceil, Cos, DegToRad, Exp, Floor, Log, Log10, Log2, Pi, Pow, Round, Sin,
               Sqrt, Trunc };

func Main() -> int {
    // Roots and powers. Sqrt wants x >= 0; Cbrt takes any sign.
    PrintLine("Sqrt(2)        {}", Sqrt(2.0));
    PrintLine("Cbrt(-8)       {}", Cbrt(-8.0));
    PrintLine("Pow(2, 10)     {}", Pow(2.0, 10.0));
    PrintLine("Sqrt(-1)       {}", Sqrt(-1.0));
    PrintLine("");

    // Logarithms want x > 0. `Log` is the natural one; the others say their base.
    PrintLine("Log(Exp(1))    {}", Log(Exp(1.0)));
    PrintLine("Log2(1024)     {}", Log2(1024.0));
    PrintLine("Log10(0.001)   {}", Log10(0.001));
    PrintLine("Log(0)         {}", Log(0.0));
    PrintLine("");

    // Trigonometry works in radians, where a half turn is Pi. DegToRad converts from degrees.
    PrintLine("Sin(30 deg)    {}", Sin(DegToRad(30.0)));
    PrintLine("Cos(60 deg)    {}", Cos(DegToRad(60.0)));
    PrintLine("Sin(Pi)        {}", Sin(Pi));

    // So a float result is compared within a tolerance.
    let tolerance = 1e-9;
    PrintLine("Sin(Pi) is 0?  {}", Abs(Sin(Pi)) < tolerance);
    PrintLine("");

    // Rounding, in its four senses. They differ on negative numbers, which is where a program
    // usually finds out it chose the wrong one.
    let value = -2.5;
    PrintLine("Floor(-2.5)    {}   towards minus infinity", Floor(value));
    PrintLine("Ceil(-2.5)     {}   towards plus infinity", Ceil(value));
    PrintLine("Trunc(-2.5)    {}   towards zero", Trunc(value));
    PrintLine("Round(-2.5)    {}   to nearest, halves away from zero", Round(value));
    return 0;
}

Besides Io, its Rux.toml lists Math under [Dependencies].

Run it

cd Examples/Numbers/Math
rux run
Sqrt(2)        1.4142135623730951
Cbrt(-8)       -2.0
Pow(2, 10)     1024.0
Sqrt(-1)       NaN

Log(Exp(1))    1.0
Log2(1024)     10.0
Log10(0.001)   -3.0
Log(0)         -Inf

Sin(30 deg)    0.49999999999999994
Cos(60 deg)    0.5000000000000001
Sin(Pi)        1.2246467991473532e-16
Sin(Pi) is 0?  true

Floor(-2.5)    -3.0   towards minus infinity
Ceil(-2.5)     -2.0   towards plus infinity
Trunc(-2.5)    -2.0   towards zero
Round(-2.5)    -3.0   to nearest, halves away from zero

Common mistakes

Passing an integer.
Sqrt(2) fails with error: no matching overload for 'Sqrt' with argument types (int), and the notes list the two candidates, Sqrt(x: float32) and Sqrt(x: float64). Write 2.0, or convert a variable with as float64.
Giving degrees to a function that wants radians.
Sin(30.0) is the sine of 30 radians, about -0.988, not ½. Convert first: Sin(DegToRad(30.0)).
Comparing a result with ==.
Cos(DegToRad(60.0)) == 0.5 is false, because the result is 0.5000000000000001. Compare the difference with a tolerance: Abs(result - 0.5) < 1e-9.

Try it yourself

  1. Import Hypot and work out the hypotenuse of a 3–4–5 triangle.
  2. Convert Pi back to degrees with RadToDeg.
  3. Round −2.4 and −2.6 to the nearest whole number and convert them to int32. Then do the same with as alone and compare.
  4. Call Sqrt(2.0f32). How many digits does the float32 version print?

Learn more