Skip to content

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:

🦀 main.rs

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()fn declares a function. main is the magic name Rust looks for when it starts your program. Every executable Rust program has exactly one main.
  • { 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 like println! 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:

🦀 main.rs

A few things to notice:

  • x: i32 — the parameter name comes first, then a colon, then the type. i32 is a 32-bit signed integer. Rust has a family of integer types — i8, i16, i32, i64, i128, plus the unsigned u8 through u128, plus isize and usize which are pointer-sized. For everyday math i32 is 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 write return 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:

🦀 main.rs

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:

🦀 main.rs

For multiple return values, the move is to return a tuple:

🦀 main.rs

Two new things slipped in there:

  1. &[i32] — a slice of i32 values. 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.
  2. let (small, big) = min_max(&numbers); — that’s destructuring. The left side of let can 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:

🦀 main.rs

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:

🦀 main.rs

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:

🦀 main.rs

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:

🦀 main.rs

Ranges are iterators, which means you can use them anywhere an iterator goes — for loops, .map(…), .filter(…), the works.

🦀 main.rs

|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.

🦀 main.rs

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:

🦀 main.rs

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.