Complex
Math ended with Sqrt(-1) giving NaN: no real number squares to a negative one. That is why x * x + 2 * x + 5 = 0 has no real solution, and why a program that solves quadratics has to stop there, or work the answer out by hand. Complex numbers add the missing square root, i, with i * i = -1, and with it every quadratic has its two roots.
They are more than a trick for quadratics. A complex number is also a point in the plane, and multiplying by one turns and stretches — which is why electrical engineering, signal processing and graphics use them all the time.
import Math::{ Complex, DegToRad, Pi, RadToDeg };
A pair of floats
A Complex is real + imaginary·i, held as two public float64 fields, real and imaginary. Complex::New makes one, and Complex32 is the same over float32.
The type defines the four operators, so complex arithmetic reads like any other:
let a = Complex::New(3.0, 4.0);
let b = Complex::New(1.0, -2.0);
Show("a + b", a + b);
Show("a * b", a * b);
Show("a / b", a / b);
Multiplying works out the brackets and uses i * i = -1: (3 + 4i)(1 - 2i) = 3 - 6i + 4i + 8 = 11 - 2i. Both sides of an operator must be Complex; to multiply by a plain float, use Scale.
| Method | Gives | For 3 + 4i |
|---|---|---|
Conjugate() | the imaginary part's sign flipped | 3 - 4i |
Negate() | both signs flipped | -3 - 4i |
Scale(2.0) | both parts times a float | 6 + 8i |
Abs() | the magnitude: the length of the arrow | 5 |
Norm() | the magnitude squared | 25 |
Arg() | the angle, in radians, between −π and π | 0.927… |
A Complex does not implement Display, so the program has a Show function that prints a + bi or a - bi with three decimals.
Length and angle
Draw 3 + 4i as an arrow from the origin to the point (3, 4). Its length is Abs(), and its direction is Arg(), the angle from the positive real axis, in radians:
PrintLine("{:<14} {:.3}", "magnitude", a.Abs());
PrintLine("{:<14} {:.3} degrees", "angle", RadToDeg(a.Arg()));
That is the polar form: 5 long, at about 53.13°. FromPolar(length, angle) goes the other way, so FromPolar(2.0, DegToRad(60.0)) is 1 + 1.732i.
The polar form explains multiplication. Multiplying two complex numbers multiplies their lengths and adds their angles. i is 1 long at 90°, so multiplying by i turns a number a quarter turn anticlockwise and leaves its length alone:
flowchart LR
a["`3 + 4i`<br/>length 5, 53.13°"] -- "times `i`<br/>length 1, 90°" --> t["`-4 + 3i`<br/>length 5, 143.13°"]Solving a quadratic
The quadratic formula needs the square root of the discriminant, q * q - 4 * p * r. For x² + 2x + 5 that is -16, and the real Sqrt gives NaN. Made complex, -16 has the square root 4i:
let root = Complex::New(q * q - 4.0 * p * r, 0.0).Sqrt();
let first = (Complex::New(-q, 0.0) + root).Scale(1.0 / (2.0 * p));
let second = (Complex::New(-q, 0.0) - root).Scale(1.0 / (2.0 * p));
The roots are -1 + 2i and -1 - 2i. They come as a conjugate pair, as the roots of every quadratic with real coefficients do. Putting one back into the equation gives zero.
Sqrt, like Exp, Log and Pow, returns the principal value: of the two square roots of -16, 4i and -4i, it picks the one with a positive real part, or on the imaginary axis the one above zero. That is why the formula adds and subtracts it.
Rounding still applies
Euler's identity says e^(iπ) = -1, and Exp agrees — almost. The imaginary part comes out as about 1.22e-16, because Pi is only the nearest float64 to π. Every float caveat from Math carries over: compare complex results part by part, within a tolerance.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// No real number squares to -1, so `x * x + 1 = 0` has no real solution. Complex numbers add
// one: `i`, with `i * i = -1`. A complex number is a pair, `real + imaginary·i`, and with it
// every quadratic has its two roots, and a rotation in the plane becomes a multiplication.
//
// The Math package's `Complex` holds the pair as two `float64` fields, `real` and `imaginary`,
// and gives it the four operators, so complex arithmetic is written as ordinary arithmetic.
// (`Complex32` is the same over `float32`.)
//
// The pair can also be read as a point in the plane, or as an arrow from the origin to it. The
// arrow's length is the magnitude, `Abs()`, and its direction is the angle, `Arg()`, measured in
// radians from the positive real axis. `FromPolar` builds a number from those two instead.
import Io::PrintLine;
import Math::{ Complex, DegToRad, Pi, RadToDeg };
// A Complex does not implement Display, so `PrintLine("{}", a)` is refused:
//
// error: no matching overload for 'PrintLine' with argument types (char8[..], Complex)
//
// This writes it as `a + bi` or `a - bi` instead.
func Show(label: char8[..], z: Complex) {
if z.imaginary < 0.0 {
PrintLine("{:<14} {:.3} - {:.3}i", label, z.real, -z.imaginary);
} else {
PrintLine("{:<14} {:.3} + {:.3}i", label, z.real, z.imaginary);
}
}
func Main() -> int {
// Arithmetic. Multiplying uses i·i = -1: (3 + 4i)(1 - 2i) = 3 - 6i + 4i + 8 = 11 - 2i.
let a = Complex::New(3.0, 4.0);
let b = Complex::New(1.0, -2.0);
Show("a + b", a + b);
Show("a * b", a * b);
Show("a / b", a / b);
Show("conjugate a", a.Conjugate());
// The operators take a Complex on both sides; a plain float is refused:
//
// error: operator '*' cannot combine left operand 'Complex' with right operand 'float64'
//
// `Scale` multiplies by a real number instead.
Show("a scaled by 2", a.Scale(2.0));
// Polar form: 3 + 4i is 5 long and points about 53 degrees above the real axis.
PrintLine("{:<14} {:.3}", "magnitude", a.Abs());
PrintLine("{:<14} {:.3} degrees", "angle", RadToDeg(a.Arg()));
// Multiplying by i turns a number a quarter turn anticlockwise, keeping its length.
let i = Complex::New(0.0, 1.0);
let turned = a * i;
Show("a * i", turned);
PrintLine("{:<14} {:.3}, {:.3} degrees", "turned polar", turned.Abs(), RadToDeg(turned.Arg()));
// `FromPolar` goes the other way: length 2 at 60 degrees.
Show("polar 2, 60", Complex::FromPolar(2.0, DegToRad(60.0)));
// x^2 + 2x + 5 = 0. The discriminant, 4 - 20, is negative, so the real square root would be
// NaN; the complex one is 4i, and the two roots are -1 + 2i and -1 - 2i.
let p = 1.0;
let q = 2.0;
let r = 5.0;
let root = Complex::New(q * q - 4.0 * p * r, 0.0).Sqrt();
let first = (Complex::New(-q, 0.0) + root).Scale(1.0 / (2.0 * p));
let second = (Complex::New(-q, 0.0) - root).Scale(1.0 / (2.0 * p));
Show("first root", first);
Show("second root", second);
// Putting a root back in gives zero, as far as floats allow.
let check = first * first + first.Scale(q) + Complex::New(r, 0.0);
Show("check", check);
// Euler's identity, e^(i·pi) = -1. The imaginary part comes out as about 1.2e-16, not 0,
// because `Pi` is only the nearest float to pi; at three places it shows as 0.000.
let euler = Complex::New(0.0, Pi).Exp();
Show("e^(i pi)", euler);
PrintLine("{:<14} {}", "imaginary part", euler.imaginary);
return 0;
}
Besides Io, its Rux.toml lists Math under [Dependencies].
Run it
cd Examples/Numbers/Complex
rux run
a + b 4.000 + 2.000i
a * b 11.000 - 2.000i
a / b -1.000 + 2.000i
conjugate a 3.000 - 4.000i
a scaled by 2 6.000 + 8.000i
magnitude 5.000
angle 53.130 degrees
a * i -4.000 + 3.000i
turned polar 5.000, 143.130 degrees
polar 2, 60 1.000 + 1.732i
first root -1.000 + 2.000i
second root -1.000 - 2.000i
check 0.000 + 0.000i
e^(i pi) -1.000 + 0.000i
imaginary part 1.2246467991473532e-16
Common mistakes
Try it yourself
- Solve
x² - 2x + 5 = 0andx² - 5x + 6 = 0. What does the second give for the imaginary parts? - Multiply
abyifour times in a loop, printing each result. Where does it end up? - Print the twelve points
FromPolar(1.0, DegToRad(30.0 * k))forkfrom 0 to 11 — the twelve hours of a clock face. - Write
Close(left: Complex, right: Complex) -> bool, true when both parts differ by less than1e-9, and use it to check thateuleris-1.
Learn more
- Math — the real functions, and their domains
- Operator overload — how a type gets its own
+and* - Float special — the NaN that a real square root of a negative number gives
- Quadratic — a checkpoint project that reads a quadratic and works out its complex roots by hand
- Math in the API reference
- Floating-point types and Operators in the Rux Reference
16.11 Math
Tour the Math package — roots, powers, logarithms, trigonometry and rounding — with the domain of each and the limits of floating-point answers.
Overview
Containers from the Collections package: vectors that grow, arrays sized at run time, double-ended queues, hash and tree maps and sets, priority queues, bit sets, small vectors and LRU caches — and how to choose between them.