Introduction to Rust Data types
From Philosophy to Practice
This is Episode 1 of a Rust tutorial blog post beginner series based on my new book ‘Rust the Good Parts’. The pilot episode made the case for why Rust’s guarantees are worth the friction of earning them: the ownership model erases whole categories of memory bugs before a program ever runs, the compiler chooses discipline over after-the-fact tooling, and the language saves you time by catching those bug hardest to trace. None of that becomes real until you have written a value down and watched the compiler hold you to it. That is where this episode begins, working entirely with individual values and closing with a worked problem borrowed from quantitative finance: the Sharpe ratio, computed by hand from six days of returns using nothing but what is covered here. The next episode picks up exactly where this one puts it down, once comparing several strategies side by side becomes the real problem, and structs, ownership, and borrowing stop being abstract.
What This Episode Covers
- Getting, Installing and Using Rust
- Rust’s scalar types, integers, floats, booleans, and characters, and why usize gets a section of its own
- const, const fn, and scoped enums, and why Rust never needed a second, less-checked way to name a fixed value
- Immutability as the language default, mut as the explicit opt-out.
- Expressions, arithmetic, blocks-as-expressions, and shadowing, the mechanism that lets a value’s meaning evolve without inventing a new name every time it does
- A closing challenge, we compute an annualised Sharpe ratio for one strategy’s returns, using only what the episode has covered, entirely inside main
Getting started
Before we can start to write Rust programs, we need to get Rust on your machine. The official tool is rustup, a command-line installer that manages Rust versions and associated tools. Here’s how to get it on Mac and Linux.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
On Windows, download and run rustup‑init.exe from rustup.rs. Once installed, verify everything is in place:
rustc --version
cargo --version
rustc is the compiler. cargo however, is Rust’s build system and package manager. This is what is used it for almost everything Rust.
To create your first project, here is the cargo command:
cargo new hello_world
cd hello_world
This generates two files. Cargo.toml is the manifest with your project’s name, version, and dependencies (none yet). src/main.rs contains a starter program:
fn main() {
println!("Hello, world!");
}
That println! is a macro: the ! tells you it is not a regular function but a macro that expands into Rust code before compilation. What it does is straightforward: it prints a line of text to the terminal, substituting values inside {} braces. Here it simply prints the literal string Hello, world!. The ; ends the statement, just like in C, Java, or JavaScript.
Even though this is a macro call, the mechanics are the same as a function call: you write the name, parentheses, and arguments. The only visible difference is the !. You will see println! constantly through this episode as a way to inspect values, and later episodes cover writing your own functions in full.
Build and run it with one command:
cargo run
You should see Hello, world! printed to the terminal. cargo run compiles the program and executes it in one step. For checking without running, use cargo build; the binary lands in target/debug/.
Every Rust project starts this way: cargo new, edit src/main.rs, cargo run. The rest of this episode assumes you have this working before moving on.
Here’s how it works
As seen in the Getting Started section above, every Rust program has an entry point called main, the function the runtime calls when the program starts:
fn main() {
// where execution begins
}
fn declares a function. main is the name. Parentheses hold parameters (empty for now). Braces hold the body. The // is a comment: everything after it on the same line is ignored. Block comments (/* ... */) exist too, but you’ll reach for // almost every time.
A file with only fn main() {} is a valid, compilable, runnable program. It does nothing and exits cleanly.
All the code in this episode lives inside main. Later you’ll split work across modules and libraries, but for exploring the language itself, main is the only scaffolding you need.
A number that can’t change its mind
Imagine a thermostat. It has a target temperature, a current reading, and a rule: heat turns on when the reading falls below the target. Now imagine that target temperature could be silently overwritten by any part of the system, at any time, without anyone announcing it. Debugging that thermostat would mean asking, at every single line of code, “wait, is the target still what I think it is?”
Most languages make you carry that question around in your head for the whole program. Rust removes it. In Rust, once you write a value down, it stays what you wrote down, unless you explicitly say it’s allowed to change. That one decision shapes almost everything else in this post: how numbers are typed, how constants work, and how an expression like let x = x + 1; can make perfect sense despite looking like nonsense at first glance.
We’re picking this up assuming Rust is already installed and cargo new is familiar. No detours: straight into the language, working entirely inside main for now.
Scalar types
Integers and type suffixes
Whole numbers in Rust come in explicit, fixed widths: i32, u32, i64, usize, and several more. The i or u tells you whether negative values are allowed (signed vs unsigned); the number tells you how many bits it occupies.
let attempts: u32 = 3; // unsigned: never negative
let temperature: i32 = -4; // signed: can be negative
let row_count: usize = 10; // usize: the type Rust reaches for when counting or indexing
usize deserves a special mention. It’s the type of “how many things are there” and “which position am I at”: array lengths, loop counts, indices. You’ll see it constantly, and the reason it exists as its own type (rather than just reusing i32) is that its size is tied to the machine’s pointer width: it’s guaranteed to be big enough to index anything in memory, no more, no less.
If a literal’s type isn’t obvious from context, you can suffix it directly: 5u8, 100i64, 3.0f32. This is rare in everyday code but occasionally the clearest way to remove ambiguity.
Floating-point numbers
For anything with a fractional part, Rust gives you f32 and f64, 32-bit and 64-bit floating point. f64 is the default, and unless you have a specific reason to save memory, it’s the one to reach for.
let reading = 21.6; // inferred as f64
let precise: f32 = 21.6; // explicitly the smaller type
Putting it together: Using println!
The println! macro has a small formatting language worth learning early, because “print this number nicely” and “print this number’s raw representation” are genuinely different tasks:
fn main {
let reading = 21.6234;
println!("{reading}"); // 21.6234: raw
println!("{reading:.1}"); // 21.6: one decimal place
println!("{reading:>8.1}"); // right-aligned in an 8-character field
}
That {:.1} isn’t decoration: it’s an instruction to the formatter, and you’ll use it constantly the moment your numbers come from a calculation rather than a hand-typed literal.
Booleans
bool in Rust is exactly two values: true and false. Nothing else silently converts into one: there’s no “any nonzero number counts as true” shortcut anywhere in the language. If you want a boolean, you write something that produces one:
let target = 21.0;
let reading = 19.5;
let heater_on = reading < target; // bool: true
Characters and Unicode
char in Rust is a single Unicode scalar value, always four bytes wide, which is why it holds '£' or '€' just as comfortably as 'A': there’s no separate “wide character” type hiding behind it.
let unit: char = '°';
println!("Reading: 21.6{unit}C");
Immutability by default
Here is the idea the thermostat story was setting up. Every let binding is immutable unless you say otherwise:
let target = 21.0;
// target = 22.0; // error: cannot assign twice to immutable variable
let mut reading = 19.5;
reading = 20.1; // fine: explicitly opted into mutability with `mut`
This isn’t a restriction Rust reluctantly imposes on you: it’s the default state of every value, and you have to actively opt out of it with mut. Compare that to languages where every variable is mutable by default and discipline is what keeps values from changing unexpectedly: here, the discipline is enforced by the compiler, not by your memory of which values you promised yourself not to touch.
References inherit the same idea. Borrowing a value with & gives you read-only access by default:
let target = 21.0;
let view = ⌖
// *view = 22.0; // error: cannot assign through `&` reference
A writable reference, &mut T, exists, but it’s a separate, explicit thing you ask for, and while one is alive, the compiler guarantees nothing else can read or write that value through any other path at the same time. That’s a stronger promise than “please don’t change this.” It’s “it is currently impossible to change this from anywhere else.”
Constants with const
Some values aren’t just “currently not being changed”: they’re never going to change, by nature, and it’s worth telling the compiler that directly:
const FREEZING_POINT_C: f64 = 0.0;
const MAX_SENSORS: u32 = 16;
A const is always evaluated at compile time, always explicitly typed, and always scoped exactly like any other item: visible where you declare it visible, checked by the type system like everything else. There’s no separate textual-substitution step living outside the language that you have to remember to avoid using; const (and, for grouped whole-number values, enum) are the tools. There isn’t a second, worse option sitting next to them.
You can even compute a constant from other constants, entirely before your program runs, using a const fn:
const fn seconds_in(hours: u32) -> u32 {
hours * 3600
}
const SHIFT_LENGTH_SECS: u32 = seconds_in(8); // computed at compile time: 28800
Nothing about seconds_in executes when your program starts. The compiler works it out while it’s still compiling: SHIFT_LENGTH_SECS is just the number 28800, already sitting in the binary.
Rust function primer
Before expressions do their work, it helps to see how Rust wraps work into reusable units. A function is declared with fn, followed by its name, parameters with their types, and an optional return type:
fn celsius_to_fahrenheit(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
Parameters are written name: type, always explicitly. The return type follows ->. The function’s body is a block expression: whatever the last line evaluates to (without a semicolon) becomes the return value. That c * 9.0 / 5.0 + 32.0 has no semicolon, so it’s the return value. No return keyword needed.
For early returns, return works as expected:
fn validate_temperature(c: f64) -> bool {
if c.is_nan() {
return false;
}
c > -273.15
}
A function that doesn’t need to return a meaningful value omits the -> and returns (), the unit type, Rust’s equivalent of “void”:
fn log_reading(c: f64) {
println!("{c:.1}°C");
}
Once you’ve written a function, you call it the way you’d expect: celsius_to_fahrenheit(21.0). The key thing to hold onto is that every function body is an expression, which connects directly to what comes next.
Values also carry their own built-in operations, accessed with the dot syntax: value.method(). You already saw c.is_nan() above: is_nan is a method on every f64 value that checks whether the number is Not-a-Number. Floating-point numbers also give you .powi(n) for integer powers and .sqrt() for square roots. These are not standalone functions you call with the value as an argument; they are operations attached to the value itself, written after a dot. The compiler resolves which method to run based on the value’s type, which is why .sqrt() on an f64 works without you importing anything.
Throughout this episode you will see methods called on individual values: .powi(2), .sqrt(), .is_finite(), and the pattern is always the same: the value, a dot, the method name, and parentheses with any arguments.
Expressions and arithmetic
let bindings and type inference
Most of the bindings above didn’t carry an explicit type, and that was fine: Rust looks at how a value is used and infers its type without you spelling it out:
let count = 3; // inferred as i32, Rust's default integer type
let ratio = 0.5; // inferred as f64
You can annotate a binding when the inference genuinely needs help, or just for clarity: let count: u32 = 3;. Most of the time, though, you’ll let the compiler do this work.
Arithmetic and compound assignment
Ordinary arithmetic behaves exactly as expected: + - * /, plus compound forms like +=:
let mut total = 0;
total += 3;
total += 5;
One thing to flag early: Rust never silently mixes integer and floating-point types in the same expression. If you have a usize and need it in a floating-point calculation, you convert explicitly with as:
let count: usize = 6;
let average = 18.0 / count as f64;
That as f64 isn’t ceremony. It’s the exact point where you’re telling the compiler “yes, I mean for this count to be treated as a float here”, rather than the compiler silently guessing on your behalf and potentially hiding a mistake.
Comparison and logical operators
Comparisons (>, <, ==, and friends) produce bool values, which you can then combine with && (and) and || (or):
let reading = 21.6;
let in_range = reading > 18.0 && reading < 24.0;
Rust also gives you methods on floating-point values worth knowing about here, is_finite(), is_nan(), which matter the moment a value could come from a division:
let ratio = 4.0 / 0.0;
println!("{}", ratio.is_finite()); // false: division by zero produced infinity
The operator landscape
Beyond arithmetic and comparison, Rust has bitwise operators (& | ^ << >>) for working directly with bits, and the usual precedence rules you’d expect from any C-family language. Parentheses always win; when in doubt, use them: a clear expression is worth more than a clever one.
Blocks are expressions
This is one of the more distinctive things about Rust, and it’s worth sitting with. A { ... } block is not just a grouping construct: it evaluates to a value, specifically whatever its final line produces, as long as that final line has no trailing semicolon:
let category = {
let midpoint = (18.0 + 24.0) / 2.0;
if midpoint > 20.0 { "warm side" } else { "cool side" }
};
Everything inside those braces is scratch work. midpoint doesn’t exist outside the block; only the final expression escapes and becomes the value of category. This means if itself is an expression too: the pattern let x = if cond { a } else { b }; is completely ordinary Rust, not a special case.
Shadowing a variable
Here’s a pattern that looks odd the first time you see it and becomes indispensable once you’re used to it. You’re allowed to declare a new let using the same name as an existing one:
let reading = "21.6"; // starts life as text, from some input source
let reading: f64 = reading.trim().parse().unwrap(); // now it's a number
let reading = reading.round(); // now it's rounded
None of these lines mutate the previous reading: each let creates a brand-new binding that happens to reuse the name, quietly retiring the one before it. The type is even allowed to change between shadows, which mut alone could never do (mut fixes a variable’s type for its entire lifetime; shadowing doesn’t). What’s really happening is that the value’s meaning is evolving: from “raw text,” to “a parsed number,” to “a rounded number”, and the name evolves with it instead of forcing you to invent reading_str, reading_num, reading_rounded.
Internalize: shadowing creates a new variable and retires the old one; it never involves
mut, and it’s the right tool whenever a value’s meaning genuinely changes partway through a computation.
Throwing a value away
Occasionally a computation produces something you don’t need. An unused let binding earns a compiler warning by default: Rust is nudging you to explain yourself. Prefixing the name with an underscore acknowledges that on purpose:
let _unused_remainder = total % 3;
References, ownership, and holding data together
Everything so far has been individual values: a single reading, a single count. Real programs almost immediately need to hold several related values together, and to pass them around without needlessly copying them. Rust’s answer to “how do I pass this without duplicating it” is one of its defining ideas: you choose between owning a value (moving it), borrowing it read-only (&T), or borrowing it exclusively and writably (&mut T). There’s no separate convention for “small values” versus “large values,” no rule of thumb to memorize: the type signature itself tells you which mode you’re in.
fn is_high(reading: &f64) -> bool { // borrows: doesn't take ownership
*reading > 24.0
}
We’ll go much further into this: structs, slices, iterators, and what it really means to own a piece of data, in Part 2. For now, hold onto the shape of the idea: own, borrow, or mutably borrow, and the compiler enforces whichever one you picked.
The challenge: your first Sharpe ratio
Time to put every one of these ideas to work on something with real stakes. In quantitative finance, the Sharpe ratio answers a question that sounds simple and isn’t: given a strategy’s returns, how much reward did it deliver per unit of risk taken? A strategy that earns 8% steadily can beat one that earns 28% erratically, once risk is priced in.
Sharpe = average(excess return) / spread(excess return)
where “excess return” means return above a safe baseline (the risk-free rate), and “spread” means standard deviation: how much the daily numbers bounce around their own average.
Here’s the challenge: using only what you’ve learned in this post: scalar types, const, immutability, let, arithmetic, blocks as expressions, and shadowing, compute the Sharpe ratio for six days of returns, entirely inside main. No structs, no vectors, no functions beyond main itself. Try it before reading on.
A note before you start: the solution uses .powi(2) (raise to an integer power), .sqrt() (square root), and .is_finite() (check for infinity): all method calls on f64 values, as introduced in the function primer above. These are built-in operations on the value type itself.
Here’s one way to solve it
fn main() {
const TRADING_DAYS: usize = 252;
const RISK_FREE_ANNUAL: f64 = 0.05;
let risk_free_daily = RISK_FREE_ANNUAL / TRADING_DAYS as f64;
// Six days of returns for one strategy.
let r1 = 0.0041;
let r2 = -0.0018;
let r3 = 0.0026;
let r4 = 0.0003;
let r5 = -0.0007;
let r6 = 0.0052;
// Mean return, then shadowed into mean *excess* return.
let mean_return = (r1 + r2 + r3 + r4 + r5 + r6) / 6.0;
let mean_return = mean_return - risk_free_daily;
// Variance: six squared deviations from the mean, averaged.
let variance = ((r1 - mean_return).powi(2)
+ (r2 - mean_return).powi(2)
+ (r3 - mean_return).powi(2)
+ (r4 - mean_return).powi(2)
+ (r5 - mean_return).powi(2)
+ (r6 - mean_return).powi(2))
/ 6.0;
let std_dev = variance.sqrt();
let sharpe_ratio = {
let scale = (TRADING_DAYS as f64).sqrt();
(mean_return / std_dev) * scale
};
let is_usable = sharpe_ratio.is_finite() && std_dev > 0.0;
println!("Mean excess daily return : {:.4}%", mean_return * 100.0);
println!("Daily volatility : {:.4}%", std_dev * 100.0);
println!("Annualised Sharpe ratio : {sharpe_ratio:.3}");
println!("Ratio usable? : {is_usable}");
}
Here is how this code works from the first line to the last.
The program starts with two constants. TRADING_DAYS is set to 252, the standard number of trading days in a year, and its type is usize: the unsigned integer type Rust uses for counting and indexing. RISK_FREE_ANNUAL is the annual risk-free rate, set to 5%. Both are const, meaning they are evaluated at compile time, always immutable, and their values are baked directly into the binary. The compiler does not allocate memory for them at runtime; wherever TRADING_DAYS appears in the code, the number 252 is already there.
The first computation converts the annual risk-free rate into a daily rate. RISK_FREE_ANNUAL is an f64 but TRADING_DAYS is usize, and Rust never mixes numeric types implicitly. The as f64 cast converts the usize to f64 so the division is legal. The result, risk_free_daily, is a tiny number, roughly 0.000198, representing the daily return a risk-free asset would deliver.
Next come six individual variables: r1 through r6. Each holds one day’s return for a hypothetical strategy, expressed as a decimal. Day one gained 0.41%, day two lost 0.18%, and so on. Every one of these is a let binding, immutable by default. None of them will change after this line: each day’s number is fixed, which is exactly what you want when the data is historical.
The mean return is computed by adding all six daily returns together and dividing by 6.0. The addition and division are ordinary arithmetic operators: + and /, and the result is an f64 inferred by the compiler. Then the variable mean_return is shadowed: a new let binding with the same name replaces the old one. The old value was the raw average of the six returns; the new value subtracts risk_free_daily from that average, producing the mean excess return: how much the strategy earned above the risk-free baseline on an average day. Shadowing is essential here: the meaning of mean_return has changed, and the code makes that visible by replacing the binding rather than mutating it.
The variance calculation is the densest part. Each of the six returns has its deviation from the mean computed (r1 - mean_return), squared with .powi(2), and then all six squared deviations are summed. .powi(2) is a method on f64 that raises the value to an integer power: it is faster than the general .powf() because the exponent is known at compile time. The sum of squared deviations is divided by 6.0 to produce the variance, which is the average squared distance of each return from the mean. Every term in this expression is computed with arithmetic operators and method calls, and every intermediate value is a temporary that exists only within the expression: no mutable variable accumulates a running total.
The standard deviation std_dev is computed by calling .sqrt() on the variance. .sqrt() is another f64 method that returns the square root. Standard deviation is measured in the same units as the original returns, which makes it interpretable directly: a daily volatility of roughly 0.0025 means the strategy’s daily returns typically deviate from the mean by about 0.25 percentage points.
The Sharpe ratio is computed inside its own block. The block contains a local variable scale, which is the square root of TRADING_DAYS, approximately 15.87. This annualises the daily Sharpe ratio, converting “excess return per unit of daily risk” into “excess return per unit of annual risk,” which is the conventional way the ratio is quoted. Because scale lives inside the block, it does not exist outside it; the block’s final expression, (mean_return / std_dev) * scale, is the block’s value, and that value is bound to sharpe_ratio. The scratch variable scale disappears as soon as the block finishes: it can never accidentally be used elsewhere.
The last computation before printing is is_usable. This is a boolean that checks two conditions: sharpe_ratio.is_finite() returns false if the ratio is infinity or NaN (which could happen if std_dev is zero, meaning no variance at all), and std_dev > 0.0 guards against degenerate cases where the variance is exactly zero. Both conditions must be true for is_usable to be true.
Finally, four println! calls print the results. {:.4} formats a floating-point number to four decimal places. {:>8.1} would right-align in an 8-character field with one decimal place, but the final code keeps things simple. The curly braces in {sharpe_ratio:.3} and {is_usable} are direct variable interpolations: a shorthand that avoids passing the variable as a separate argument. The % signs and labels are just literal text inside the format strings.
Every binding in this program is immutable. No variable changes value after its let. The only data flow is from one binding to the next through arithmetic expressions, method calls, and the block expression that scopes scale out of existence. The compiler checked every type, every cast, and every method call before the program ever ran, and what you get out is exactly what the math promises, with no mutable state to audit, no variable that could have been changed between the line where it was set and the line where it was read.
Coming up next
That challenge worked for one strategy and six numbers. Now imagine three strategies, five years of daily returns each: thousands of values, and a genuine need to compare them side by side rather than copy-pasting this program three times with different variable names.
What you actually want is a way to say “here is one strategy” as a single bundled thing, and a way to hold many of those together for comparison. That’s a struct, and a vector of structs, and the moment data starts being shared and compared rather than owned by a single block of code, you run straight into the question this post only gestured at: who owns this data, and who’s just borrowing it? That’s Episode 2.
Get the Book
Computer programming doesn’t have to be so hard, does it? It can be as easy as A, B, C. As 1, 2, 3. As “doh,” “ray,” “mi.” As data, control, and structures.
Don’t worry. My new book has you covered, and guides you through these concepts step by step. You can learn about data and control structures in one week, function structures by week two, and by the end of week four you will have built a complete working system, building a game from scratch.
References
[1] The Rust Programming Language, “Common Programming Concepts”, https://doc.rust-lang.org/book/ch03-00-common-programming-concepts.html
[2] The Rust Reference, “Constant Evaluation”, https://doc.rust-lang.org/reference/const_eval.html
[3] The Rust Standard Library, std::cell module (Cell, RefCell, OnceCell), https://doc.rust-lang.org/std/cell/
[4] The Rust Reference, “The must_use Attribute”, https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-must_use-attribute
[5] Sharpe, W. F., “Mutual Fund Performance,” Journal of Business, 1966; revised as “The Sharpe Ratio,” Journal of Portfolio Management, 1994.
[6] Rust RFC 2000, “Const Generics”, https://rust-lang.github.io/rfcs/2000-const-generics.html
Glossary
- Scalar type: a type holding a single value: an integer, a float, a boolean, or a character.
- usize: an unsigned integer type sized to guarantee it can index anything in memory on the current machine. Rust’s default type for counting and indexing.
- const: a named, typed value evaluated entirely at compile time, scoped like any other item.
- const fn: a function the compiler can fully evaluate at compile time when called from a const context.
- Shadowing: declaring a new binding with the same name as an existing one, retiring the old one without using mut. Unlike mut, shadowing may change the binding’s type.
- Interior mutability: a pattern where a value is read-only from the outside, accessed through &self, but permits controlled internal mutation, made explicit through types like
Cell<T>, RefCell<T>, or OnceCell<T>. - Slice (&[T]): a non-owning, read-only view over a contiguous run of values, regardless of whether they live in a Vec, an array, or elsewhere.
- #[must_use]: an attribute that produces a compiler warning if a function’s return value is silently discarded.
- Sharpe ratio: the average excess return of a strategy, return above a risk-free baseline, divided by the standard deviation of those returns. A measure of return earned per unit of risk taken.