A Gentle Introduction To Rust
Why learn a new Programming Language?
The aim of this tutorial is to take you to a place where you can read and write enough Rust to fully appreciate the excellent learning resources available online, in particular The Book. It's an opportunity to try before you buy, and get enough feeling for the power of the language to want to go deeper.
As Einstein might have said, "As gentle as possible, but no gentler.". There is a lot of new stuff to learn here, and it's different enough to require some rearrangement of your mental furniture. By 'gentle' I mean that the features are presented practically with examples; as we encounter difficulties, I hope to show how Rust solves these problems. It is important to understand the problems before the solutions make sense. To put it in flowery language, we are going for a hike in hilly country and I will point out some interesting rock formations on the way, with only a few geology lectures. There will be some uphill but the view will be inspiring; the community is unusually pleasant and happy to help. There is the Rust Users Forum and an active subreddit which is unusually well-moderated. The FAQ is a good resource if you have specific questions.
First, why learn a new programming language? It is an investment of time and energy and that needs some justification. Even if you do not immediately land a cool job using that language, it stretches the mental muscles and makes you a better programmer. That seems a poor kind of return-on-investment but if you're not learning something genuinely new all the time then you will stagnate and be like the person who has ten years of experience in doing the same thing over and over.
Where Rust Shines
Rust is a statically and strongly typed systems programming language. statically means that all types are known at compile-time, strongly means that these types are designed to make it harder to write incorrect programs. A successful compilation means you have a much better guarantee of correctness than with a cowboy language like C. systems means generating the best possible machine code with full control of memory use. So the uses are pretty hardcore: operating systems, device drivers and embedded systems that might not even have an operating system. However, it's actually a very pleasant language to write normal application code in as well.
The big difference from C and C++ is that Rust is safe by default; all memory accesses are checked. It is not possible to corrupt memory by accident.
The unifying principles behind Rust are:
- strictly enforcing safe borrowing of data
- functions, methods and closures to operate on data
- tuples, structs and enums to aggregate data
- pattern matching to select and destructure data
- traits to define behaviour on data
There is a fast-growing ecosystem of available libraries through Cargo
but here we will concentrate on the core principles of the language
by learning to use the standard library. My advice is to write lots of small programs,
so learning to use rustc
directly is a core skill. When doing the examples in this
tutorial I defined a little script called rrun
which does a compilation and runs
the result:
rustc $1.rs && ./$1
Setting Up
This tutorial assumes that you have Rust installed locally. Fortunately this is very straightforward.
$ curl https://sh.rustup.rs -sSf | sh
$ rustup component add rust-docs
I would recommend getting the default stable version; it's easy to download unstable versions later and to switch between.
This gets the compiler, the Cargo package manager, the API documentation, and the Rust Book. The journey of a thousand miles starts with one step, and this first step is painless.
rustup
is the command you use to manage your Rust installation. When a new stable release
appears, you just have to say rustup update
to upgrade. rustup doc
will open
the offline documentation in your browser.
You will probably already have an editor you like, and basic Rust support is good. I'd suggest you start out with basic syntax highlighting at first, and work up as your programs get larger.
Personally I'm a fan of Geany which is one of the few editors with Rust support out-of-the-box; it's particularly easy on Linux since it's available through the package manager, but it works fine on other platforms.
The main thing is knowing how to edit, compile and run Rust programs. You learn to program with your fingers; type in the code yourself, and learn to rearrange things efficiently with your editor.
Zed Shaw's advice about learning to program in Python remains good, whatever the language. He says learning to program is like learning a musical instrument - the secret is practice and persistence. There's also good advice from Yoga and the soft martial arts like Tai Chi; feel the strain, but don't over-strain.
I'd like to thank the many contributors who caught bad English or bad Rust for me, and thanks to David Marino for his cool characterization of Rust as a friendly-but-hardcore no-nonsense knight in shining armour.
Steve Donovan © 2017-2018 MIT license version 0.4.0
Basics
Hello, World!
The original purpose of "hello world", ever since the first C version was written, was to test the compiler and run an actual program.
// hello.rs fn main() { println!("Hello, World!"); }
$ rustc hello.rs
$ ./hello
Hello, World!
Rust is a curly-braces language with semicolons, C++-style comments and a main
function - so far, so familiar. The exclamation mark indicates that this is a
macro call.
For anybody else, it's probably "Great, now I have to remember when to say bang!". However, the compiler is unusually helpful; if you leave out that exclamation, you get:
error[E0425]: unresolved name `println`
--> hello2.rs:2:5
|
2 | println("Hello, World!");
| ^^^^^^^ did you mean the macro `println!`?
Learning a language means getting comfortable with its errors. Try to see the compiler as a strict but friendly helper rather than a computer shouting at you, because you are going to see a lot of red ink in the beginning. It's much better for the compiler to catch you out than for your program to blow up in front of actual humans.
The next step is to introduce a variable:
// let1.rs fn main() { let answer = 42; println!("Hello {}", answer); }
Spelling mistakes are compile errors, not runtime errors like with dynamic languages like Python or JavaScript. This will save you a lot of stress later! And if I wrote 'answr' instead of 'answer', the compiler is actually nice about it:
4 | println!("Hello {}", answr);
| ^^^^^ did you mean `answer`?
The println!
macro takes a format string
and some values; it's very similar to the formatting used by Python 3.
Another very useful macro is assert_eq!
. This is the workhorse of testing
in Rust; you assert that two things must be equal, and if not, panic.
// let2.rs fn main() { let answer = 42; assert_eq!(answer,42); }
Which won't produce any output. But change 42 to 40:
thread 'main' panicked at
'assertion failed: `(left == right)` (left: `42`, right: `40`)',
let2.rs:4
note: Run with `RUST_BACKTRACE=1` for a backtrace.
And that's our first runtime error in Rust.
Looping and Ifing
Anything interesting can be done more than once:
// for1.rs fn main() { for i in 0..5 { println!("Hello {}", i); } }
The range is not inclusive, so i
goes from 0 to 4. This is convenient in a
language which indexes things like arrays from 0.
And interesting things have to be done conditionally:
// for2.rs fn main() { for i in 0..5 { if i % 2 == 0 { println!("even {}", i); } else { println!("odd {}", i); } } }
even 0
odd 1
even 2
odd 3
even 4
i % 2
is zero if 2 can divide into i
cleanly; Rust uses C-style operators.
There are no brackets around the condition, just like in Go, but
you must use curly brackets around the block.
This does the same, written in a more interesting way:
// for3.rs fn main() { for i in 0..5 { let even_odd = if i % 2 == 0 {"even"} else {"odd"}; println!("{} {}", even_odd, i); } }
Traditionally, programming languages have statements (like if
) and
expressions (like 1+i
). In Rust, nearly everything has a value and can
be an expression. The seriously ugly C 'ternary operator' i % 2 == 0 ? "even" : "odd"
is not needed.
Note that there aren't any semi-colons in those blocks!
Adding Things Up
Computers are very good at arithmetic. Here is a first attempt at adding all the numbers from 0 to 4:
// add1.rs fn main() { let sum = 0; for i in 0..5 { sum += i; } println!("sum is {}", sum); }
But it fails to compile:
error[E0384]: re-assignment of immutable variable `sum`
--> add1.rs:5:9
3 | let sum = 0;
| --- first assignment to `sum`
4 | for i in 0..5 {
5 | sum += i;
| ^^^^^^^^ re-assignment of immutable variable
'Immutable'? A variable that cannot vary? let
variables by default can only
be assigned a value when declared. Adding the magic word mut
(please make
this variable mutable) does the trick:
// add2.rs fn main() { let mut sum = 0; for i in 0..5 { sum += i; } println!("sum is {}", sum); }
This can be puzzling when coming from other languages, where variables can be re-written by default. What makes something a 'variable' is that it gets assigned a computed value at run-time - it is not a constant. It is also how the word is used in mathematics, like when we say 'let n be the largest number in set S'.
There is a reason for declaring variables to be read-only by default. In a larger program, it gets hard to track where writes are taking place. So Rust makes things like mutability ('write-ability') explicit. There's a lot of cleverness in the language, but it tries not to hide anything.
Rust is both statically-typed and strongly-typed - these are often confused, but think of C (statically but weakly typed) and Python (dynamically but strongly typed). In static types the type is known at compile time, and dynamic types are only known at run time.
At the moment, however, it feels like Rust is hiding those types from you. What
exactly is the type of i
? The compiler can work it out, starting with 0,
with type inference, and comes up with i32
(four byte signed integer.)
Let's make exactly one change - turn that 0
into 0.0
. Then we get errors:
error[E0277]: the trait bound `{float}: std::ops::AddAssign<{integer}>` is not satisfied
--> add3.rs:5:9
|
5 | sum += i;
| ^^^^^^^^ the trait `std::ops::AddAssign<{integer}>` is not implemented for `{float}`
|
Ok, so the honeymoon is over: what does this mean? Each operator (like +=
) corresponds
to a trait, which is like an abstract interface that must be implemented for each concrete type.
We'll deal with traits in detail later, but here all you need to know is that
AddAssign
is the name of the trait implementing the +=
operator, and the error is saying
that floating point numbers do not implement this operator for integers. (The full list of
operator traits is here.)
Again, Rust likes to be explicit - it will not silently convert that integer into a float for you.
We have to cast that value to a floating-point value explicitly.
// add3.rs fn main() { let mut sum = 0.0; for i in 0..5 { sum += i as f64; } println!("sum is {}", sum); }
Function Types are Explicit
Functions are one place where the compiler will not work out types for you. And this in fact was a deliberate decision, since languages like Haskell have such powerful type inference that there are hardly any explicit type names. It's actually good Haskell style to put in explicit type signatures for functions. Rust requires this always.
Here is a simple user-defined function:
// fun1.rs fn sqr(x: f64) -> f64 { return x * x; } fn main() { let res = sqr(2.0); println!("square is {}", res); }
Rust goes back to an older style of argument declaration, where the type follows the name. This is how it was done in Algol-derived languages like Pascal.
Again, no integer-to-float conversions - if you replace the 2.0
with 2
then we
get a clear error:
8 | let res = sqr(2);
| ^ expected f64, found integral variable
|
You will actually rarely see functions written using a return
statement. More
often, it will look like this:
# #![allow(unused_variables)] # #fn main() { fn sqr(x: f64) -> f64 { x * x } #}
This is because the body of the function (inside {}
) has the value of its
last expression, just like with if-as-an-expression.
Since semicolons are inserted semi-automatically by human fingers, you might add it here and get the following error:
|
3 | fn sqr(x: f64) -> f64 {
| ^ expected f64, found ()
|
= note: expected type `f64`
= note: found type `()`
help: consider removing this semicolon:
--> fun2.rs:4:8
|
4 | x * x;
| ^
The ()
type is the empty type, nada, void
, zilch, nothing. Everything in Rust
has a value, but sometimes it's just nothing. The compiler knows this is
a common mistake, and actually helps you.
A few more examples of this no-return expression style:
# #![allow(unused_variables)] # #fn main() { // absolute value of a floating-point number fn abs(x: f64) -> f64 { if x > 0.0 { x } else { -x } } // ensure the number always falls in the given range fn clamp(x: f64, x1: f64, x2: f64) -> f64 { if x < x1 { x1 } else if x > x2 { x2 } else { x } } #}
It's not wrong to use return
, but code is cleaner without it. You will still
use return
for returning early from a function.
Some operations can be elegantly expressed recursively:
# #![allow(unused_variables)] # #fn main() { fn factorial(n: u64) -> u64 { if n == 0 { 1 } else { n * factorial(n-1) } } #}
This can be a little strange at first, and the best thing is then to use pencil and paper and work out some examples. It isn't usually the most efficient way to do that operation however.
Values can also be passed by reference. A reference is created by &
and dereferenced
by *
.
fn by_ref(x: &i32) -> i32{ *x + 1 } fn main() { let i = 10; let res1 = by_ref(&i); let res2 = by_ref(&41); println!("{} {}", res1,res2); } // 11 42
What if you want a function to modify one of its arguments? Enter mutable references:
// fun4.rs fn modifies(x: &mut f64) { *x = 1.0; } fn main() { let mut res = 0.0; modifies(&mut res); println!("res is {}", res); }
This is more how C would do it than C++. You have to explicitly pass the
reference (with &
) and explicitly dereference with *
. And then throw in mut
because it's not the default. (I've always felt that C++ references are
too easy to miss compared to C.)
Basically, Rust is introducing some friction here, and not-so-subtly pushing
you towards returning values from functions directly. Fortunately, Rust has
powerful ways to express things like "operation succeeded and here's the result"
so &mut
isn't needed that often. Passing by reference is important when we have a
large object and don't wish to copy it.
The type-after-variable style applies to let
as well, when you really want to nail
down the type of a variable:
# #![allow(unused_variables)] # #fn main() { let bigint: i64 = 0; #}
Learning Where to Find the Ropes
It's time to start using the documentation. This will be installed on your machine,
and you can use rustup doc --std
to open it in a browser.
Note the search field at the top, since this is going to be your friend; it operates completely offline.
Let's say we want to see where the mathematical functions are, so search for 'cos'. The first two hits show it defined for both single and double-precision floating point numbers. It is defined on the value itself as a method, like so:
# #![allow(unused_variables)] # #fn main() { let pi: f64 = 3.1416; let x = pi/2.0; let cosine = x.cos(); #}
And the result will be sort-of zero; we obviously need a more authoritative source of pi-ness!
(Why do we need an explicit f64
type? Because without it, the constant could
be either f32
or f64
, and these are very different.)
Let me quote the example given for cos
, but written as a complete program
( assert!
is a cousin of assert_eq!
; the expression must be true):
fn main() { let x = 2.0 * std::f64::consts::PI; let abs_difference = (x.cos() - 1.0).abs(); assert!(abs_difference < 1e-10); }
std::f64::consts::PI
is a mouthful! ::
means much the same as it does in C++,
(often written using '.' in other languages) - it is a fully qualified name. We get
this full name from the second hit on searching for PI
.
Up to now, our little Rust programs have been free of all that import
and
include
stuff that tends to slow down the discussion of 'Hello World' programs.
Let's make this program more readable with a use
statement:
use std::f64::consts; fn main() { let x = 2.0 * consts::PI; let abs_difference = (x.cos() - 1.0).abs(); assert!(abs_difference < 1e-10); }
Why haven't we needed to do this up to now?
This is because Rust helpfully makes a lot of basic functionality visible without
explicit use
statements through the Rust prelude.
Arrays and Slices
All statically-typed languages have arrays, which are values packed nose to tail in memory. Arrays are indexed from zero:
// array1.rs fn main() { let arr = [10, 20, 30, 40]; let first = arr[0]; println!("first {}", first); for i in 0..4 { println!("[{}] = {}", i,arr[i]); } println!("length {}", arr.len()); }
And the output is:
first 10
[0] = 10
[1] = 20
[2] = 30
[3] = 40
length 4
In this case, Rust knows exactly how big the array is and if you try to
access arr[4]
it will be a compile error.
 We will
come to the Rust equivalent of List
soon, but arrays are not the droids you're looking
for; they are fixed in size. They can be mutable (if we ask nicely) but you
cannot add new elements.
Arrays are not used that often in Rust, because the type of an array includes its
size. The type of the array in the example is
[i32; 4]
; the type of [10, 20]
would be [i32; 2]
and so forth: they
have different types. So they are bastards to pass around as
function arguments.
What are used often are slices. You can think of these as views into an underlying array of values. They otherwise behave very much like an array, and know their size, unlike those dangerous animals C pointers.
Note two important things here - how to write a slice's type, and that
you have to use &
to pass it to the function.
// array2.rs // read as: slice of i32 fn sum(values: &[i32]) -> i32 { let mut res = 0; for i in 0..values.len() { res += values[i] } res } fn main() { let arr = [10,20,30,40]; // look at that & let res = sum(&arr); println!("sum {}", res); }
Ignore the code of sum
for a while, and look at &[i32]
. The relationship between
Rust arrays and slices is similar to that between C arrays and pointers, except for
two important differences - Rust slices keep track of their size (and will
panic if you try to access outside that size) and you have to explicitly say that
you want to pass an array as a slice using the &
operator.
A C programmer pronounces &
as 'address of'; a Rust programmer pronounces it
'borrow'. This is going to be the key word when learning Rust. Borrowing is the name
given to a common pattern in programming; whenever you pass something by reference
(as nearly always happens in dynamic languages) or pass a pointer in C. Anything
borrowed remains owned by the original owner.
Slicing and Dicing
You cannot print out an array in the usual way with {}
but you can do a debug
print with {:?}
.
// array3.rs fn main() { let ints = [1, 2, 3]; let floats = [1.1, 2.1, 3.1]; let strings = ["hello", "world"]; let ints_ints = [[1, 2], [10, 20]]; println!("ints {:?}", ints); println!("floats {:?}", floats); println!("strings {:?}", strings); println!("ints_ints {:?}", ints_ints); }
Which gives:
ints [1, 2, 3]
floats [1.1, 2.1, 3.1]
strings ["hello", "world"]
ints_ints [[1, 2], [10, 20]]
So, arrays of arrays are no problem, but the important thing is that an array contains values of only one type. The values in an array are arranged next to each other in memory so that they are very efficient to access.
If you are curious about the actual types of these variables, here is a useful trick. Just declare a variable with an explicit type which you know will be wrong:
# #![allow(unused_variables)] # #fn main() { let var: () = [1.1, 1.2]; #}
Here is the informative error:
3 | let var: () = [1.1, 1.2];
| ^^^^^^^^^^ expected (), found array of 2 elements
|
= note: expected type `()`
= note: found type `[{float}; 2]`
({float}
means 'some floating-point type which is not fully specified yet')
Slices give you different views of the same array:
// slice1.rs fn main() { let ints = [1, 2, 3, 4, 5]; let slice1 = &ints[0..2]; let slice2 = &ints[1..]; // open range! println!("ints {:?}", ints); println!("slice1 {:?}", slice1); println!("slice2 {:?}", slice2); }
ints [1, 2, 3, 4, 5]
slice1 [1, 2]
slice2 [2, 3, 4, 5]
This is a neat notation which looks similar to Python slices but with a big difference: a copy of the data is never made. These slices all borrow their data from their arrays. They have a very intimate relationship with that array, and Rust spends a lot of effort to make sure that relationship does not break down.
Optional Values
Slices, like arrays, can be indexed. Rust knows the size of an array at
compile-time, but the size of a slice is only known at run-time. So s[i]
can
cause an out-of-bounds error when running and will panic. This is really not
what you want to happen - it can be the difference between a safe launch abort and
scattering pieces of a very expensive satellite all over Florida. And there are
no exceptions.
Let that sink in, because it comes as a shock. You cannot wrap dodgy-may-panic code in some try-block and 'catch the error' - at least not in a way you'd want to use every day. So how can Rust be safe?
There is a slice method get
which does not panic. But what does it return?
// slice2.rs fn main() { let ints = [1, 2, 3, 4, 5]; let slice = &ints; let first = slice.get(0); let last = slice.get(5); println!("first {:?}", first); println!("last {:?}", last); } // first Some(1) // last None
last
failed (we forgot about zero-based indexing), but returned something called None
.
first
is fine, but appears as a value wrapped in Some
. Welcome to the Option
type! It may be either Some
or None
.
 But at least you can call
is_some
first to make sure - for instance, if you had a distinct no-value default:
# #![allow(unused_variables)] # #fn main() { let maybe_last = slice.get(5); let last = if maybe_last.is_some() { *maybe_last.unwrap() } else { -1 }; #}
Note the *
- the precise type inside the Some
is &i32
, which is a reference. We need
to dereference this to get back to a i32
value.
Which is long-winded, so there's a shortcut - unwrap_or
will return the value it
is given if the Option
was None
. The types must match up - get
returns
a reference. so you have to make up a &i32
with &-1
. Finally, again use *
to get the value as i32
.
# #![allow(unused_variables)] # #fn main() { let last = *slice.get(5).unwrap_or(&-1); #}
It's easy to miss the &
, but the compiler has your back here. If it was -1
,
rustc
says 'expected &{integer}, found integral variable' and then
'help: try with &-1
'.
You can think of Option
as a box which may contain a value, or nothing (None
).
(It is called Maybe
in Haskell). It may contain any kind of value, which is
its type parameter. In this case, the full type is Option<&i32>
, using
C++-style notation for generics. Unwrapping this box may cause an explosion,
but unlike Schroedinger's Cat, we can know in advance if it contains a value.
It is very common for Rust functions/methods to return such maybe-boxes, so learn how to use them comfortably.
Vectors
We'll return to slice methods again, but first: vectors. These are re-sizeable
arrays and behave much like Python List
and C++ std::vector
. The Rust type
Vec
(pronounced 'vector') behaves very much like an slice in fact; the
difference is that you can append extra values to a vector - note that it must
be declared as mutable.
// vec1.rs fn main() { let mut v = Vec::new(); v.push(10); v.push(20); v.push(30); let first = v[0]; // will panic if out-of-range let maybe_first = v.get(0); println!("v is {:?}", v); println!("first is {}", first); println!("maybe_first is {:?}", maybe_first); } // v is [10, 20, 30] // first is 10 // maybe_first is Some(10)
A common beginner mistake is to forget the mut
; you will get a helpful error
message:
3 | let v = Vec::new();
| - use `mut v` here to make mutable
4 | v.push(10);
| ^ cannot borrow mutably
There is a very intimate relation between vectors and slices:
// vec2.rs fn dump(arr: &[i32]) { println!("arr is {:?}", arr); } fn main() { let mut v = Vec::new(); v.push(10); v.push(20); v.push(30); dump(&v); let slice = &v[1..]; println!("slice is {:?}", slice); }
That little, so-important borrow operator &
is coercing the vector into a
slice. And it makes complete sense, because the vector is also looking after an array of
values, with the difference that the array is allocated dynamically.
If you come from a dynamic language, now is time for that little talk. In systems languages, program memory comes in two kinds: the stack and the heap. It is very fast to allocate data on the stack, but the stack is limited; typically of the order of megabytes. The heap can be gigabytes, but allocating is relatively expensive, and such memory must be freed later.  Once the system is sure that data is no longer referenced by other data, it goes back into the pool of available memory.
Generally, this is a price worth paying. Playing with the stack is terribly unsafe, because if you make one mistake you can override the return address of the current function, and you die an ignominious death or (worse) got pwned by some guy living in his Mom's basement in Minsk.
The first C program I wrote (on an DOS PC) took out the whole computer. Unix systems always behaved better, and only the process died with a segfault. Why is this worse than a Rust (or Go) program panicking? Because a panic happens when the original problem happens, not when the program has become hopelessly confused and eaten all your homework. Panics are memory safe because they happen before any illegal access to memory. This is a common cause of security problems in C, because all memory accesses are unsafe and a cunning attacker can exploit this weakness.
Panicking sounds desperate and unplanned, but Rust panics are structured - the stack is unwound just as with exceptions. All allocated objects are dropped, and a backtrace is generated.
The downsides of garbage collection? The first is that it is wasteful of memory, which matters in those small embedded microchips which increasingly rule our world. The second is that it will decide, at the worst possible time, that a clean up must happen now. (The Mom analogy is that she wants to clean your room when you are at a delicate stage with a new lover). Those embedded systems need to respond to things when they happen ('real-time') and can't tolerate unscheduled outbreaks of cleaning. Roberto Ierusalimschy, the chief designer of Lua (one of the most elegant dynamic languages ever) said that he would not like to fly on an airplane that relied on garbage-collected software.
Back to vectors: when a vector is modified or created, it allocates from the heap and becomes the owner of that memory. The slice borrows the memory from the vector. When the vector dies or drops, it lets the memory go.
Iterators
We have got so far without mentioning a key part of the Rust puzzle - iterators.
The for-loop over a range was using an iterator (0..n
is actually similar to the
Python 3 range
function).
An iterator is easy to define informally. It is an 'object' with a next
method
which returns an Option
. As long as that value is not None
, we keep calling
next
:
// iter1.rs fn main() { let mut iter = 0..3; assert_eq!(iter.next(), Some(0)); assert_eq!(iter.next(), Some(1)); assert_eq!(iter.next(), Some(2)); assert_eq!(iter.next(), None); }
And that is exactly what for var in iter {}
does.
This may seem an inefficient way to define a for-loop, but rustc
does crazy-ass
optimizations in release mode and it will be just as fast as a while
loop.
Here is the first attempt to iterate over an array:
// iter2.rs fn main() { let arr = [10, 20, 30]; for i in arr { println!("{}", i); } }
which fails, but helpfully:
4 | for i in arr {
| ^ the trait `std::iter::Iterator` is not implemented for `[{integer}; 3]`
|
= note: `[{integer}; 3]` is not an iterator; maybe try calling
`.iter()` or a similar method
= note: required by `std::iter::IntoIterator::into_iter`
Following rustc
's advice, the following program works as expected.
// iter3.rs fn main() { let arr = [10, 20, 30]; for i in arr.iter() { println!("{}", i); } // slices will be converted implicitly to iterators... let slice = &arr; for i in slice { println!("{}", i); } }
In fact, it is more efficient to iterate over an array or slice this way
than to use for i in 0..slice.len() {}
because Rust does not have to obsessively
check every index operation.
We had an example of summing up a range of integers earlier. It involved a mut
variable and a loop. Here's the idiomatic, pro-level way of doing the sum:
// sum1.rs fn main() { let sum: i32 = (0..5).sum(); println!("sum was {}", sum); let sum: i64 = [10, 20, 30].iter().sum(); println!("sum was {}", sum); }
Note that this is one of those cases where you need to be explicit about the type of the variable, since otherwise Rust doesn't have enough information. Here we do sums with two different integer sizes, no problem. (It is also no problem to create a new variable of the same name if you run out of names to give things.)
With this background, some more of the slice methods will make more sense. (Another documentation tip; on the right-hand side of every doc page there's a '[-]' which you can click to collapse the method list. You can then expand the details of anything that looks interesting. Anything that looks too weird, just ignore for now.)
The windows
method gives you an iterator of slices - overlapping windows of
values!
// slice4.rs fn main() { let ints = [1, 2, 3, 4, 5]; let slice = &ints; for s in slice.windows(2) { println!("window {:?}", s); } } // window [1, 2] // window [2, 3] // window [3, 4] // window [4, 5]
Or chunks
:
# #![allow(unused_variables)] # #fn main() { for s in slice.chunks(2) { println!("chunks {:?}", s); } // chunks [1, 2] // chunks [3, 4] // chunks [5] #}
More about vectors...
There is a useful little macro vec!
for initializing a vector. Note that you
can remove values from the end of a vector using pop
, and extend a vector
using any compatible iterator.
// vec3.rs fn main() { let mut v1 = vec![10, 20, 30, 40]; v1.pop(); let mut v2 = Vec::new(); v2.push(10); v2.push(20); v2.push(30); assert_eq!(v1, v2); v2.extend(0..2); assert_eq!(v2, &[10, 20, 30, 0, 1]); }
Vectors compare with each other and with slices by value.
You can insert values into a vector at arbitrary positions with insert
,
and remove with remove
. This is not as efficient as pushing and popping since
the values will have to be moved to make room, so watch out for these operations on big
vectors.
Vectors have a size and a capacity. If you clear
a vector, its size becomes zero,
but it still retains its old capacity. So refilling it with push
, etc only requires
reallocation when the size gets larger than that capacity.
Vectors can be sorted, and then duplicates can be removed - these operate in-place
on the vector. (If you want to make a copy first, use clone
.)
// vec4.rs fn main() { let mut v1 = vec![1, 10, 5, 1, 2, 11, 2, 40]; v1.sort(); v1.dedup(); assert_eq!(v1, &[1, 2, 5, 10, 11, 40]); }
Strings
Strings in Rust are a little more involved than in other languages; the String
type,
like Vec
, allocates dynamically and is resizeable. (So it's like C++'s std::string
but not like the immutable strings of Java and Python.) But a program may contain a lot
of string literals (like "hello") and a system language should be able to store
these statically in the executable itself. In embedded micros, that could mean putting
them in cheap ROM rather than expensive RAM (for low-power devices, RAM is
also expensive in terms of power consumption.) A system language has to have
two kinds of string, allocated or static.
So "hello" is not of type String
. It is of type &str
(pronounced 'string slice').
It's like the distinction between const char*
and std::string
in C++, except
&str
is much more intelligent. In fact, &str
and String
have a very
similar relationship to each other as do &[T]
to Vec<T>
.
// string1.rs fn dump(s: &str) { println!("str '{}'", s); } fn main() { let text = "hello dolly"; // the string slice let s = text.to_string(); // it's now an allocated string dump(text); dump(&s); }
Again, the borrow operator can coerce String
into &str
, just as Vec<T>
could
be coerced into &[T]
.
Under the hood, String
is basically a Vec<u8>
and &str
is &[u8]
, but
those bytes must represent valid UTF-8 text.
Like a vector, you can push
a character and pop
one off the end of String
:
// string5.rs fn main() { let mut s = String::new(); // initially empty! s.push('H'); s.push_str("ello"); s.push(' '); s += "World!"; // short for `push_str` // remove the last char s.pop(); assert_eq!(s, "Hello World"); }
You can convert many types to strings using to_string
(if you can display them with '{}' then they can be converted).
The format!
macro is a very useful way to build
up more complicated strings using the same format strings as println!
.
// string6.rs fn array_to_str(arr: &[i32]) -> String { let mut res = '['.to_string(); for v in arr { res += &v.to_string(); res.push(','); } res.pop(); res.push(']'); res } fn main() { let arr = array_to_str(&[10, 20, 30]); let res = format!("hello {}", arr); assert_eq!(res, "hello [10,20,30]"); }
Note the &
in front of v.to_string()
- the operator is defined on a string
slice, not a String
itself, so it needs a little persuasion to match.
The notation used for slices works with strings as well:
// string2.rs fn main() { let text = "static"; let string = "dynamic".to_string(); let text_s = &text[1..]; let string_s = &string[2..4]; println!("slices {:?} {:?}", text_s, string_s); } // slices "tatic" "na"
But, you cannot index strings! This is because they use the One True Encoding, UTF-8, where a 'character' may be a number of bytes.
// string3.rs fn main() { let multilingual = "Hi! ¡Hola! привет!"; for ch in multilingual.chars() { print!("'{}' ", ch); } println!(""); println!("len {}", multilingual.len()); println!("count {}", multilingual.chars().count()); let maybe = multilingual.find('п'); if maybe.is_some() { let hi = &multilingual[maybe.unwrap()..]; println!("Russian hi {}", hi); } } // 'H' 'i' '!' ' ' '¡' 'H' 'o' 'l' 'a' '!' ' ' 'п' 'р' 'и' 'в' 'е' 'т' '!' // len 25 // count 18 // Russian hi привет!
Now, let that sink in - there are 25 bytes, but only 18 characters! However, if
you use a method like find
, you will get a valid index (if found) and then
any slice will be fine.
(The Rust char
type is a 4-byte Unicode code point. Strings are not arrays
of chars!)
String slicing may explode like vector indexing, because it uses byte offsets. In this case, the string consists of two bytes, so trying to pull out the first byte is a Unicode error. So be careful to only slice strings using valid offsets that come from string methods.
# #![allow(unused_variables)] # #fn main() { let s = "¡"; println!("{}", &s[0..1]); <-- bad, first byte of a multibyte character #}
Breaking up strings is a popular and useful pastime. The string split_whitespace
method returns an iterator, and we then choose what to do with it. A common need
is to create a vector of the split substrings.
collect
is very general and so needs some clues about what it is collecting - hence
the explicit type.
# #![allow(unused_variables)] # #fn main() { let text = "the red fox and the lazy dog"; let words: Vec<&str> = text.split_whitespace().collect(); // ["the", "red", "fox", "and", "the", "lazy", "dog"] #}
You could also say it like this, passing the iterator into the extend
method:
# #![allow(unused_variables)] # #fn main() { let mut words = Vec::new(); words.extend(text.split_whitespace()); #}
In most languages, we would have to make these separately allocated strings, whereas here each slice in the vector is borrowing from the original string. All we allocate is the space to keep the slices.
Have a look at this cute two-liner; we get an iterator over the chars,
and only take those characters which are not space. Again, collect
needs
a clue (we may have wanted a vector of chars, say):
# #![allow(unused_variables)] # #fn main() { let stripped: String = text.chars() .filter(|ch| ! ch.is_whitespace()).collect(); // theredfoxandthelazydog #}
The filter
method takes a closure, which is Rust-speak for
lambdas or anonymous functions. Here the argument type is clear from the
context, so the explicit rule is relaxed.
Yes, you can do this as an explicit loop over chars, pushing the returned slices into a mutable vector, but this is shorter, reads well (when you are used to it, of course) and just as fast. It is not a sin to use a loop, however, and I encourage you to write that version as well.
Interlude: Getting Command Line Arguments
Up to now our programs have lived in blissful ignorance of the outside world; now it's time to feed them data.
std::env::args
is how you access command-line arguments; it returns an iterator
over the arguments as strings, including the program name.
// args0.rs fn main() { for arg in std::env::args() { println!("'{}'", arg); } }
src$ rustc args0.rs
src$ ./args0 42 'hello dolly' frodo
'./args0'
'42'
'hello dolly'
'frodo'
Would it have been better to return a Vec
? It's easy enough to use collect
to
make that vector, using the iterator skip
method to move past the program
name.
# #![allow(unused_variables)] # #fn main() { let args: Vec<String> = std::env::args().skip(1).collect(); if args.len() > 0 { // we have args! ... } #}
Which is fine; it's pretty much how you would do it in most languages.
A more Rust-y approach to reading a single argument (together with parsing an integer value):
// args1.rs use std::env; fn main() { let first = env::args().nth(1).expect("please supply an argument"); let n: i32 = first.parse().expect("not an integer!"); // do your magic }
nth(1)
gives you the second value of the iterator, and expect
is like an unwrap
with a readable message.
Converting a string into a number is straightforward, but you do need to specify
the type of the value - how else could parse
know?
This program can panic, which is fine for dinky test programs. But don't get too comfortable with this convenient habit.
Matching
The code in string3.rs
where we extract the Russian greeting is not how it would
be usually written. Enter match:
# #![allow(unused_variables)] # #fn main() { match multilingual.find('п') { Some(idx) => { let hi = &multilingual[idx..]; println!("Russian hi {}", hi); }, None => println!("couldn't find the greeting, Товарищ") }; #}
match
consists of several patterns with a matching value following the fat arrow,
separated by commas. It has conveniently unwrapped the value from the Option
and
bound it to idx
. You must specify all the possibilities, so we have to handle
None
.
Once you are used to it (and by that I mean, typed it out in full a few times) it
feels more natural than the explicit is_some
check which needed an extra
variable to store the Option
.
But if you're not interested in failure here, then if let
is your friend:
# #![allow(unused_variables)] # #fn main() { if let Some(idx) = multilingual.find('п') { println!("Russian hi {}", &multilingual[idx..]); } #}
This is convenient if you want to do a match and are only interested in one possible result.
match
can also operate like a C switch
statement, and like other Rust constructs
can return a value:
# #![allow(unused_variables)] # #fn main() { let text = match n { 0 => "zero", 1 => "one", 2 => "two", _ => "many", }; #}
The _
is like C default
- it's a fall-back case. If you don't provide one then
rustc
will consider it an error. (In C++ the best you can expect is a warning, which
says a lot about the respective languages).
Rust match
statements can also match on ranges. Note that these ranges have
three dots and are inclusive ranges, so that the first condition would match 3.
# #![allow(unused_variables)] # #fn main() { let text = match n { 0...3 => "small", 4...6 => "medium", _ => "large", }; #}
Reading from Files
The next step to exposing our programs to the world is to reading files.
Recall that expect
is like unwrap
but gives a custom error message. We are
going to throw away a few errors here:
// file1.rs use std::env; use std::fs::File; use std::io::Read; fn main() { let first = env::args().nth(1).expect("please supply a filename"); let mut file = File::open(&first).expect("can't open the file"); let mut text = String::new(); file.read_to_string(&mut text).expect("can't read the file"); println!("file had {} bytes", text.len()); }
src$ file1 file1.rs
file had 366 bytes
src$ ./file1 frodo.txt
thread 'main' panicked at 'can't open the file: Error { repr: Os { code: 2, message: "No such file or directory" } }', ../src/libcore/result.rs:837
note: Run with `RUST_BACKTRACE=1` for a backtrace.
src$ file1 file1
thread 'main' panicked at 'can't read the file: Error { repr: Custom(Custom { kind: InvalidData, error: StringError("stream did not contain valid UTF-8") }) }', ../src/libcore/result.rs:837
note: Run with `RUST_BACKTRACE=1` for a backtrace.
So open
can fail because the file doesn't exist or we aren't allowed to read it,
and read_to_string
can fail because the file doesn't contain valid UTF-8. (Which is
fair enough, you can use read_to_end
and put the contents into a vector of bytes
instead.) For files that aren't too big, reading them in one gulp is useful and
straightforward.
If you know anything about file handling in other languages, you may wonder when
the file is closed. If we were writing to this file, then not closing it could
result in loss of data.
But the file here is closed when the function ends and the file
variable is dropped.
This 'throwing away errors' thing is getting too much of a habit. You do not
want to put this code into a function, knowing that it could so easily crash
the whole program. So now we have to talk about exactly what File::open
returns.
If Option
is a value that may contain something or nothing, then Result
is a value
that may contain something or an error. They both understand unwrap
(and its cousin
expect
) but they are quite different. Result
is defined by two type parameters,
for the Ok
value and the Err
value.
The Result
'box' has two compartments, one labelled Ok
and the other Err
.
fn good_or_bad(good: bool) -> Result<i32,String> { if good { Ok(42) } else { Err("bad".to_string()) } } fn main() { println!("{:?}",good_or_bad(true)); //Ok(42) println!("{:?}",good_or_bad(false)); //Err("bad") match good_or_bad(true) { Ok(n) => println!("Cool, I got {}",n), Err(e) => println!("Huh, I just got {}",e) } // Cool, I got 42 }
(The actual 'error' type is arbitrary - a lot of people use strings until they are comfortable with Rust error types.) It's a convenient way to either return one value or another.
This version of the file reading function does not crash. It returns a Result
and
it is the caller who must decide how to handle the error.
// file2.rs use std::env; use std::fs::File; use std::io::Read; use std::io; fn read_to_string(filename: &str) -> Result<String,io::Error> { let mut file = match File::open(&filename) { Ok(f) => f, Err(e) => return Err(e), }; let mut text = String::new(); match file.read_to_string(&mut text) { Ok(_) => Ok(text), Err(e) => Err(e), } } fn main() { let file = env::args().nth(1).expect("please supply a filename"); let text = read_to_string(&file).expect("bad file man!"); println!("file had {} bytes", text.len()); }
The first match safely extracts the value from Ok
, which
becomes the value of the match. If it's Err
it returns the error,
rewrapped as an Err
.
The second match returns the string, wrapped up as an Ok
, otherwise
(again) the error. The actual value in the Ok
is unimportant, so we ignore
it with _
.
This is not so pretty; when most of a function is error handling, then the 'happy path' gets lost. Go tends to have this problem, with lots of explicit early returns, or just ignoring errors. (That is, by the way, the closest thing to evil in the Rust universe.)
Fortunately, there is a shortcut.
The std::io
module defines a type alias io::Result<T>
which is exactly
the same as Result<T,io::Error>
and easier to type.
# #![allow(unused_variables)] # #fn main() { fn read_to_string(filename: &str) -> io::Result<String> { let mut file = File::open(&filename)?; let mut text = String::new(); file.read_to_string(&mut text)?; Ok(text) } #}
That ?
operator does almost exactly what the match on File::open
does;
if the result was an error, then it will immediately return that error.
Otherwise, it returns the Ok
result.
At the end, we still need to wrap up the string as a result.
2017 was a good year for Rust, and ?
was one of the cool things that
became stable. You will still see the macro try!
used in older code:
# #![allow(unused_variables)] # #fn main() { fn read_to_string(filename: &str) -> io::Result<String> { let mut file = try!(File::open(&filename)); let mut text = String::new(); try!(file.read_to_string(&mut text)); Ok(text) } #}
In summary, it's possible to write perfectly safe Rust that isn't ugly, without needing exceptions.
Structs, Enums and Matching
Rust likes to Move It, Move It
I'd like to move back a little, and show you something surprising:
// move1.rs fn main() { let s1 = "hello dolly".to_string(); let s2 = s1; println!("s1 {}", s1); }
And we get the following error:
error[E0382]: use of moved value: `s1`
--> move1.rs:5:22
|
4 | let s2 = s1;
| -- value moved here
5 | println!("s1 {}", s1);
| ^^ value used here after move
|
= note: move occurs because `s1` has type `std::string::String`,
which does not implement the `Copy` trait
Rust has different behaviour than other languages. In a language where variables are
always references (like Java or Python), s2
becomes yet another reference to the
string object referenced by s1
. In C++, s1
is a value, and it is copied to s2
.
But Rust moves the value. It doesn't see strings as copyable
("does not implement the Copy trait").
We would not see this with 'primitive' types like numbers, since they are just values;
they are allowed to be copyable because they are cheap to copy. But String
has allocated
memory containing "Hello dolly", and copying will involve allocating some more memory
and copying the characters. Rust will not do this silently.
Consider a String
containing the whole text of 'Moby-Dick'. It's not a big struct,
just has the address in memory of the text, its size, and how big the allocated block is.
Copying this is going to be expensive, because that memory is allocated on the heap and
the copy will need its own allocated block.
String
| addr | ---------> Call me Ishmael.....
| size | |
| cap | |
|
&str |
| addr | -------------------|
| size |
f64
| 8 bytes |
The second value is a string slice (&str
) which refers to the same memory as the string,
with a size - just the guy's name. Cheap to copy!
The third value is an f64
- just 8 bytes. It does not refer to any other memory, so
it's just as cheap to copy as to move.
Copy
values are only defined by their representation in memory, and when
Rust copies, it just copies those bytes elsewhere. Similarly, a non-Copy
value
is also just moved. There is no cleverness in copying and moving, unlike in C++.
Re-writing with a function call reveals exactly the same error:
// move2.rs fn dump(s: String) { println!("{}", s); } fn main() { let s1 = "hello dolly".to_string(); dump(s1); println!("s1 {}", s1); // <---error: 'value used here after move' }
Here, you have a choice. You may pass a reference to that string, or
explicitly copy it using its clone
method. Generally, the first is the better way
to go.
fn dump(s: &String) { println!("{}", s); } fn main() { let s1 = "hello dolly".to_string(); dump(&s1); println!("s1 {}", s1); }
The error goes away. But you'll rarely see a plain
String
reference like this, since to pass a string literal is really ugly and involves
creating a temporary string.
# #![allow(unused_variables)] # #fn main() { dump(&"hello world".to_string()); #}
So altogether the best way to declare that function is:
# #![allow(unused_variables)] # #fn main() { fn dump(s: &str) { println!("{}", s); } #}
And then both dump(&s1)
and dump("hello world")
work properly. (Here Deref
coercion kicks in and Rust will convert &String
to &str
for you.)
To summarise, assignment of a non-Copy value moves the value from one location to another. Otherwise, Rust would be forced to implicitly do a copy and break its promise to make allocations explicit.
Scope of Variables
So, the rule of thumb is to prefer to keep references to the original data - to 'borrow' it.
But a reference must not outlive the owner!
First, Rust is a block-scoped language. Variables only exist for the duration of their block:
# #![allow(unused_variables)] # #fn main() { { let a = 10; let b = "hello"; { let c = "hello".to_string(); // a,b and c are visible } // the string c is dropped // a,b are visible for i in 0..a { let b = &b[1..]; // original b is no longer visible - it is shadowed. } // the slice b is dropped // i is _not_ visible! } #}
Loop variables (like i
) are a little different, they are only visible in the loop
block. It is not an error to create a new variable using the same name ('shadowing')
but it can be confusing.
When a variable 'goes out of scope' then it is dropped. Any memory used is reclaimed,
and any other resources owned by that variable are given back to the system - for
instance, dropping a File
closes it. This is a Good Thing. Unused resources are
reclaimed immediately when not needed.
(A further Rust-specific issue is that a variable may appear to be in scope, but its value has moved.)
Here a reference rs1
is made to a value tmp
which only lives for the duration
of its block:
01 // ref1.rs 02 fn main() { 03 let s1 = "hello dolly".to_string(); 04 let mut rs1 = &s1; 05 { 06 let tmp = "hello world".to_string(); 07 rs1 = &tmp; 08 } 09 println!("ref {}", rs1); 10 }
We borrow the value of s1
and then borrow the value of tmp
. But tmp
's value
does not exist outside that block!
error: `tmp` does not live long enough
--> ref1.rs:8:5
|
7 | rs1 = &tmp;
| --- borrow occurs here
8 | }
| ^ `tmp` dropped here while still borrowed
9 | println!("ref {}", rs1);
10 | }
| - borrowed value needs to live until here
Where is tmp
? Gone, dead, gone back to the Big Heap in the Sky: dropped.
Rust is here saving you from the dreaded 'dangling pointer' problem of C -
a reference that points to stale data.
Tuples
It's sometimes very useful to return multiple values from a function. Tuples are a convenient solution:
// tuple1.rs fn add_mul(x: f64, y: f64) -> (f64,f64) { (x + y, x * y) } fn main() { let t = add_mul(2.0,10.0); // can debug print println!("t {:?}", t); // can 'index' the values println!("add {} mul {}", t.0,t.1); // can _extract_ values let (add,mul) = t; println!("add {} mul {}", add,mul); } // t (12, 20) // add 12 mul 20 // add 12 mul 20
Tuples may contain different types, which is the main difference from arrays.
# #![allow(unused_variables)] # #fn main() { let tuple = ("hello", 5, 'c'); assert_eq!(tuple.0, "hello"); assert_eq!(tuple.1, 5); assert_eq!(tuple.2, 'c'); #}
They appear in some Iterator
methods. enumerate
is like the Python generator
of the same name:
# #![allow(unused_variables)] # #fn main() { for t in ["zero","one","two"].iter().enumerate() { print!(" {} {};",t.0,t.1); } // 0 zero; 1 one; 2 two; #}
zip
combines two iterators into a single iterator of
tuples containing the values from both:
# #![allow(unused_variables)] # #fn main() { let names = ["ten","hundred","thousand"]; let nums = [10,100,1000]; for p in names.iter().zip(nums.iter()) { print!(" {} {};", p.0,p.1); } // ten 10; hundred 100; thousand 1000; #}
Structs
Tuples are convenient, but saying t.1
and keeping track of the meaning of each part
is tedious for anything that isn't straightforward.
Rust structs contain named fields:
// struct1.rs struct Person { first_name: String, last_name: String } fn main() { let p = Person { first_name: "John".to_string(), last_name: "Smith".to_string() }; println!("person {} {}", p.first_name,p.last_name); }
The values of a struct will be placed next to each other in memory, although you should not assume any particular memory layout, since the compiler will organize the memory for efficiency, not size, and there may be padding.
Initializing this struct is a bit clumsy, so we want to move the construction of a Person
into its own function. This function can be made into an associated function of Person
by putting
it into a impl
block:
// struct2.rs struct Person { first_name: String, last_name: String } impl Person { fn new(first: &str, name: &str) -> Person { Person { first_name: first.to_string(), last_name: name.to_string() } } } fn main() { let p = Person::new("John","Smith"); println!("person {} {}", p.first_name,p.last_name); }
There is nothing magic or reserved about the name new
here. Note that it's accessed
using a C++-like notation using double-colon ::
.
Here's a Person
method , that takes a reference self argument:
# #![allow(unused_variables)] # #fn main() { impl Person { ... fn full_name(&self) -> String { format!("{} {}", self.first_name, self.last_name) } } ... println!("fullname {}", p.full_name()); // fullname John Smith #}
The self
is used explicitly and is passed as a reference.
(You can think of &self
as short for self: &Person
.)
The keyword Self
refers to the struct type - you can mentally substitute Person
for Self
here:
# #![allow(unused_variables)] # #fn main() { fn copy(&self) -> Self { Self::new(&self.first_name,&self.last_name) } #}
Methods may allow the data to be modified using a mutable self argument:
# #![allow(unused_variables)] # #fn main() { fn set_first_name(&mut self, name: &str) { self.first_name = name.to_string(); } #}
And the data will move into the method when a plain self argument is used:
# #![allow(unused_variables)] # #fn main() { fn to_tuple(self) -> (String,String) { (self.first_name, self.last_name) } #}
(Try that with &self
- structs will not let go of their data without a fight!)
Note that after v.to_tuple()
is called, then v
has moved and is no longer
available.
To summarize:
- no
self
argument: you can associate functions with structs, like thenew
"constructor". &self
argument: can use the values of the struct, but not change them&mut self
argument: can modify the valuesself
argument: will consume the value, which will move.
If you try to do a debug dump of a Person
, you will get an informative error:
error[E0277]: the trait bound `Person: std::fmt::Debug` is not satisfied
--> struct2.rs:23:21
|
23 | println!("{:?}", p);
| ^ the trait `std::fmt::Debug` is not implemented for `Person`
|
= note: `Person` cannot be formatted using `:?`; if it is defined in your crate,
add `#[derive(Debug)]` or manually implement it
= note: required by `std::fmt::Debug::fmt`
The compiler is giving advice, so we put #[derive(Debug)]
in front of Person
, and now
there is sensible output:
Person { first_name: "John", last_name: "Smith" }
The directive makes the compiler generate a Debug
implementation, which is very
helpful. It's good practice to do this for your structs, so they can be
printed out (or written as a string using format!
). (Doing so by default would be
very un-Rustlike.)
Here is the final little program:
// struct4.rs use std::fmt; #[derive(Debug)] struct Person { first_name: String, last_name: String } impl Person { fn new(first: &str, name: &str) -> Person { Person { first_name: first.to_string(), last_name: name.to_string() } } fn full_name(&self) -> String { format!("{} {}",self.first_name, self.last_name) } fn set_first_name(&mut self, name: &str) { self.first_name = name.to_string(); } fn to_tuple(self) -> (String,String) { (self.first_name, self.last_name) } } fn main() { let mut p = Person::new("John","Smith"); println!("{:?}", p); p.set_first_name("Jane"); println!("{:?}", p); println!("{:?}", p.to_tuple()); // p has now moved. } // Person { first_name: "John", last_name: "Smith" } // Person { first_name: "Jane", last_name: "Smith" } // ("Jane", "Smith")
Lifetimes Start to Bite
Usually structs contain values, but often they also need to contain references. Say we want to put a string slice, not a string value, in a struct.
// life1.rs #[derive(Debug)] struct A { s: &str } fn main() { let a = A { s: "hello dammit" }; println!("{:?}", a); }
error[E0106]: missing lifetime specifier
--> life1.rs:5:8
|
5 | s: &str
| ^ expected lifetime parameter
To understand the complaint, you have to see the problem from the point of view of Rust. It will not allow a reference to be stored without knowing its lifetime. All references are borrowed from some value, and all values have lifetimes. The lifetime of a reference cannot be longer than the lifetime of that value. Rust cannot allow a situation where that reference could suddenly become invalid.
Now, string slices borrow from string literals
like "hello" or from String
values. String literals exist for the duration
of the whole program, which is called the 'static' lifetime.
So this works - we assure Rust that the string slice always refers to such static strings:
// life2.rs #[derive(Debug)] struct A { s: &'static str } fn main() { let a = A { s: "hello dammit" }; println!("{:?}", a); } // A { s: "hello dammit" }
It is not the most pretty notation, but sometimes ugliness is the necessary price of being precise.
This can also be used to specify a string slice that is returned from a function:
# #![allow(unused_variables)] # #fn main() { fn how(i: u32) -> &'static str { match i { 0 => "none", 1 => "one", _ => "many" } } #}
That works for the special case of static strings, but this is very restrictive.
However we can specify that the lifetime of the reference is at least as long as that of the struct itself.
// life3.rs #[derive(Debug)] struct A <'a> { s: &'a str } fn main() { let s = "I'm a little string".to_string(); let a = A { s: &s }; println!("{:?}", a); }
Lifetimes are conventionally called 'a','b',etc but you could just as well called it 'me' here.
After this point, our a
struct and the s
string are bound by a strict contract:
a
borrows from s
, and cannot outlive it.
With this struct definition, we would like to write a function that returns an A
value:
# #![allow(unused_variables)] # #fn main() { fn makes_a() -> A { let string = "I'm a little string".to_string(); A { s: &string } } #}
But A
needs a lifetime - "expected lifetime parameter":
= help: this function's return type contains a borrowed value,
but there is no value for it to be borrowed from
= help: consider giving it a 'static lifetime
rustc
is giving advice, so we follow it:
# #![allow(unused_variables)] # #fn main() { fn makes_a() -> A<'static> { let string = "I'm a little string".to_string(); A { s: &string } } #}
And now the error is
8 | A { s: &string }
| ^^^^^^ does not live long enough
9 | }
| - borrowed value only lives until here
There is no way that this could safely work, because string
will be dropped when the
function ends, and no reference to string
can outlast it.
You can usefully think of lifetime parameters as being part of the type of a value.
Sometimes it seems like a good idea for a struct to contain a value and a reference that borrows from that value. It's basically impossible because structs must be moveable, and any move will invalidate the reference. It isn't necessary to do this - for instance, if your struct has a string field, and needs to provide slices, then it could keep indices and have a method to generate the actual slices.
Traits
Please note that Rust does not spell struct
class. The keyword class
in other
languages is so overloaded with meaning that it effectively shuts down original thinking.
Let's put it like this: Rust structs cannot inherit from other structs; they are all unique types. There is no sub-typing.
So how does one establish relationships between types? This is where traits come in.
rustc
often talks about implementing X trait
and so it's time to talk about traits
properly.
Here's a little example of defining a trait and implementing it for a particular type.
// trait1.rs trait Show { fn show(&self) -> String; } impl Show for i32 { fn show(&self) -> String { format!("four-byte signed {}", self) } } impl Show for f64 { fn show(&self) -> String { format!("eight-byte float {}", self) } } fn main() { let answer = 42; let maybe_pi = 3.14; let s1 = answer.show(); let s2 = maybe_pi.show(); println!("show {}", s1); println!("show {}", s2); } // show four-byte signed 42 // show eight-byte float 3.14
It's pretty cool; we have added a new method to both i32
and f64
!
Getting comfortable with Rust involves learning the basic traits of the standard library (they tend to hunt in packs.)
Debug
is very common.
We gave Person
a default implementation with the
convenient #[derive(Debug)]
, but say we want a Person
to display as its full name:
# #![allow(unused_variables)] # #fn main() { use std::fmt; impl fmt::Debug for Person { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.full_name()) } } ... println!("{:?}", p); // John Smith #}
write!
is a very useful macro - here f
is anything that implements Write
.
(This would also work with a File
- or even a String
.)
Display
controls how values are printed out with "{}" and is implemented
just like Debug
. As a useful side-effect, ToString
is automatically
implemented for anything implementing Display
. So if we implement
Display
for Person
, then p.to_string()
also works.
Clone
defines the method clone
, and can simply be defined with
"#[derive(Clone)]" if all the fields themselves implement Clone
.
Example: iterator over floating-point range
We have met ranges before (0..n
) but they don't work for floating-point values. (You
can force this but you'll end up with a step of 1.0 which is uninteresting.)
Recall the informal definition of an iterator; it is an struct with a next
method
which may return Some
-thing or None
. In the process, the iterator itself gets modified,
it keeps the state for the iteration (like next index and so forth.) The data that
is being iterated over doesn't change usually, (But see Vec::drain
for an
interesting iterator that does modify its data.)
And here is the formal definition: the Iterator trait.
# #![allow(unused_variables)] # #fn main() { trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; ... } #}
Here we meet an associated type of the Iterator
trait.
This trait must work for any type, so you must specify that return type somehow.
The method next
can then be written without using a
particular type - instead it refers to that type parameter's Item
via Self
.
The iterator trait for f64
is written Iterator<Item=f64>
, which can be read as
"an Iterator with its associated type Item set to f64".
The ...
refers to the provided methods of Iterator
. You only need to define Item
and next
, and the provided methods are defined for you.
// trait3.rs struct FRange { val: f64, end: f64, incr: f64 } fn range(x1: f64, x2: f64, skip: f64) -> FRange { FRange {val: x1, end: x2, incr: skip} } impl Iterator for FRange { type Item = f64; fn next(&mut self) -> Option<Self::Item> { let res = self.val; if res >= self.end { None } else { self.val += self.incr; Some(res) } } } fn main() { for x in range(0.0, 1.0, 0.1) { println!("{} ", x); } }
And the rather messy looking result is
0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
This is because 0.1 is not precisely representable as a float, so a little formatting
help is needed. Replace the println!
with this
# #![allow(unused_variables)] # #fn main() { println!("{:.1} ", x); #}
And we get cleaner output (this format means 'one decimal after dot'.)
All of the default iterator methods are available, so we can collect these values into a vector, map them, and so forth.
# #![allow(unused_variables)] # #fn main() { let v: Vec<f64> = range(0.0, 1.0, 0.1).map(|x| x.sin()).collect(); #}
Generic Functions
We want a function which will dump out any value that implements Debug
. Here is
a first attempt at a generic function, where we can pass a reference to any type
of value. T
is a type parameter, which needs to be declared just after the
function name:
# #![allow(unused_variables)] # #fn main() { fn dump<T> (value: &T) { println!("value is {:?}",value); } let n = 42; dump(&n); #}
However, Rust clearly knows nothing about this generic type T
:
error[E0277]: the trait bound `T: std::fmt::Debug` is not satisfied
...
= help: the trait `std::fmt::Debug` is not implemented for `T`
= help: consider adding a `where T: std::fmt::Debug` bound
For this to work, Rust needs to be told that T
does in fact implement Debug
!
# #![allow(unused_variables)] # #fn main() { fn dump<T> (value: &T) where T: std::fmt::Debug { println!("value is {:?}",value); } let n = 42; dump(&n); // value is 42 #}
Rust generic functions need trait bounds on types - we are saying here that
"T is any type that implements Debug". rustc
is being very helpful, and
suggests exactly what bound needs to be provided.
Now that Rust knows the trait bounds for T
, it can give you sensible compiler messages:
# #![allow(unused_variables)] # #fn main() { struct Foo { name: String } let foo = Foo{name: "hello".to_string()}; dump(&foo) #}
And the error is "the trait std::fmt::Debug
is not implemented for Foo
".
Functions are already generic in dynamic languages because values carry their actual type around, and the type checking happens at run-time - or fails miserably. For larger programs, we really do want to know about problems at compile-time rather! Rather than sitting down calmly with compiler errors, a programmer in these languages has to deal with problems that only show up when the program is running. Murphy's Law then implies that these problems will tend to happen at the most inconvenient/disastrous time.
The operation of squaring a number is generic: x*x
will work for integers,
floats and generally for anything that knows about the multiplication operator *
.
But what are the type bounds?
// gen1.rs fn sqr<T> (x: T) -> T { x * x } fn main() { let res = sqr(10.0); println!("res {}",res); }
The first problem is that Rust does not know that T
can be multiplied:
error[E0369]: binary operation `*` cannot be applied to type `T`
--> gen1.rs:4:5
|
4 | x * x
| ^
|
note: an implementation of `std::ops::Mul` might be missing for `T`
--> gen1.rs:4:5
|
4 | x * x
| ^
Following the advice of the compiler, let's constrain that type parameter using
that trait, which is used to implement the multiplication operator *
:
# #![allow(unused_variables)] # #fn main() { fn sqr<T> (x: T) -> T where T: std::ops::Mul { x * x } #}
Which still doesn't work:
rror[E0308]: mismatched types
--> gen2.rs:6:5
|
6 | x * x
| ^^^ expected type parameter, found associated type
|
= note: expected type `T`
= note: found type `<T as std::ops::Mul>::Output`
What rustc
is saying that the type of x*x
is the associated type T::Output
, not T
.
There's actually no reason that the type of x*x
is the same as the type of x
, e.g. the dot product
of two vectors is a scalar.
# #![allow(unused_variables)] # #fn main() { fn sqr<T> (x: T) -> T::Output where T: std::ops::Mul { x * x } #}
and now the error is:
error[E0382]: use of moved value: `x`
--> gen2.rs:6:7
|
6 | x * x
| - ^ value used here after move
| |
| value moved here
|
= note: move occurs because `x` has type `T`, which does not implement the `Copy` trait
So, we need to constrain the type even further!
# #![allow(unused_variables)] # #fn main() { fn sqr<T> (x: T) -> T::Output where T: std::ops::Mul + Copy { x * x } #}
And that (finally) works. Calmly listening to the compiler will often get you closer to the magic point when ... things compile cleanly.
It is a bit simpler in C++:
template <typename T>
T sqr(x: T) {
return x * x;
}
but (to be honest) C++ is adopting cowboy tactics here. C++ template errors are famously bad, because all the compiler knows (ultimately) is that some operator or method is not defined. The C++ committee knows this is a problem and so they are working toward concepts, which are pretty much like trait-constrained type parameters in Rust.
Rust generic functions may look a bit overwhelming at first, but being explicit means you will know exactly what kind of values you can safely feed it, just by looking at the definition.
These functions are called monomorphic, in constrast to polymorphic. The body of the function is compiled separately for each unique type. With polymorphic functions, the same machine code works with each matching type, dynamically dispatching the correct method.
Monomorphic produces faster code,
specialized for the particular type, and can often be inlined. So when sqr(x)
is
seen, it's effectively replaced with x*x
. The downside is that large generic
functions produce a lot of code, for each type used, which can result in code bloat.
As always, there are trade-offs; an experienced person learns to make the right choice
for the job.
Simple Enums
Enums are types which have a few definite values. For instance, a direction has only four possible values.
# #![allow(unused_variables)] # #fn main() { enum Direction { Up, Down, Left, Right } ... // `start` is type `Direction` let start = Direction::Left; #}
They can have methods defined on them, just like structs.
The match
expression is the basic way to handle enum
values.
# #![allow(unused_variables)] # #fn main() { impl Direction { fn as_str(&self) -> &'static str { match *self { // *self has type Direction Direction::Up => "Up", Direction::Down => "Down", Direction::Left => "Left", Direction::Right => "Right" } } } #}
Punctuation matters. Note that *
before self
. It's easy to forget, because often
Rust will assume it (we said self.first_name
, not (*self).first_name
). However,
matching is a more exact business. Leaving it out would give a whole spew of messages,
which boil down to this type mismatch:
= note: expected type `&Direction`
= note: found type `Direction`
This is because self
has type &Direction
, so we have to throw in the *
to
deference the type.
Like structs, enums can implement traits, and our friend #[derive(Debug)]
can
be added to Direction
:
# #![allow(unused_variables)] # #fn main() { println!("start {:?}",start); // start Left #}
So that as_str
method isn't really necessary, since we can always get the name from Debug
.
(But as_str
does not allocate, which may be important.)
You should not assume any particular ordering here - there's no implied integer 'ordinal' value.
Here's a method which defines the 'successor' of each Direction
value. The
very handy wildcard use temporarily puts the enum names into the method context:
# #![allow(unused_variables)] # #fn main() { fn next(&self) -> Direction { use Direction::*; match *self { Up => Right, Right => Down, Down => Left, Left => Up } } ... let mut d = start; for _ in 0..8 { println!("d {:?}", d); d = d.next(); } // d Left // d Up // d Right // d Down // d Left // d Up // d Right // d Down #}
So this will cycle endlessly through the various directions in this particular, arbitrary, order. It is (in fact) a very simple state machine.
These enum values can't be compared:
assert_eq!(start, Direction::Left);
error[E0369]: binary operation `==` cannot be applied to type `Direction`
--> enum1.rs:42:5
|
42 | assert_eq!(start, Direction::Left);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: an implementation of `std::cmp::PartialEq` might be missing for `Direction`
--> enum1.rs:42:5
The solution is to say #[derive(Debug,PartialEq)]
in front of enum Direction
.
This is an important point - Rust user-defined types start out fresh and unadorned.
You give them sensible default behaviours by implementing the common traits. This
applies also to structs - if you ask for Rust to derive PartialEq
for a struct it
will do the sensible thing, assume that all fields implement it and build up
a comparison. If this isn't so, or you want to redefine equality, then you are free
to define PartialEq
explicitly.
Rust does 'C style enums' as well:
// enum2.rs enum Speed { Slow = 10, Medium = 20, Fast = 50 } fn main() { let s = Speed::Slow; let speed = s as u32; println!("speed {}", speed); }
They are initialized with an integer value, and can be converted into that integer with a type cast.
You only need to give the first name a value, and thereafter the value goes up by one each time:
# #![allow(unused_variables)] # #fn main() { enum Difficulty { Easy = 1, Medium, // is 2 Hard // is 3 } #}
By the way, 'name' is too vague, like saying 'thingy' all the time. The proper term here
is variant - Speed
has variants Slow
,Medium
and Fast
.
These enums do have a natural ordering, but you have to ask nicely.
After placing #[derive(PartialEq,PartialOrd)]
in front of enum Speed
, then it's indeed
true that Speed::Fast > Speed::Slow
and Speed::Medium != Speed::Slow
.
Enums in their Full Glory
Rust enums in their full form are like C unions on steroids, like a Ferrari compared to a Fiat Uno. Consider the problem of storing different values in a type-safe way.
// enum3.rs #[derive(Debug)] enum Value { Number(f64), Str(String), Bool(bool) } fn main() { use Value::*; let n = Number(2.3); let s = Str("hello".to_string()); let b = Bool(true); println!("n {:?} s {:?} b {:?}", n,s,b); } // n Number(2.3) s Str("hello") b Bool(true)
Again, this enum can only contain one of these values; its size will be the size of the largest variant.
So far, not really a supercar, although it's cool that enums know how to print themselves
out. But they also know how what kind of value they contain, and that is the
superpower of match
:
# #![allow(unused_variables)] # #fn main() { fn eat_and_dump(v: Value) { use Value::*; match v { Number(n) => println!("number is {}", n), Str(s) => println!("string is '{}'", s), Bool(b) => println!("boolean is {}", b) } } .... eat_and_dump(n); eat_and_dump(s); eat_and_dump(b); //number is 2.3 //string is 'hello' //boolean is true #}
(And that's what Option
and Result
are - enums.)
We like this eat_and_dump
function, but we want to pass the value as a reference, because currently
a move takes place and the value is 'eaten':
# #![allow(unused_variables)] # #fn main() { fn dump(v: &Value) { use Value::*; match *v { // type of *v is Value Number(n) => println!("number is {}", n), Str(s) => println!("string is '{}'", s), Bool(b) => println!("boolean is {}", b) } } error[E0507]: cannot move out of borrowed content --> enum3.rs:12:11 | 12 | match *v { | ^^ cannot move out of borrowed content 13 | Number(n) => println!("number is {}",n), 14 | Str(s) => println!("string is '{}'",s), | - hint: to prevent move, use `ref s` or `ref mut s` #}
There are things you cannot do with borrowed references. Rust is not letting
you extract the string contained in the original value. It did not complain about Number
because it's happy to copy f64
, but String
does not implement Copy
.
I mentioned earlier that match
is picky about exact types;
here we follow the hint and things will work; now we are just borrowing a reference
to that contained string.
# #![allow(unused_variables)] # #fn main() { fn dump(v: &Value) { use Value::*; match *v { Number(n) => println!("number is {}", n), Str(ref s) => println!("string is '{}'", s), Bool(b) => println!("boolean is {}", b) } } .... dump(&s); // string is 'hello' #}
Before we move on, filled with the euphoria of a successful Rust compilation, let's
pause a little. rustc
is unusually good at generating errors that have enough
context for a human to fix the error without necessarily understanding the error.
The issue is a combination of the exactness of matching, with the determination of the
borrow checker to foil any attempt to break the Rules. One of those Rules is that
you cannot yank out a value which belongs to some owning type. Some knowledge of
C++ is a hindrance here, since C++ will copy its way out of the problem, whether that
copy even makes sense. You will get exactly the same error if you try to pull out
a string from a vector, say with *v.get(0).unwrap()
(*
because indexing returns references.)
It will simply not let you do this. (Sometimes clone
isn't such a bad solution to this.)
(By the way, v[0]
does not work for non-copyable values like strings for precisely this reason.
You must either borrow with &v[0]
or clone with v[0].clone()
)
As for match
, you can see Str(s) =>
as short for Str(s: String) =>
. A local variable
(often called a binding) is created. Often that inferred type is cool, when you
eat up a value and extract its contents. But here we really needed is s: &String
, and the
ref
is a hint that ensures this: we just want to borrow that string.
Here we do want to extract that string, and don't care about
the enum value afterwards. _
as usual will match anything.
# #![allow(unused_variables)] # #fn main() { impl Value { fn to_str(self) -> Option<String> { match self { Value::Str(s) => Some(s), _ => None } } } ... println!("s? {:?}", s.to_str()); // s? Some("hello") // println!("{:?}", s) // error! s has moved... #}
Naming matters - this is called to_str
, not as_str
. You can write a
method that just borrows that string as an Option<&String>
(The reference will need
the same lifetime as the enum value.) But you would not call it to_str
.
You can write to_str
like this - it is completely equivalent:
# #![allow(unused_variables)] # #fn main() { fn to_str(self) -> Option<String> { if let Value::Str(s) = self { Some(s) } else { None } } #}
More about Matching
Recall that the values of a tuple can be extracted with '()':
# #![allow(unused_variables)] # #fn main() { let t = (10,"hello".to_string()); ... let (n,s) = t; // t has been moved. It is No More // n is i32, s is String #}
This is a special case of destructuring; we have some data and wish to either pull it apart (like here) or just borrow its values. Either way, we get the parts of a structure.
The syntax is like that used in match
. Here
we are explicitly borrowing the values.
# #![allow(unused_variables)] # #fn main() { let (ref n,ref s) = t; // n and s are borrowed from t. It still lives! // n is &i32, s is &String #}
Destructuring works with structs as well:
# #![allow(unused_variables)] # #fn main() { struct Point { x: f32, y: f32 } let p = Point{x:1.0,y:2.0}; ... let Point{x,y} = p; // p still lives, since x and y can and will be copied // both x and y are f32 #}
Time to revisit match
with some new patterns. The first two patterns are exactly like let
destructuring - it only matches tuples with first element zero, but any string;
the second adds an if
so that it only matches (1,"hello")
.
Finally, just a variable matches anything. This is useful if the match
applies
to an expression and you don't want to bind a variable to that expression. _
works
like a variable but is ignored. It's a common
way to finish off a match
.
# #![allow(unused_variables)] # #fn main() { fn match_tuple(t: (i32,String)) { let text = match t { (0, s) => format!("zero {}", s), (1, ref s) if s == "hello" => format!("hello one!"), tt => format!("no match {:?}", tt), // or say _ => format!("no match") if you're not interested in the value }; println!("{}", text); } #}
Why not just match against (1,"hello")
? Matching is an exact business, and the compiler
will complain:
= note: expected type `std::string::String`
= note: found type `&'static str`
Why do we need ref s
? It's a slightly obscure gotcha (look up the E00008 error) where
if you have an if guard you need to borrow, since the if guard happens in a different
context, a move will take place otherwise. It's a case of the implementation leaking
ever so slightly.
If the type was &str
then we match it directly:
# #![allow(unused_variables)] # #fn main() { match (42,"answer") { (42,"answer") => println!("yes"), _ => println!("no") }; #}
What applies to match
applies to if let
. This is a cool example, since if we
get a Some
, we can match inside it and only extract the string from the tuple. So it
isn't necessary to have nested if let
statements here. We use _
because we aren't interested
in the first part of the tuple.
# #![allow(unused_variables)] # #fn main() { let ot = Some((2,"hello".to_string()); if let Some((_,ref s)) = ot { assert_eq!(s, "hello"); } // we just borrowed the string, no 'destructive destructuring' #}
An interesting problem happens when using parse
(or any function which needs to work
out its return type from context)
# #![allow(unused_variables)] # #fn main() { if let Ok(n) = "42".parse() { ... } #}
So what's the type of n
?  Is it
even an integer?
# #![allow(unused_variables)] # #fn main() { if let Ok(n) = "42".parse::<i32>() { ... } #}
This somewhat non-elegant syntax is called the 'turbofish operator'.
If you are in a function returning Result
, then the question-mark operator provides a much
more elegant solution:
# #![allow(unused_variables)] # #fn main() { let n: i32 = "42".parse()?; #}
However, the parse error needs to be convertible to the error type of the Result
, which is a topic
we'll take up later when discussing error handling.
Closures
A great deal of Rust's power comes from closures. In their simplest form, they act like shortcut functions:
# #![allow(unused_variables)] # #fn main() { let f = |x| x * x; let res = f(10); println!("res {}", res); // res 100 #}
There are no explicit types in this example - everything is deduced, starting with the integer literal 10.
We get an error if we call f
on different types - Rust has already decided that
f
must be called on an integer type:
let res = f(10);
let resf = f(1.2);
|
8 | let resf = f(1.2);
| ^^^ expected integral variable, found floating-point variable
|
= note: expected type `{integer}`
= note: found type `{float}`
So, the first call fixes the type of the argument x
. It's equivalent to this function:
# #![allow(unused_variables)] # #fn main() { fn f (x: i32) -> i32 { x * x } #}
But there's a big difference between functions and closures, apart from the need for explicit typing. Here we evaluate a linear function:
# #![allow(unused_variables)] # #fn main() { let m = 2.0; let c = 1.0; let lin = |x| m*x + c; println!("res {} {}", lin(1.0), lin(2.0)); // res 3 5 #}
You cannot do this with the explicit fn
form - it does not know about variables
in the enclosing scope. The closure has borrowed m
and c
from its context.
Now, what's the type of lin
? Only rustc
knows.
Under the hood, a closure is a struct that is callable ('implements the call operator').
It behaves as if it was written out like this:
# #![allow(unused_variables)] # #fn main() { struct MyAnonymousClosure1<'a> { m: &'a f64, c: &'a f64 } impl <'a>MyAnonymousClosure1<'a> { fn call(&self, x: f64) -> f64 { self.m * x + self.c } } #}
The compiler is certainly being helpful, turning simple closure syntax into all that code! You do need to know that a closure is a struct and it borrows values from its environment. And that therefore it has a lifetime.
All closures are unique types, but they have traits in common. So even though we don't know the exact type, we know the generic constraint:
# #![allow(unused_variables)] # #fn main() { fn apply<F>(x: f64, f: F) -> f64 where F: Fn(f64)->f64 { f(x) } ... let res1 = apply(3.0,lin); let res2 = apply(3.14, |x| x.sin()); #}
In English: apply
works for any type T
such that T
implements Fn(f64)->f64
- that
is, is a function which takes f64
and returns f64
.
After the call to apply(3.0,lin)
, trying to access lin
gives an interesting error:
let l = lin;
error[E0382]: use of moved value: `lin`
--> closure2.rs:22:9
|
16 | let res = apply(3.0,lin);
| --- value moved here
...
22 | let l = lin;
| ^ value used here after move
|
= note: move occurs because `lin` has type
`[closure@closure2.rs:12:15: 12:26 m:&f64, c:&f64]`,
which does not implement the `Copy` trait
That's it, apply
ate our closure. And there's the actual type of the struct that
rustc
made up to implement it. Always thinking of closures as structs is helpful.
Calling a closure is a method call: the three kinds of function traits correspond to the three kinds of methods:
Fn
struct passed as&self
FnMut
struct passed as&mut self
FnOnce
struct passed asself
So it's possible for a closure to mutate its captured references:
# #![allow(unused_variables)] # #fn main() { fn mutate<F>(mut f: F) where F: FnMut() { f() } let mut s = "world"; mutate(|| s = "hello"); assert_eq!(s, "hello"); #}
Note that mut
- f
needs to be mutable for this to work.
However, you cannot escape the rules for borrowing. Consider this:
# #![allow(unused_variables)] # #fn main() { let mut s = "world"; // closure does a mutable borrow of s let mut changer = || s = "world"; changer(); // does an immutable borrow of s assert_eq!(s, "world"); #}
Can't be done! The error is that we cannot borrow s
in the assert statement, because it has been previously borrowed by the
closure changer
as mutable. As long as that closure lives, no other
code can access s
, so the solution is to control that lifetime by
putting the closure in a limited scope:
# #![allow(unused_variables)] # #fn main() { let mut s = "world"; { let mut changer = || s = "world"; changer(); } assert_eq!(s, "world"); #}
At this point, if you are used to languages like JavaScript or Lua, you may wonder at the
complexity of Rust closures compared with how straightforward they are in those languages.
This is the necessary cost of Rust's promise to not sneakily make any allocations. In JavaScript,
the equivalent mutate(function() {s = "hello";})
will always result in a dynamically
allocated closure.
Sometimes you don't want a closure to borrow those variables, but instead move them.
# #![allow(unused_variables)] # #fn main() { let name = "dolly".to_string(); let age = 42; let c = move || { println!("name {} age {}", name,age); }; c(); println!("name {}",name); #}
And the error at the last println
is: "use of moved value: name
". So one solution
here - if we did want to keep name
alive - is to move a cloned copy into the closure:
# #![allow(unused_variables)] # #fn main() { let cname = name.to_string(); let c = move || { println!("name {} age {}",cname,age); }; #}
Why are moved closures needed? Because we might need to call them at a point where the original context no longer exists. A classic case is when creating a thread. A moved closure does not borrow, so does not have a lifetime.
A major use of closures is within iterator methods. Recall the range
iterator we
defined to go over a range of floating-point numbers. It's straightforward to operate
on this (or any other iterator) using closures:
# #![allow(unused_variables)] # #fn main() { let sine: Vec<f64> = range(0.0,1.0,0.1).map(|x| x.sin()).collect(); #}
map
isn't defined on vectors (although it's easy enough to create a trait that does this),
because then every map will create a new vector. This way, we have a choice. In this
sum, no temporary objects are created:
# #![allow(unused_variables)] # #fn main() { let sum: f64 = range(0.0,1.0,0.1).map(|x| x.sin()).sum(); #}
It will (in fact) be as fast as writing it out as an explicit loop! That performance guarantee would be impossible if Rust closures were as 'frictionless' as Javascript closures.
filter
is another useful iterator method - it only lets through values that match
a condition:
# #![allow(unused_variables)] # #fn main() { let tuples = [(10,"ten"),(20,"twenty"),(30,"thirty"),(40,"forty")]; let iter = tuples.iter().filter(|t| t.0 > 20).map(|t| t.1); for name in iter { println!("{} ", name); } // thirty // forty #}
The Three Kinds of Iterators
The three kinds correspond (again) to the three basic argument types. Assume we
have a vector of String
values. Here are the iterator types explicitly, and
then implicitly, together with the actual type returned by the iterator.
# #![allow(unused_variables)] # #fn main() { for s in vec.iter() {...} // &String for s in vec.iter_mut() {...} // &mut String for s in vec.into_iter() {...} // String // implicit! for s in &vec {...} // &String for s in &mut vec {...} // &mut String for s in vec {...} // String #}
Personally I prefer being explicit, but it's important to understand both forms, and their implications.
into_iter
consumes the vector and extracts its strings,
and so afterwards the vector is no longer available - it has been moved. It's
a definite gotcha for Pythonistas used to saying for s in vec
!
So the
implicit form for s in &vec
is usually the one you want, just as &T
is a good
default in passing arguments to functions.
It's important to understand how the three kinds works because Rust relies heavily on type deduction - you won't often see explicit types in closure arguments. And this is a Good Thing, because it would be noisy if all those types were explicitly typed out. However, the price of this compact code is that you need to know what the implicit types actually are!
map
takes whatever value the iterator returns and converts it into something else,
but filter
takes a reference to that value. In this case, we're using iter
so
the iterator item type is &String
. Note that filter
receives a reference to this type.
# #![allow(unused_variables)] # #fn main() { for n in vec.iter().map(|x: &String| x.len()) {...} // n is usize .... } for s in vec.iter().filter(|x: &&String| x.len() > 2) { // s is &String ... } #}
When calling methods, Rust will derefence automatically, so the problem isn't obvious.
But |x: &&String| x == "one"|
will not work, because operators are more strict
about type matching. rustc
will complain that there is no such operator that
compares &&String
and &str
. So you need an explicit deference to make that &&String
into a &String
which does match.
# #![allow(unused_variables)] # #fn main() { for s in vec.iter().filter(|x: &&String| *x == "one") {...} // same as implicit form: for s in vec.iter().filter(|x| *x == "one") {...} #}
If you leave out the explicit type, you can modify the argument so that the type of s
is now &String
:
# #![allow(unused_variables)] # #fn main() { for s in vec.iter().filter(|&x| x == "one") #}
And that's usually how you will see it written.
Structs with Dynamic Data
A most powerful technique is a struct that contain references to itself.
Here is the basic building block of a binary tree, expressed in C (everyone's favourite old relative with a frightening fondness for using power tools without protection.)
# #![allow(unused_variables)] # #fn main() { struct Node { const char *payload; struct Node *left; struct Node *right; }; #}
You can not do this by directly including Node
fields, because then the size of
Node
depends on the size of Node
... it just doesn't compute. So we use pointers
to Node
structs, since the size of a pointer is always known.
If left
isn't NULL
, the Node
will have a left pointing to another node, and so
moreorless indefinitely.
Rust does not do NULL
(at least not safely) so it's clearly a job for Option
.
But you cannot just put a Node
in that Option
, because we don't know the size
of Node
(and so forth.) This is a job for Box
, since it contains an allocated
pointer to the data, and always has a fixed size.
So here's the Rust equivalent, using type
to create an alias:
# #![allow(unused_variables)] # #fn main() { type NodeBox = Option<Box<Node>>; #[derive(Debug)] struct Node { payload: String, left: NodeBox, right: NodeBox } #}
(Rust is forgiving in this way - no need for forward declarations.)
And a first test program:
impl Node { fn new(s: &str) -> Node { Node{payload: s.to_string(), left: None, right: None} } fn boxer(node: Node) -> NodeBox { Some(Box::new(node)) } fn set_left(&mut self, node: Node) { self.left = Self::boxer(node); } fn set_right(&mut self, node: Node) { self.right = Self::boxer(node); } } fn main() { let mut root = Node::new("root"); root.set_left(Node::new("left")); root.set_right(Node::new("right")); println!("arr {:#?}", root); }
The output is surprisingly pretty, thanks to "{:#?}" ('#' means 'extended'.)
root Node {
payload: "root",
left: Some(
Node {
payload: "left",
left: None,
right: None
}
),
right: Some(
Node {
payload: "right",
left: None,
right: None
}
)
}
Now, what happens when root
is dropped? All fields are dropped; if the 'branches' of
the tree are dropped, they drop their fields and so on. Box::new
may be the
closest you will get to a new
keyword, but we have no need for delete
or free
.
We must now work out a use for this tree. Note that strings can be ordered: 'bar' < 'foo', 'abba' > 'aardvark'; so-called 'alphabetical order'. (Strictly speaking, this is lexical order, since human languages are very diverse and have strange rules.)
Here is a method which inserts nodes in lexical order of the strings. We compare the new data
to the current node - if it's less, then we try to insert on the left, otherwise try to insert
on the right. There may be no node on the left, so then set_left
and so forth.
fn insert(&mut self, data: &str) { if data < &self.payload { match self.left { Some(ref mut n) => n.insert(data), None => self.set_left(Self::new(data)), } } else { match self.right { Some(ref mut n) => n.insert(data), None => self.set_right(Self::new(data)), } } } ... fn main() { let mut root = Node::new("root"); root.insert("one"); root.insert("two"); root.insert("four"); println!("root {:#?}", root); }
Note the match
- we're pulling out a mutable reference to the box, if the Option
is Some
, and applying the insert
method. Otherwise, we need to create a new Node
for the left side and so forth.
And here's the output tree:
root Node {
payload: "root",
left: Some(
Node {
payload: "one",
left: Some(
Node {
payload: "four",
left: None,
right: None
}
),
right: None
}
),
right: Some(
Node {
payload: "two",
left: None,
right: None
}
)
}
The strings that are 'less' than other strings get put down the left side, otherwise the right side.
Time for a visit. This is in-order traversal - we visit the left, do something on the node, and then visit the right.
# #![allow(unused_variables)] # #fn main() { fn visit(&self) { if let Some(ref left) = self.left { left.visit(); } println!("'{}'", self.payload); if let Some(ref right) = self.right { right.visit(); } } ... ... root.visit(); // 'four' // 'one' // 'root' // 'two' #}
So we're visiting the strings in order! Please note the reappearance of ref
- if let
uses exactly the same rules as match
.
Generic Structs
Consider the previous example of a binary tree. It would be seriously irritating to
have to rewrite it for all possible kinds of payload.
So here's our generic Node
with its type parameter T
.
# #![allow(unused_variables)] # #fn main() { type NodeBox<T> = Option<Box<Node<T>>>; #[derive(Debug)] struct Node<T> { payload: T, left: NodeBox<T>, right: NodeBox<T> } #}
The implementation shows the difference between the languages. The fundamental operation
on the payload is comparison, so T must be comparable with <
, i.e. implements PartialOrd
.
The type parameter must be declared in the impl
block with its constraints:
impl <T: PartialOrd> Node<T> { fn new(s: T) -> Node<T> { Node{payload: s, left: None, right: None} } fn boxer(node: Node<T>) -> NodeBox<T> { Some(Box::new(node)) } fn set_left(&mut self, node: Node<T>) { self.left = Self::boxer(node); } fn set_right(&mut self, node: Node<T>) { self.right = Self::boxer(node); } fn insert(&mut self, data: T) { if data < self.payload { match self.left { Some(ref mut n) => n.insert(data), None => self.set_left(Self::new(data)), } } else { match self.right { Some(ref mut n) => n.insert(data), None => self.set_right(Self::new(data)), } } } } fn main() { let mut root = Node::new("root".to_string()); root.insert("one".to_string()); root.insert("two".to_string()); root.insert("four".to_string()); println!("root {:#?}", root); }
So generic structs need their type parameter(s) specified
in angle brackets, like C++. Rust is usually smart enough to work out
that type parameter from context - it knows it has a Node<T>
, and knows
that its insert
method is passed T
. The first call of insert
nails
down T
to be String
. If any further calls are inconsistent it will complain.
But you do need to constrain that type appropriately!
Filesystem and Processes
Another look at Reading Files
At the end of Part 1, I showed how to read a whole file into a string. Naturally this isn't always such a good idea, so here is how to read a file line-by-line.
fs::File
implements io::Read
, which is the trait for anything readable.
This trait defines a read
method which will fill a slice of u8
with bytes -
this is the only required method of the trait, and you get some provided methods
for free, much like with Iterator
. You can use read_to_end
to fill a vector of
bytes with contents from the readable, and read_to_string
to fill a string - which
must be UTF-8 encoded.
This is a 'raw' read, with no buffering. For buffered reading there is the
io::BufRead
trait which gives us read_line
and a lines
iterator.
io::BufReader
will provide an implementation of io::BufRead
for any readable.
fs::File
also implements io::Write
.
The easiest way to make sure all these traits are visible is use std::io::prelude::*
.
# #![allow(unused_variables)] # #fn main() { use std::fs::File; use std::io; use std::io::prelude::*; fn read_all_lines(filename: &str) -> io::Result<()> { let file = File::open(&filename)?; let reader = io::BufReader::new(file); for line in reader.lines() { let line = line?; println!("{}", line); } Ok(()) } #}
The let line = line?
may look a bit strange. The line
returned by the
iterator is actually an io::Result<String>
which we unwrap with ?
.
Because things can go wrong during this iteration - I/O errors, swallowing
a chunk of bytes that aren't UTF-8, and so forth.
lines
being an iterator, it is straightforward to read a file into a vector
of strings using collect
, or print out the line with line numbers using the
enumerate
iterator.
It isn't the most efficient way to read all the lines, however, because a new
string is allocated for each line. It is more efficient to use read_line
, although
more awkward. Note that the returned line includes the linefeed, which
can be removed using trim_right
.
# #![allow(unused_variables)] # #fn main() { let mut reader = io::BufReader::new(file); let mut buf = String::new(); while reader.read_line(&mut buf)? > 0 { { let line = buf.trim_right(); println!("{}", line); } buf.clear(); } #}
This results in far less allocations, because clearing that string does not free its allocated memory; once the string has enough capacity, no more allocations will take place.
This is one of those cases where we use a block to control a borrow. line
is
borrowed from buf
, and this borrow must finish before we modify buf
. Again,
Rust is trying to stop us doing something stupid, which is to access line
after
we've cleared the buffer. (The borrow checker can be restrictive sometimes.
Rust is due to get 'non-lexical lifetimes', where
it will analyze the code and see that line
isn't used after buf.clear()
.)
This isn't very pretty. I cannot give you a proper iterator that returns references to a buffer, but I can give you something that looks like an iterator.
First define a generic struct;
the type parameter R
is 'any type that implements Read'. It contains the reader
and the buffer which we are going to borrow from.
# #![allow(unused_variables)] # #fn main() { // file5.rs use std::fs::File; use std::io; use std::io::prelude::*; struct Lines<R> { reader: io::BufReader<R>, buf: String } impl <R: Read> Lines<R> { fn new(r: R) -> Lines<R> { Lines{reader: io::BufReader::new(r), buf: String::new()} } ... } #}
Then the next
method. It returns an Option
- just like an iterator, when it
returns None
the iterator finishes. The returned type is a Result
because
read_line
might fail, and we never throw errors away. So if fails, we
wrap up its error in a Some<Result>
. Otherwise, it may have read zero bytes,
which is the natural end of the file - not an error, just a None
.
At this point, the buffer contains the line with a linefeed (`\n') appended. Trim this away, and package up the string slice.
# #![allow(unused_variables)] # #fn main() { fn next<'a>(&'a mut self) -> Option<io::Result<&'a str>>{ self.buf.clear(); match self.reader.read_line(&mut self.buf) { Ok(nbytes) => if nbytes == 0 { None // no more lines! } else { let line = self.buf.trim_right(); Some(Ok(line)) }, Err(e) => Some(Err(e)) } } #}
Now, note how the lifetimes work. We need an explicit lifetime because Rust will never
allow us to hand out borrowed string slices without knowing their lifetime. And here
we say that the lifetime of this borrowed string is within the lifetime of self
.
And this signature, with the lifetime, is incompatible with the interface of Iterator
.
But it's easy to see problems if it were compatible; consider collect
trying to make
a vector of these string slices. There's no way this could work, since they're all
borrowed from the same mutable string! (If you had read all the file into a string, then
the string's lines
iterator can return string slices because they are all borrowed from
distinct parts of the original string.)
The resulting loop is much cleaner, and the file buffering is invisible to the user.
# #![allow(unused_variables)] # #fn main() { fn read_all_lines(filename: &str) -> io::Result<()> { let file = File::open(&filename)?; let mut lines = Lines::new(file); while let Some(line) = lines.next() { let line = line?; println!("{}", line); } Ok(()) } #}
You can even write the loop like this, since the explicit match can pull out the string slice:
# #![allow(unused_variables)] # #fn main() { while let Some(Ok(line)) = lines.next() { println!("{}", line)?; } #}
It's tempting, but you are throwing away a possible error here; this loop will silently stop whenever an error occurs. In particular, it will stop at the first place where Rust can't convert a line to UTF-8. Fine for casual code, bad for production code!
Writing To Files
We met the write!
macro when implementing Debug
- it also works with anything
that implements Write
. So here's a another way of saying print!
:
# #![allow(unused_variables)] # #fn main() { let mut stdout = io::stdout(); ... write!(stdout,"answer is {}\n", 42).expect("write failed"); #}
If an error is possible, you must handle it. It may not be
very likely but it can happen. It's usually fine, because if you
are doing file i/o you should be in a context where ?
works.
But there is a difference: print!
locks stdout for each write. This is usually
what you want for output, because without that locking multithreaded
programs can mix up that output in interesting ways. But if you are pumping out
a lot of text, then write!
is going to be faster.
For arbitrary files we need write!
. The
file is closed when out
is dropped at the end of write_out
, which is
both convenient and important.
// file6.rs use std::fs::File; use std::io; use std::io::prelude::*; fn write_out(f: &str) -> io::Result<()> { let mut out = File::create(f)?; write!(out,"answer is {}\n", 42)?; Ok(()) } fn main() { write_out("test.txt").expect("write failed"); }
If you care about performance, you need to know that Rust files are unbuffered
by default. So each little write request goes straight to the OS, and this is
going to be significantly slower. I mention this because this default is different
from other programming languages, and could lead to the shocking discovery that Rust
can be left in the dust by scripting languages!
Just as with Read
and io::BufReader
, there is io::BufWriter
for
buffering any Write
.
Files, Paths and Directories
Here is a little program for printing out the Cargo directory on a machine. The
simplest case is that it's '~/.cargo'. This is a Unix shell expansion,
so we use env::home_dir
because it's cross-platform. (It might fail, but a
computer without a home directory isn't going to be hosting Rust tools anyway.)
We then create a PathBuf
and use its push
method to build up the full file path from its components.
(This is much easier than fooling around with '/','' or whatever, depending on
the system.)
// file7.rs use std::env; use std::path::PathBuf; fn main() { let home = env::home_dir().expect("no home!"); let mut path = PathBuf::new(); path.push(home); path.push(".cargo"); if path.is_dir() { println!("{}", path.display()); } }
A PathBuf
is like String
- it owns a growable set of characters, but with methods
specialized to building up paths. Most of its functionality however comes from
the borrowed version Path
, which is like &str
. So, for instance, is_dir
is
a Path
method.
This might sound suspiciously like a form of inheritance, but the magic Deref
trait works differently. It works just like it does with String/&str
-
a reference to PathBuf
can be coerced into a reference to Path
.
('Coerce' is a strong word, but this really
is one of the few places where Rust does conversions for you.)
# #![allow(unused_variables)] # #fn main() { fn foo(p: &Path) {...} ... let path = PathBuf::from(home); foo(&path); #}
PathBuf
has an intimate relationship with OsString
, which represents strings we get
directly from the system. (There is a corresponding OsString/&OsStr
relationship.)
Such strings are not guaranteed to be representable as UTF-8! Real life is a complicated matter, particularly see the answer to 'Why are they so hard?'. To summarize, first there are years of ASCII legacy coding, and multiple special encodings for other languages. Second, human languages are complicated. For instance 'noël' is five Unicode code points!
It's true that most of the time
with modern operating systems file names will be Unicode (UTF-8 on the Unix side, UTF-16
for Windows), except when they're not. And Rust must handle that possibility
rigorously. For instance,
Path
has a method as_os_str
which returns a &OsStr
, but the to_str
method
returns an Option<&str>
. Not always possible!
People have trouble at this point because they have become too attached to 'string' and
'character' as the only necessary abstractions. As Einstein could have said, a programming language
has to be as simple as possible, but no simpler. A systems language needs a
String/&str
distinction (owned versus borrowed: this is also very convenient)
and if it wishes to standardize on Unicode strings then it needs another type to handle
text which isn't valid Unicode - hence OsString/&OsStr
. Notice that there aren't
any interesting string-like methods for these types, precisely because we don't know the
encoding.
But, people are used to processing filenames as if they were strings, which is why
Rust makes it easier to manipulate file paths using PathBuf
methods.
You can pop
to successively remove path components. Here we start with the
current directory of the program:
// file8.rs use std::env; fn main() { let mut path = env::current_dir().expect("can't access current dir"); loop { println!("{}", path.display()); if ! path.pop() { break; } } } // /home/steve/rust/gentle-intro/code // /home/steve/rust/gentle-intro // /home/steve/rust // /home/steve // /home // /
Here's a useful variation. I have a program which searches for a configuration file,
and the rule is that it may appear in any subdirectory of the current directory.
So I create /home/steve/rust/config.txt
and start this program up in /home/steve/rust/gentle-intro/code
:
// file9.rs use std::env; fn main() { let mut path = env::current_dir().expect("can't access current dir"); loop { path.push("config.txt"); if path.is_file() { println!("gotcha {}", path.display()); break; } else { path.pop(); } if ! path.pop() { break; } } } // gotcha /home/steve/rust/config.txt
This is pretty much how git works when it wants to know what the current repo is.
The details about a file (its size, type, etc) are called its metadata. As always, there may be an error - not just 'not found' but also if we don't have permission to read this file.
// file10.rs use std::env; use std::path::Path; fn main() { let file = env::args().skip(1).next().unwrap_or("file10.rs".to_string()); let path = Path::new(&file); match path.metadata() { Ok(data) => { println!("type {:?}", data.file_type()); println!("len {}", data.len()); println!("perm {:?}", data.permissions()); println!("modified {:?}", data.modified()); }, Err(e) => println!("error {:?}", e) } } // type FileType(FileType { mode: 33204 }) // len 488 // perm Permissions(FilePermissions { mode: 436 }) // modified Ok(SystemTime { tv_sec:<PHONE_NUMBER>, tv_nsec: 600495644 })
The length of the file (in bytes) and modified time are straightforward to interpret.
(Note we may not be able to get this time!) The file type has methods is_dir
,
is_file
and is_symlink
.
permissions
is an interesting one. Rust strives to be cross-platform, and so it's
a case of the 'lowest common denominator'. In general, all you can query is whether
the file is read-only - the 'permissions' concept is extended in Unix and encodes
read/write/executable for user/group/others.
But, if you are not interested in Windows, then bringing in a platform-specific trait will give us at least the permission mode bits. (As usual, a trait only kicks in when it is visible.) Then, applying the program to its own executable gives:
# #![allow(unused_variables)] # #fn main() { use std::os::unix::fs::PermissionsExt; ... println!("perm {:o}",data.permissions().mode()); // perm 755 #}
(Note '{:o}' for printing out in octal)
(Whether a file is executable on Windows is determined by its extension. The executable
extensions are found in the PATHEXT
environment variable - '.exe','.bat' and so forth).
std::fs
contains a number of useful functions for working with files, such as copying or
moving files, making symbolic links and creating directories.
To find the contents of a directory, std::fs::read_dir
provides an iterator.
Here are all files with extension '.rs' and size greater than 1024 bytes:
# #![allow(unused_variables)] # #fn main() { fn dump_dir(dir: &str) -> io::Result<()> { for entry in fs::read_dir(dir)? { let entry = entry?; let data = entry.metadata()?; let path = entry.path(); if data.is_file() { if let Some(ex) = path.extension() { if ex == "rs" && data.len() > 1024 { println!("{} length {}", path.display(),data.len()); } } } } Ok(()) } // ./enum4.rs length 2401 // ./struct7.rs length 1151 // ./sexpr.rs length 7483 // ./struct6.rs length 1359 // ./new-sexpr.rs length 7719 #}
Obviously read_dir
might fail (usually 'not found' or 'no permission'), but
also getting each new entry might fail (it's like the lines
iterator over a buffered
reader's contents). Plus, we might not be able to get the metadata corresponding to
the entry. A file might have no extension, so we have to check for that as well.
Why not just an iterator over paths? On Unix this is the way the opendir
system call works,
but on Windows you cannot iterate over a directory's contents without getting the
metadata. So this is a reasonably elegant compromise that allows cross-platform
code to be as efficient as possible.
You can be forgiven for feeling 'error fatigue' at this point. But please note that the errors always existed - it's not that Rust is inventing new ones. It's just trying hard to make it impossible for you to ignore them. Any operating system call may fail.
Languages like Java and Python throw exceptions; languages like Go and Lua return two values, where the first is the result and the second is the error: like Rust it is considered bad manners for library functions to raise errors. So there is a lot of error checking and early-returns from functions.
Rust uses Result
because it's either-or: you cannot get both a result and an error.
And the question-mark operator makes handling errors much cleaner.
Processes
A fundamental need is for programs to run programs, or to launch processes. Your program can spawn as many child processes it likes, and as the name suggests they have a special relationship with their parent.
To run a program is straightforward using the Command
struct, which builds up
arguments to pass to the program:
use std::process::Command; fn main() { let status = Command::new("rustc") .arg("-V") .status() .expect("no rustc?"); println!("cool {} code {}", status.success(), status.code().unwrap()); } // rustc 1.15.0-nightly (8f02c429a 2016-12-15) // cool true code 0
So new
receives the name of the program (it will be looked up on PATH
if not
an absolute filename), arg
adds a new argument, and status
causes it to be run.
This returns a Result
, which is Ok
if the program actually run, containing an
ExitStatus
. In this case, the program succeeded, and returned an exit code 0. (The
unwrap
is because we can't always get the code if the program was killed by
a signal).
If we change the -V
to -v
(an easy mistake) then rustc
fails:
error: no input filename given
cool false code 101
So there are three possibilities:
- program didn't exist, was bad, or we were not allowed to run it
- program ran, but was not successful - non-zero exit code
- program ran, with zero exit code. Success!
By default, the program's standard output and standard error streams go to the terminal.
Often we are very interested in capturing that output, so there's the output
method.
// process2.rs use std::process::Command; fn main() { let output = Command::new("rustc") .arg("-V") .output() .expect("no rustc?"); if output.status.success() { println!("ok!"); } println!("len stdout {} stderr {}", output.stdout.len(), output.stderr.len()); } // ok! // len stdout 44 stderr 0
As with status
our program blocks until the child process is finished, and we get
back three things - the status (as before), the contents of stdout and the contents
of stderr.
The captured output is simply Vec<u8>
- just bytes. Recall we have no guarantee
that data we receive from the operating system is a properly encoded UTF-8 string. In
fact, we have no guarantee that it even is a string - programs may return arbitrary
binary data.
If we are pretty sure the output is UTF-8, then String::from_utf8
will convert those
vectors or bytes - it returns a Result
because this conversion may not succeed.
A more sloppy function is String::from_utf8_lossy
which will make a good attempt at
conversion and insert the invalid Unicode mark � where it failed.
Here is a useful function which runs a program using the shell. This uses the usual shell mechanism for joining stderr to stdout. The name of the shell is different on Windows, but otherwise things work as expected.
# #![allow(unused_variables)] # #fn main() { fn shell(cmd: &str) -> (String,bool) { let cmd = format!("{} 2>&1",cmd); let shell = if cfg!(windows) {"cmd.exe"} else {"/bin/sh"}; let flag = if cfg!(windows) {"/c"} else {"-c"}; let output = Command::new(shell) .arg(flag) .arg(&cmd) .output() .expect("no shell?"); ( String::from_utf8_lossy(&output.stdout).trim_right().to_string(), output.status.success() ) } fn shell_success(cmd: &str) -> Option<String> { let (output,success) = shell(cmd); if success {Some(output)} else {None} } #}
I'm trimming any whitespace from the right so that if you said shell("which rustc")
you will get the path without any extra linefeed.
You can control the execution of a program launched by Process
by specifying the directory it will run
in using the current_dir
method and the environment variables it sees using env
.
Up to now, our program simply waits for the child process to finish. If you use
the spawn
method then we return immediately, and must explicitly wait for it to
finish - or go off and do something else in the meantime! This example also
shows how to suppress both standard out and standard error:
// process5.rs use std::process::{Command,Stdio}; fn main() { let mut child = Command::new("rustc") .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("no rustc?"); let res = child.wait(); println!("res {:?}", res); }
By default, the child 'inherits' the standard input and output of the parent. In this case,
we redirect the child's output handles into 'nowhere'. It's equivalent to saying
> /dev/null 2> /dev/null
in the Unix shell.
Now, it's possible to do these things using the shell (sh
or cmd
) in Rust.
But this way you get full programmatic control of process creation.
For example, if we just had .stdout(Stdio::piped())
then the child's standard output
is redirected to a pipe. Then child.stdout
is something you can use to directly
read the output (i.e. implements Read
). Likewise, you can use the .stdout(Stdio::piped())
method so you can write to child.stdin
.
But if we used wait_with_output
instead of wait
then
it returns a Result<Output>
and the child's output is captured into the stdout
field of that Output
as a Vec<u8>
just as before.

Modules and Cargo
Modules
As programs get larger, it's necessary to spread them over more than one file and put functions and types in different namespaces. The Rust solution for both of these is modules.
C does the first, and not
the second, so you end up with awful names like primitive_display_set_width
and
so forth. The actual filenames can be named arbitrarily.
In Rust the full name would look like primitive::display::set_width
,
and after saying use primitive::display
you can then refer to it as display::set_width
.
You can even say use primitive::display::set_width
and then just say set_width
, but
it's not a good idea to get carried away with this. rustc
will not be confused, but you
may get confused later. But for this to work, filenames must follow some simple rules.
A new keyword mod
is used to define a module as a block
where Rust types or functions can be written:
mod foo { #[derive(Debug)] struct Foo { s: &'static str } } fn main() { let f = foo::Foo{s: "hello"}; println!("{:?}", f); }
But it's still not quite right - we get 'struct Foo is private'. To solve this, we need the pub
keyword
to export Foo
. The error then changes to 'field s of struct foo::Foo is private', so put pub
before the field s
to export Foo::s
. Then things will work.
# #![allow(unused_variables)] # #fn main() { pub struct Foo { pub s: &'static str } #}
Needing an explicit pub
means that you must choose what items to make public from a module.
The set of functions and types exported from a module is called its interface.
It is usually better to hide the insides of a struct, and only allow access through methods:
mod foo { #[derive(Debug)] pub struct Foo { s: &'static str } impl Foo { pub fn new(s: &'static str) -> Foo { Foo{s: s} } } } fn main() { let f = foo::Foo::new("hello"); println!("{:?}", f); }
Why is hiding the implementation a good thing? Because it means you may change it later without breaking the interface, without consumers of a module getting too dependent on its details. The great enemy of large-scale programing is a tendency for code to get entangled, so that understanding a piece of code is impossible in isolation.
In a perfect world a module does one thing, does it well, and keeps its own secrets.
When not to hide? As Stroustrup says, when the interface is the implementation, like
struct Point{x: f32, y: f32}
.
Within a module, all items are visible to all other items. It's a cozy place where everyone can be friends and know intimate details about each other.
Everyone gets to a point where they want to break a program up into separate files, depending on taste. I start getting uncomfortable around 500 lines, but we all agree that more than 2000 lines is pushing it.
So how to break this program into separate files?
We put the foo
code into foo.rs
:
# #![allow(unused_variables)] # #fn main() { // foo.rs #[derive(Debug)] pub struct Foo { s: &'static str } impl Foo { pub fn new(s: &'static str) -> Foo { Foo{s: s} } } #}
And use a mod foo
statement without a block in the main program:
// mod3.rs mod foo; fn main() { let f = foo::Foo::new("hello"); println!("{:?}", f); }
Now rustc mod3.rs
will cause foo.rs
to be compiled as well. There is no need to fool around
with makefiles!
The compiler will also look at MODNAME/mod.rs
, so this will work
if I create a directory boo
containing a file mod.rs
:
# #![allow(unused_variables)] # #fn main() { // boo/mod.rs pub fn answer()->u32 { 42 } #}
And now the main program can use both modules as separate files:
// mod3.rs
mod foo;
mod boo;
fn main() {
let f = foo::Foo::new("hello");
let res = boo::answer();
println!("{:?} {}", f,res);
}
So far, there's mod3.rs
, containing main
, a module foo.rs
and a directory boo
containing
mod.rs
. The usual convention is that the file containing main
is just called main.rs
.
Why two ways to do the same thing? Because boo/mod.rs
can refer to other modules defined in boo
,
Update boo/mod.rs
and add a new module - note that this is explicitly exported. (Without the pub
,
bar
can only be seen inside the boo
module.)
# #![allow(unused_variables)] # #fn main() { // boo/mod.rs pub fn answer()->u32 { 42 } pub mod bar { pub fn question() -> &'static str { "the meaning of everything" } } #}
and then we have the question corresponding to the answer (the bar
module is inside boo
):
# #![allow(unused_variables)] # #fn main() { let q = boo::bar::question(); #}
That module block can be pulled out as boo/bar.rs
:
# #![allow(unused_variables)] # #fn main() { // boo/bar.rs pub fn question() -> &'static str { "the meaning of everything" } #}
And boo/mod.rs
becomes:
# #![allow(unused_variables)] # #fn main() { // boo/mod.rs pub fn answer()->u32 { 42 } pub mod bar; #}
In summary, modules are about organization and visibility, and this may or may not involve separate files.
Please note that use
has nothing to do with importing, and simply specifies visibility
of module names. For example:
# #![allow(unused_variables)] # #fn main() { { use boo::bar; let q = bar::question(); ... } { use boo::bar::question(); let q = question(); ... } #}
An important point to note is there is no separate compilation here. The main program and its
module files will be recompiled each time. Larger programs will take a fair amount of time to build,
although rustc
is getting better at incremental compilation.
Crates
The 'compilation unit' for Rust is the crate, which is either an executable or a library.
To separately compile the files from the last section,
first build foo.rs
as a Rust static library crate:
src$ rustc foo.rs --crate-type=lib
src$ ls -l libfoo.rlib
-rw-rw-r-- 1 steve steve 7888 Jan 5 13:35 libfoo.rlib
We can now link this into our main program:
src$ rustc mod4.rs --extern foo=libfoo.rlib
But the main program must now look like this, where the extern
name is the same
as the one used when linking. There is an implicit top-level module foo
associated
with the library crate:
// mod4.rs
extern crate foo;
fn main() {
let f = foo::Foo::new("hello");
println!("{:?}", f);
}
Before people start chanting 'Cargo! Cargo!' let me justify this lower-level look at building Rust. I'm a great believer in 'Know Thy Toolchain', and this will reduce the amount of new magic you need to learn when we look at managing projects with Cargo. Modules are basic language features and can be used outside Cargo projects.
It's time to understand why Rust binaries are so large:
src$ ls -lh mod4
-rwxrwxr-x 1 steve steve 3,4M Jan 5 13:39 mod4
That's rather fat! There is a lot of debug information in that executable. This is not a Bad Thing, if you want to use a debugger and actually want meaningful backtraces when your program panics.
So let's strip that debug information and see:
src$ strip mod4
src$ ls -lh mod4
-rwxrwxr-x 1 steve steve 300K Jan 5 13:49 mod4
Still feels a little large for something so simple, but this program links statically against
the Rust standard library. This is a Good Thing, since you can hand this executable to anyone
with the right operating system - they will not need a 'Rust runtime'. (And rustup
will even let
you cross-compile for other operating systems and platforms as well.)
 We
can hack our way to happiness, at least on Unix (yes, I know the best solution is a symlink.)
src$ export LD_LIBRARY_PATH=~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib
src$ ./mod4
Foo { s: "hello" }
Rust does not have a philosophical problem with dynamic linking, in the same way as Go does. It's just that when there's a stable release every 6 weeks it becomes inconvenient to have to recompile everything. If you have a stable version that Works For You, then cool. As stable versions of Rust get increasingly delivered by the OS package manager, dynamic linking will become more popular.
Cargo
The Rust standard library is not very large, compared to Java or Python; although much more fully featured than C or C++, which lean heavily on operating system provided libraries.
But it is straightforward to access community-provided libraries in crates.io using Cargo. Cargo will look up the correct version and download the source for you, and ensures that any other needed crates are downloaded as well.
Let's create a simple program which needs to read JSON. This data format is very widely used, but is too specialized for inclusion in the standard library. So we initialize a Cargo project, using '--bin' because the default is to create a library project.
test$ cargo init --bin test-json
Created binary (application) project
test$ cd test-json
test$ cat Cargo.toml
[package]
name = "test-json"
version = "0.1.0"
authors = ["Your Name<EMAIL_ADDRESS>[dependencies]
To make the project depend on the JSON crate, edit the 'Cargo.toml' file so:
[dependencies]
json="0.11.4"
Then do a first build with Cargo:
test-json$ cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading json v0.11.4
Compiling json v0.11.4
Compiling test-json v0.1.0 (file:///home/steve/c/rust/test/test-json)
Finished debug [unoptimized + debuginfo] target(s) in 1.75 secs
The main file of this project has already been created - it's 'main.rs' in the 'src' directory. It starts out just as a 'hello world' app, so let's edit it to be a proper test program.
Note the very convenient 'raw' string literal - otherwise we would need to escape those double quotes and end up with ugliness:
// test-json/src/main.rs extern crate json; fn main() { let doc = json::parse(r#" { "code": 200, "success": true, "payload": { "features": [ "awesome", "easyAPI", "lowLearningCurve" ] } } "#).expect("parse failed"); println!("debug {:?}", doc); println!("display {}", doc); }
You can now build and run this project - only main.rs
has changed.
test-json$ cargo run
Compiling test-json v0.1.0 (file:///home/steve/c/rust/test/test-json)
Finished debug [unoptimized + debuginfo] target(s) in 0.21 secs
Running `target/debug/test-json`
debug Object(Object { store: [("code", Number(Number { category: 1, exponent: 0, mantissa: 200 }),
0, 1), ("success", Boolean(true), 0, 2), ("payload", Object(Object { store: [("features",
Array([Short("awesome"), Short("easyAPI"), Short("lowLearningCurve")]), 0, 0)] }), 0, 0)] })
display {"code":200,"success":true,"payload":{"features":["awesome","easyAPI","lowLearningCurve"]}}
The debug output shows some internal details of the JSON document, but a
plain '{}', using the Display
trait, regenerates JSON from the parsed document.
Let's explore the JSON API.
It would not be useful if we could not extract values. The as_TYPE
methods
return Option<TYPE>
since we cannot be sure that the field exists or is of the correct type.
(see the docs for JsonValue)