Rust Romandie meetup + Hackers at EPFL
Dimiter Petrov & Romain Ruetschi
November 12, 2014
Rust is a fairly new systems programming language focused on safety and speed.
fn main() { println!("Hello, world!"); }
> Hello, world!
unsafe block| Name | Type | Example |
|---|---|---|
| Signed integer | int |
5i |
| Unsigned integer | uint |
5u |
| 8-bit uint | u8 |
5u8 |
| 16-bit uint | u16 |
5i |
| etc. | ||
| 32-bit float | f32 |
3.14f32 |
| 64-bit float | f64 |
3.14_f64 |
| Name | Type | Example | |
|---|---|---|---|
| Unit | () |
() |
|
| Boolean | bool |
true | false |
|
| Array | [T] |
[1, 2, 3]
| fixed size, can be allocated on either the stack or the heap |
| Slice | &[T] |
&[1, 2, 3]
| 'view' into an array, doesn't own the data it points to, but borrows it |
| Tuple | (A, B, C...) |
("Rust", 2006i, 'r') |
32-bit unsigned word |
| Name | Type | Example | |
|---|---|---|---|
| Unicode scalar value | char |
'a' |
32-bit unsigned word |
| Unicode scalar array | [char] |
['a', 'b', 'c'] |
~ UTF-32 string |
| Unicode string | str |
"rust is cool" |
array of 8-bit unsigned bytes ~ sequence of UTF-8 codepoints |
let x: int = 5; // type annotations are (usually) optional let x = 5i; let mut y = 2i; y += 1;
let x = 5i; x = x + 1; // error
let mut x = 5i; x = x + 1; // ok
let x = 5i; let mut y = x; y = y + 1;
fn main() { let res = do_stuff(2, 3); println!("result is {}", res); } fn do_stuff(a: int, b: int) -> int { let c = a + b; return c * 2; }
You can omit return if you want to return the last expression.
fn main() { let res = do_stuff(2, 3); println!("result is {}", res); } fn do_stuff(a: int, b: int) -> int { let c = a + b; c * 2 // ! no semicolon }
But you also need to omit the last semicolon, otherwise the function will return ().
fn main() { let name = "John"; let age = 42; println!("{} is {}", name, age); }
println!()is actually a macro, not a function.
You can re-order arguments by putting their index inside the braces.
fn main() { let name = "John"; let age = 42; println!("{1} is {0}", age, name); }
if age > 16 { println!("Have a beer!"); } else { println!("Sorry, no beer for you."); }
No parenthesis around the condition.
if/else is an expression:
let message = if health > 0 { "Good job" } else { "Game over" }; println!("{}", message);
while x < 10 { // do stuff }
Instead of
while true { // do stuff }
use
loop { // do stuff }
Rust's control-flow analysis treats this construct differently than a
while true, which leads to better generated code.
for x in range(0i, 10i) { // do stuff }
for var in expression { code }
expressionmust be anIterator(we'll talk about it later).
&str) or growable strings (String)&str)A pointer and a length. String slices are a 'view' into already allocated strings like string literals:
let hello = "Hello, world!";
String)String can grow if defined as mutable.
let mut hello = "Hello".to_string(); hello.push_str(", world!"); // hello now contains "Hello, world!"
as_slice() on a String to convert it to a &strto_string() on a &str to convert it to a Stringlet s1: String = "Hello".to_string(); let s2: &str = s1.as_slice();
Converting String to &str is cheap, converting &str to String involves an allocation.
An array is a fixed-sized list of elements of the same type.
// Only the first item needs a type suffix let a = [1i, 2, 3]; // which you can omit if you specify the type let a: [int, ..3] = [1, 2, 3]; println!("a has {} elements", a.len()); println!("the first element of a is {}", a[0]);
A vector is a dynamic, growable array
let mut participants = vec!["Bob", "Bill"]; participants.push("Joe"); // participants[2] == "Joe";
A slice is a reference to an array.
A slice allows safe and efficient access to a portion of an array without copying.
let a: [int, ..5] = [0, 1, 2, 3, 4]; let middle: &[int] = a.slice(1, 4); // just the elements [1, 2, 3] let a = [0i, 1, 2, 3, 4]; let middle = a.slice(1, 4); // just the elements [1, 2, 3]
struct Rectangle { width: f32, height: f32 }
Methods (static and members) are defined inside an impl block.
struct Rectangle { width: f32, height: f32 } impl Rectangle { // Methods here. }
Static methods are defined this way, and can be called with Type::method_name().
struct Rectangle { width: f32, height: f32 } impl Rectangle { fn new(width: f32, height: f32) -> Rectangle { Rectangle { width: width, height: height } } } fn main() { let rect = Rectangle::new(1.2, 4.9); println!("{}", react.area()); }
Member methods take a &self parameter, which is a reference to the struct the method is called on.
struct Rectangle { width: f32, height: f32 } impl Rectangle { fn area(&self) -> f32 { self.width * self.height } } fn main() { let rect = Rectangle::new(1.2, 4.9); let area = react.area(); println!("{}", area); }
struct PairOfInts { first: int, second: int } fn swap_pair(p: PairOfInts) -> PairOfInts { PairOfInts { first: p.second, second: p.first } }
struct Pair<T> { first: T, second: T } fn swap_pair<T>(p: Pair<T>) -> Pair<T> { Pair { first: p.second, second: p.first } } fn main() { let p = Pair { first: 1i, second: 2i }; let swapped = swap_pair(p); }
By default, values are allocated on the stack.
let rect = Rectangle { width: 1.0, height: 1.0 };
rectis of typeRectangle
You can allocate a value on the heap with the box keyword.
{
let x = box 5i;
println!("{}", *x); // Prints 5
}
{
int *x = (int *)malloc(sizeof(int));
if (!x) abort();
*x = 5;
printf("%d\n", *x);
free(x);
}
let rect = box Rectangle { width: 1.0, height: 1.0 };
rectis now typeBox<Rectangle>
Although it's of type Box<Rectangle>, you can still call Rectangle methods on rect.
let rect = box Rectangle::new(1.0, 1.0); let area = rect.area(); println!("area = {}", area);
Memory safety is enforced during compilation, there is no runtime overhead.
Rust is built around 3 concepts:
Time span during which a resource (value) is valid.

{
int *x = malloc(sizeof(int));
*x = 5;
free(x);
}
{
let x = box 5i;
// x gets deallocated here
}
When you pass data to a function (or even assign it to a variable), that data can be copied, moved, or borrowed (more about it soon).
fn main() { let a = 5; let b = add_one(a); println!("{}", a); } fn add_one(x: int) -> int { x + 1 }
struct Person { name: String, age: uint } fn main() { let john = Person { name: "John".to_string(), age: 42 }; show(john); // `john` has already been deallocated } fn show(person: Person) { println!("{} is {}", person.name, person.age); // `john` will be deallocated here }
struct Person { name: String, age: uint } fn main() { let john = Person { name: "John".to_string(), age: 42 }; show(john); show(john); } fn show(person: Person) { println!("{} is {}", person.name, person.age); // `john` will be deallocated here }
error: use of moved value: `john`
show(john);
^
note: `john` moved here because it has type `Person`, which is non-copyable
show(john);
^
error: aborting due to previous error
struct Person { name: String, age: uint } fn main() { let john = Person { name: "John".to_string(), age: 42 }; // `john` moves into show here show(john); // `john` is deallocated by now // so the next line doesn't compile show(john); } fn show(person: Person) { println!("{} is {}", person.name, person.age); // `john` will be deallocated here }
Assigning a value to a variable is similar to passing it to a function.
struct Person { name: String, age: uint } fn main() { let john = Person { name: "John".to_string(), age: 42 }; // `john` moves into `john_bis` here let john_bis = john; // so the next line won't compile show(john); } fn show(person: Person) { println!("{} is {}", person.name, person.age); // `john` will be deallocated here }
When you create a resource, you're the owner of that resource.
Being an owner gives you some privileges:
But it also comes with some restrictions:
You may lend that resource, immutably, to as many borrowers as you'd like.
struct Person { name: String, age: uint } fn main() { let john = Person { name: "John".to_string(), age: 42 }; // `john` moves into `show`. show(&john); // `show` hands us `john` back. show(&john); // the previous line will thus compile. } fn show(person: &Person) { println!("{} is {}", person.name, person.age); }
John is 42
John is 42
You may lend that resource, mutably, to a single borrower.
struct Person { name: String, age: uint } fn main() { let mut john = Person { name: "John".to_string(), age: 42 }; grow_older(&mut john); show(&john); // John is 43 } fn grow_older(person: &mut Person) { person.age += 1; }
The following will compile too, as grow_older gives us john back:
fn main() { let mut john = Person { name: "John".to_string(), age: 42 }; show(&john); // John is 42 grow_older(&mut john); grow_older(&mut john); show(&john); // John is 44 }
But this won't:
fn main() { let mut john = Person { name: "John".to_string(), age: 42 }; let mut john_bis = &mut john; grow_older(&mut john); show(&john); }
error: cannot borrow `john` as mutable more than once at a time
grow_older(&mut john);
^
note: previous borrow of `john` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `p` until the borrow ends
let mut john_bis = &mut john;
^
note: previous borrow ends here
fn main() {
...
}
^
error: aborting due to previous error
fn main() { let mut john = Person { name: "John".to_string(), age: 42 }; // first borrow of `john` as mutable let mut john_bis = &mut john; // `john` cannot be mutably borrowed again, won't compile grow_older(&mut john); show(&john); }
Let's say we want to return a reference to a struct's member field.
struct Point { x: f64, y: f64 } fn get_x(point: &Point) -> &f64 { &point.x }
Until a few weeks ago, get_x() had to be written this way:
fn get_x<'a>(point: &'a Point) -> &'a f64 { &point.x }
But in that case, those annotations are now optional.
'a represents the lifetime of point. We hereby specify that the reference we return must have the same lifetime as point.
This means that code such as
let mut p = Point { x: 2.0, y: 3.0 }; let x = get_x(&p); p = Point { x: 4.0, y: 6.0 };
will not compile, because x outlives the value it is reference from.
struct Line { start: &Point, end: &Point } fn make_line(p1: &Point, p2: &Point) -> Line { Line { start: p1, end: p2 } } fn main() { let p1 = Point { x: 1.0, y: 2.3 }; let p2 = Point { x: 2.0, y: 5.9 }; let line = make_line(&p1, &p2); }
error: missing lifetime specifier
struct Line<'a> { start: &'a Point, end: &'a Point } fn make_line<'b>(p1: &'b Point, p2: &'b Point) -> Box<Line<'b>> { box Line { start: p1, end: p2 } } fn main() { let p1 = Point { x: 1.0, y: 2.3 }; let p2 = Point { x: 2.0, y: 5.9 }; let line = make_line(&p1, &p2); }
let xs = vec!(1i, 2i, 3i);
let ys = xs.map(|x| x * 2);
// ys == [2i, 4i, 6i]
fn add_one(x: int) -> int { x + 1 } fn apply(x: int, adder: |int| -> int) -> int { adder(x) }
let factor = 8;
let xs = vec!(1i, 2i, 3i);
let ys = xs.map(|x| x * factor);
// ys == [8i, 16i, 24i]
traits.A trait is a sort of interface that defines some behavior. If a type implements a trait, that means that it supports and implements the behavior the trait describes.
You can think of them as (better) Java interfaces. They are in fact very similar to Haskell's typeclasses.
One of the main differences is that you can define a trait implementation separately from the struct definition, even in another module. This means that you can eg. implement a trait you defined yourself for a type provided by a library.
They're also much more powerful (but won't get into too much detail here).
struct Rectangle { width: f32, height: f32 } trait HasArea { fn area(&self) -> f32; } impl HasArea for Rectangle { fn area(&self) -> f32 { self.width * self.height } }
fn main() { let rect = Rectangle { width: 1.2, height: 4.9 }; println!("{}", react.area()); }
struct Circle { radius: f32 } impl HasArea for Circle { fn area(&self) -> f32 { std::f32::consts::PI * (self.radius * self.radius) } }
fn print_area<T>(shape: T) { println!("This shape has an area of {}", shape.area()); }
error: type
Tdoes not implement any method in scope namedarea
fn print_area<T: HasArea>(shape: T) { println!("This shape has an area of {}", shape.area()); }
fn print_area<T: HasArea>(shape: T) { println!("This shape has an area of {}", shape.area()); } fn main() { let c = Circle { radius: 1.0 } let r = Rectangle { width: 3.0, height: 2.0 } print_area(c); print_area(r); }
This shape has an area of 3.141592654
This shape has an area of 6.0
print_area(10i);
error: failed to find an implementation of trait
main::HasAreaforint
We can implement traits for any type. So this would work, even if it makes no sense:
impl HasArea for int { fn area(&self) -> f64 { println!("this is silly"); *self as f64 } } fn main() { 10i.area(); print_area(10i); }
Implementing traits for primitive types should generally be avoided.
One restriction:
Either the trait or the type you're writing the
implfor must be inside your crate (i.e. your library).
trait ToString { fn to_string(&self) -> String; } trait ToJson { fn to_json(&self) -> Json; } trait Equiv<T> { fn equiv(&self, other: &T) -> bool; }
sum types in the literature.data and Scala's case class.enum Boolean { True, False }
Rust's
booltype is not implemented this way as it is a primitive.
enum Boolean { True, False } let b: Boolean = True;
A value of type Boolean can be either True or False.
From the standard library (almost):
enum Ordering { Less, Equal, Greater } fn cmp(a: int, b: int) -> Ordering { if a < b { Less } else if a > b { Greater } else { Equal } } let ordering = cmp(x, y); if ordering == Less { println!("less"); } else if ordering == Greater { println!("greater"); } else if ordering == Equal { println!("equal"); }
Rust provides pattern matching, which lets you rewrite this:
if ordering == Less { println!("less"); } else if ordering == Greater { println!("greater"); } else if ordering == Equal { println!("equal"); }
as this:
match ordering { Less => println!("less"), Greater => println!("greater"), Equal => println!("equal") }
It also works with primitives:
let i = 5i; match i { 0 => println!("zero"), 1 => println!("one"), _ => println!("> 1") }
Patterns must be exhaustive:
match ordering { Less => println!("less"), Greater => println!("greater") }
error: non-exhaustive patterns:
Equalnot covered [E0004]
There's a "catch-all" pattern: _.
match ordering { Less => println!("less"), _ => println!("not less") }
error: non-exhaustive patterns:
Equalnot covered [E0004]
Enums can also store data. One simple example is the built-in Option type. Here's how it is defined in the standard library:
enum Option<T> { None, Some(T), }
The type Option<T> represents an optional value of type T.
An Option is either
Some, and contains a value of type TNone, and does not contain anythinglet opt: Option<int> = Some(5); let opt_plus_two = opt + 2;
error: binary operation
+cannot be applied to typecore::option::Option<int>
To make use of the value inside an Option, we must pattern-match on it:
let opt: Option<int> = Some(5); let opt_plus_two = match opt { Some(x) => x + 2, None => -1 }
This forces us to handle the case where there might be no value.
Options have a number of uses in Rust:
Option also provides a few convenience methods:
fn is_some(&self) -> boolfn is_none(&self) -> boolfn unwrap_or(self, def: T) -> Tfn unwrap(self) -> T Use with cautionA couple more:
fn map<U>(self, f: |T| -> U) -> Option<U>fn and_then<U>(self, f: |T| -> Option<U>) -> Option<U>fn get_name() -> Option<String> { /* ... */ } let name: Option<String> = get_name(); let display_name = name.map(|n| format!("My name is {}", n)) .unwrap_or("I don't have a name"); println!(display_name);
If get_name() returns Some("Marie"), this will print My name is Marie, and if it returns None, this will print I don't have a name.
for x in range(0i, 10i) { // do stuff }
This works because range(0, 10) returns an Iterator<int>.
Iterator<T> provides a next() function that we can call repeatedly to get a sequence of values, each wrapped in Some. When no more values are available, next() returns None.
The for loop on the previous slide can be written like this:
let mut range = range(0i, 10i); loop { match range.next() { Some(x) => { println!("{}", x); }, None => { break } } }
Vectors can be iterated over too. Vec<T> provides an iter() method which returns an Iterator<&T> that we can use to iterate over the elements.
let nums = vec![1i, 2i, 3i]; for num in nums.iter() { println!("{}", num); }
Iterator provides methods such as map, filter, take, and friends.
Iterators are not collections, they just allow to iterate over a (potentially infinite) sequence of elements.
It is possible to turn an Iterator into a collection with the collect method.
let a = [1i, 2, 3, 4, 5]; let iter = a.iter().map(|&x| x + 1); let a_plus_one: Vec<int> = iter.collect(); // a_plus_one = [2i, 3, 4, 5, 6];
The type annotation is mandatory. let a_plus_one = iter.collect(); would throw an error.
You have already seen them. println!() is a macro. They're distinguishable by the ! at the end of the function name.
We won't get into too much detail here for lack of time.
A "crate" in Rust is what you'd call a "package" or "library" in other languages. A crate contains modules (which can contain other modules).
If we have a greetings crate that contains a public module english that defines a public method hello(), we'd use it like this:
extern crate greetings; fn main() { greetings::english::hello(); }
The use keyword imports names in the local scope.
extern crate greetings; use greetings::english; fn main() { english::hello(); }
Cargo is Rust's package manager. It:
Use Cargo for all your projects.
$ cargo new hello_world --bin
The --bin flag indicates you're making a binary, not a library.
This generates a package description file, Cargo.toml, where you declare the project's dependencies and metadata.
[package] name = "hello_world" version = "0.0.1" authors = ["Your Name <you@example.com>"]
src/main.rs:
fn main() { println!("Hello, world!") }
cargo build to build or cargo run to build and run your project
$ cargo run
Compiling hello_world v0.0.1 (file:///path/to/hello_world)
Running `target/hello_world`
Hello, world!
Questions?