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
blockName | 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 }
expression
must 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 mut
able.
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 &str
to_string()
on a &str
to convert it to a String
let 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 };
rect
is 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 };
rect
is 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
T
does 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::HasArea
forint
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
impl
for 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
bool
type 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:
Equal
not covered [E0004]
There's a "catch-all" pattern: _
.
match ordering { Less => println!("less"), _ => println!("not less") }
error: non-exhaustive patterns:
Equal
not 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 T
None
, 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) -> bool
fn is_none(&self) -> bool
fn unwrap_or(self, def: T) -> T
fn 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.
Iterator
s 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?