Scalar Computation And Numeric Basics¶
This module introduces Rust's scalar types and the numeric operations that are
most relevant for small scientific and technical programs. It builds on the
project workflow from learning-modules/getting-started-with-rust-projects.md: each
example is a small Cargo project that can be built, run, inspected, and
modified.
The emphasis is on explicitness. Rust does not silently convert between many numeric types, and that is an important part of how the language avoids ambiguous or accidental computations.
Learning Objectives¶
After completing this module, participants should be able to:
- Recognize common integer, floating-point, Boolean, character, and pointer-size types.
- Explain the difference between signed and unsigned integer types.
- Use integer and floating-point arithmetic operators.
- Explain the difference between integer division and floating-point division.
- Use Euclidean division and remainder for signed integers.
- Call mathematical methods on floating-point values.
- Use floating-point constants from the standard library.
- Convert integer values to floating-point values explicitly.
- Understand why Rust avoids implicit double promotion.
- Use
num-complexfor complex arithmetic.
Prerequisites¶
Participants should already be comfortable with:
- Running a Cargo project with
cargo run. - Checking a project with
cargo check. - Opening and lightly editing
src/main.rs. - Reading simple compiler diagnostics.
The examples used in this module are:
source-code/basic-typessource-code/mathsource-code/numerical-functionsource-code/no-double-promotionsource-code/complex-numbers
Scalar Types¶
Rust has several families of scalar types:
- Signed integers:
i8,i16,i32,i64,i128,isize. - Unsigned integers:
u8,u16,u32,u64,u128,usize. - Floating-point values:
f32,f64. - Booleans:
bool. - Unicode scalar values:
char.
The basic-types example prints the minimum and maximum values for many of
these types:
cd source-code/basic-types
cargo run
For fixed-width integer types, the number in the type name is the number of
bits. For example, i32 is a signed 32-bit integer, and u64 is an unsigned
64-bit integer.
The isize and usize types have the same width as a pointer on the target
platform. They are commonly used for indexing and sizes, especially usize.
Floating-Point Types¶
Rust has two built-in floating-point types:
f32: single precision.f64: double precision.
The basic-types example prints useful associated constants such as:
f32::MINf32::MAXf32::MIN_POSITIVEf32::EPSILONf64::MINf64::MAXf64::MIN_POSITIVEf64::EPSILON
It also shows constants from the standard library, such as:
std::f32::consts::PIstd::f32::consts::FRAC_1_SQRT_2std::f64::consts::Estd::f64::consts::TAU
These constants are namespaced by type, so the f32 and f64 versions are
distinct.
Type Inference And Explicit Types¶
Rust often infers types from context:
let x = 17;
let y = 5.2;
For teaching examples and numerical code, it is often clearer to write the type explicitly when the type matters:
let a: i32 = 17;
let b: i32 = 5;
let x: f64 = 17.3;
let y: f64 = 5.2;
Explicit types are especially useful when comparing integer and floating-point behavior.
Arithmetic Operators¶
The math example illustrates arithmetic for both integers and floating-point
values:
cd source-code/math
cargo run
The main arithmetic operators are:
+for addition.-for subtraction.*for multiplication./for division.%for remainder.
For integers, division discards the fractional part:
let a: i32 = 17;
let b: i32 = 5;
println!("{}", a / b);
println!("{}", a % b);
For floating-point values, division produces a floating-point result:
let x: f64 = 17.3;
let y: f64 = 5.2;
println!("{}", x / y);
println!("{}", x % y);
This is why the example uses different values for integer and floating-point arithmetic: the outputs show that these are related operations, but not the same computation.
Division And Remainder For Negative Integers¶
Signed integer division can be subtle when negative values are involved. Rust's
ordinary / and % operators use truncating division. The math example also
shows Euclidean division:
let a: i32 = -17;
let b: i32 = 5;
println!("{}", a / b);
println!("{}", a % b);
println!("{}", a.div_euclid(b));
println!("{}", a.rem_euclid(b));
For algorithms where the remainder should be non-negative, such as indexing
periodic domains, div_euclid and rem_euclid are often the clearer choice.
Mathematical Functions¶
Floating-point mathematical functions are implemented as methods on f32 and
f64 values.
Examples from source-code/math include:
let angle = std::f64::consts::FRAC_PI_6;
let value = 2.0_f64;
println!("{}", angle.sin());
println!("{}", angle.cos());
println!("{}", angle.tan());
println!("{}", value.sqrt());
println!("{}", value.powi(8));
println!("{}", value.powf(0.5));
println!("{}", value.exp());
println!("{}", value.ln());
println!("{}", value.log10());
Rounding and absolute-value methods include:
let x = -3.75_f64;
println!("{}", x.abs());
println!("{}", x.floor());
println!("{}", x.ceil());
println!("{}", x.round());
println!("{}", x.trunc());
The method-call syntax is important: these are functions associated with the floating-point type and called on a value.
Numeric Functions And Explicit Conversion¶
The numerical-function example defines a small polynomial function:
cd source-code/numerical-function
cargo run -- --help
The core function has typed parameters and a typed return value:
fn polynomial(x: f64, a: f64, b: f64, c: f64) -> f64 {
a * x.powi(2) + b * x + c
}
The example also loops over integer values and converts them to f64 when
constructing floating-point coordinates:
let delta_x = (x_max - x_min) / (nr_points as f64 - 1.0);
for i in 0..nr_points {
let x = x_min + i as f64 * delta_x;
let result = polynomial(x, args.a, args.b, args.c);
println!("{x} {result}");
}
The as f64 conversions are explicit. Rust does not automatically convert an
integer loop index to a floating-point value.
Avoiding Implicit Double Promotion¶
The no-double-promotion example shows that Rust uses context to infer the
type of floating-point literals:
cd source-code/no-double-promotion
cargo run --release
In the example, the function argument is f32:
fn compute_polynom(x: f32) -> f32 {
let a = 3.0;
let b = 2.0;
let c = 1.0;
a * x * x + b * x + c
}
Because the literals are used in an f32 expression, Rust infers them as
f32. This avoids a common problem in C, C++, and Fortran where single
precision values may accidentally be promoted to double precision and then
converted back.
The important lesson is not that Rust guesses magically, but that every expression still has a concrete type. If the context is not clear enough, the compiler will ask for more information.
Complex Numbers¶
Complex numbers are not built into Rust's standard library. Scientific Rust
programs commonly use the num-complex crate.
Run the example with:
cd source-code/complex-numbers
cargo run
The example imports Complex64, which is a complex number with f64 real and
imaginary parts:
use num_complex::Complex64;
let z1 = Complex64 { re: 1.0, im: 2.0 };
let z2 = Complex64 { re: 3.0, im: 4.0 };
println!("{}", z1 + z2);
println!("{}", z1 * z2);
println!("{}", z1.re);
println!("{}", z1.im);
println!("{}", z1.norm());
This example reinforces two earlier points:
- Numeric behavior can be extended through crates.
- External types are brought into scope with
use.
Suggested Hands-On Work¶
Use this sequence as a practical lab.
-
Run
source-code/basic-typesand identify the ranges ofi32,u32,f32, andf64. -
Modify
source-code/basic-types/src/main.rsto print one additional floating-point constant for bothf32andf64. -
Run
source-code/mathand compare integer division with floating-point division. -
Change the integer values in
source-code/math/src/main.rsand predict the result of/,%,div_euclid, andrem_euclidbefore running the code. -
Add one more mathematical function call to
source-code/math, such asvalue.cbrt()orvalue.log2(). -
Run
source-code/numerical-functionwith different polynomial coefficients:
bash
cargo run -- --a 2.0 --b -1.0 --c 0.5
-
Remove one
as f64conversion fromsource-code/numerical-functionand runcargo check. Read the compiler diagnostic, then restore the conversion. -
Run
source-code/no-double-promotionand inspect the printed type names. -
Run
source-code/complex-numbersand add a calculation ofz1 - z2.
Discussion Points¶
This module is a good place to emphasize:
- Rust's numeric types are explicit and concrete.
- Integer and floating-point arithmetic have different semantics.
- Conversions between numeric types should be visible in the code.
- Floating-point constants and mathematical functions are type-specific.
- Scientific code often needs external crates for domain-specific types such as complex numbers.
- The compiler is a useful guide when a numeric expression has an ambiguous or inconsistent type.
Connection To Later Modules¶
The concepts in this module appear throughout the rest of the training:
- Control-flow examples use integer ranges and explicit conversions.
- Iterator examples process numeric collections.
- The Julia set examples use complex arithmetic and floating-point constants.
- The N-body simulation uses vectors of floating-point values, mathematical functions, random initial conditions, and numerical diagnostics.
Once participants are comfortable with scalar values and numeric expressions, they are ready to move on to control flow, functions, and pattern matching.