Starting Out
Ready, set, go!
OK. Let’s actually do this thing. Pull up a chair. The playground is right there.
The traditional first program in any language is the one that prints Hello, world!. In Rust that looks like this:
Hit ▶ Run. You should see Hello, world! come back in the output below.
Congratulations: you have written and executed a Rust program. Update your
résumé.
Let’s pull this little program apart.
fn main()—fndeclares a function.mainis the magic name Rust looks for when it starts your program. Every executable Rust program has exactly onemain.{and}— the body of the function. Rust uses curly braces, like its C-shaped cousins.println!("Hello, world!");— the bang (!) tells you this isn’t a function, it’s a macro. Macros in Rust are how things likeprintln!can take a variable number of arguments and check the format string at compile time. You’ll meet a few more macros soon:vec!,format!,assert_eq!. The bang is the giveaway.- The semicolon at the end of the line — Rust likes semicolons. It uses them to decide what’s a statement (does a thing) and what’s an expression (produces a value). More on that shortly.
Baby’s first functions
A function in Rust takes some inputs, does some stuff, and optionally returns a value. Let’s write one:
A few things to notice:
x: i32— the parameter name comes first, then a colon, then the type.i32is a 32-bit signed integer. Rust has a family of integer types —i8,i16,i32,i64,i128, plus the unsignedu8throughu128, plusisizeandusizewhich are pointer-sized. For everyday mathi32is fine.-> i32— the return type. The arrow points from the inputs to the output.x * 2— notice no semicolon. This is an expression, and the last expression in a function body becomes the return value. You could also writereturn x * 2;with the semicolon, and Rust will not mind, but old Rustaceans will gently tease you.
That expression-vs-statement business is one of the most distinctive things about Rust. Almost everything is an expression. Look:
if is an expression. It produces a value. So let mood = if … { … } else { … };
just assigns whatever the chosen branch produced. Blocks are expressions, too —
the value of a block is the value of its last expression. Once you internalize
this, a lot of Rust code stops looking weird.
Multiple parameters, multiple returns
You can have as many parameters as you like, separated by commas:
For multiple return values, the move is to return a tuple:
Two new things slipped in there:
&[i32]— a slice ofi32values. A slice is a borrowed view into a sequence of values living somewhere else. We use it here because we don’t want our function to own the array; we just want to look at its contents. We’ll properly meet borrowing in the next chapter.let (small, big) = min_max(&numbers);— that’s destructuring. The left side ofletcan be a pattern, not just a name. We pull the two values out of the tuple in one move.
An intro to lists
Rust calls them vectors, mostly. The type is Vec<T> where T is whatever
kind of thing you’re putting in there. You make one with the vec! macro:
The {:?} formatter is for debug output — handy for inspecting any type
that knows how to print itself for debugging (almost everything in the standard
library does). Use {} for the normal “user-facing” output.
Vectors grow:
Note the mut in let mut letters. Variables in Rust are immutable by default —
yes, even the ones you declare with let. If you want to change a variable
after the fact, you have to say so. This catches an absolutely staggering
number of bugs that would otherwise survive into production.
Take an element out:
Wait, why is it printing Some(40) and not just 40? Because pop() might
return nothing — what if the stack is empty? Rust doesn’t believe in null,
so instead it returns an Option — either Some(value) or None. We’ll
spend a whole chapter on Option later, but mentally, it’s “maybe a value,
maybe not, and the compiler will make you handle both.”
Texas ranges
Sometimes you just want the numbers from 0 to 9, or 1 to 100. Rust has range literals:
Ranges are iterators, which means you can use them anywhere an iterator goes —
for loops, .map(…), .filter(…), the works.
|n| n * n is a closure — an anonymous function. The vertical bars hold the
arguments, and the body is whatever comes after. We’ll get cozy with closures
in the iterators chapter, but they crop up everywhere, so know the shape.
A wee list comprehension
Other languages let you write things like [x*2 for x in xs if x > 0]. Rust
doesn’t have list comprehension syntax, but it has something better: iterator
chains.
Read it like a sentence: “take xs, iterate, keep the positive ones, double each,
collect into a Vec.” You can build up arbitrarily complex transformations like
this and the compiler will fuse them into something tight. There’s no
intermediate vector allocated between filter and map.
The double-ampersand in |&&x| x > 0 is doing some borrow-checker bookkeeping —
iter() yields &i32, and filter’s closure receives a reference to each item,
so the parameter is &&i32, which we destructure into x: i32. If that hurt
your brain, don’t worry — we will properly do battle with references in the
ownership chapter.
Tuples
A tuple is a fixed-size, fixed-type bundle of values. Use them when you want to return two or three things and naming a whole struct would be overkill:
Tuples can mix types, which makes them perfect for “two things that go together
but aren’t the same.” A (String, i32) is fine. A (bool, bool, bool) is fine
but you should probably make a struct.
Speaking of which — that’s a great segue, but first we need to talk about the thing that makes Rust Rust: ownership. Brace yourself.