Neophyte Rustacean
One month with Rust
My background
Do Java as a job (working on a EE server, JBoss)
Learning Haskell over the past 2 years, some small things
Know Scala, C/C++ and a few other languages
Science/math background, interesting in getting industry to actually use all the awesome things that have been invented.
My background - Rust
I had been reading blog posts and articles about Rust for 18 months or so.
I had never tried to use it until the start of June, after volunteering to talk about it at BFPG!
Today
- What is Rust?
- Quick introduction to basics
- The interesting parts
- Traits
- References/Borrowing
- My first month
- Where to now?
Many examples shamelessly borrowed from the Rust Book
What is Rust?
- Personal project by Graydon Hoare, sponsored by Mozilla since 2009, announced in 2010, and self-hosting compiler in 2011, and 1.0 released in May 2015
- Has gone though many "eras" of development, with massive changes in the language (especially types)
- Designed as a "systems language", it is strict and impure, but maintains memory-safety
- Not garbage collected, has ownership system
- Pays some attention to research (IMHO)
What is Rust?
- Aimed at replacing C/C++ (arguable competitor to Go)
- Zero-cost abstraction
- Interoperability with C-compatible code
- Efficient
- Immutable bindings by default
Rust is memory-safe (outside of unsafe blocks):
- No use-after-free or double-free
- No data races
- When multiple pointers access the same memory without synchronisation, and at least one can write
What is Rust? Changes
- Rust has had a few 'eras' which are fairly different
- The large changes between Rust eras are the features not the goals.
- Many experiments to see what would be useful and good.
- Mozilla's "Servo" HTML renderer is a test-bed for much of Rust.
Rust follows Firefox-style release cycle: every 6 weeks master becomes beta, old beta becomes release.
What is Rust? Community
The Rust language and standard library is changed with the use of RFCs. (GitHub issues). Core developers use this too and open for public comment
The community is around a third each of systems language people (C/C++ etc), scripting language people and functional language people. Very open to ideas from FP, as long as it fits with the goals.
Rust Basics - book example
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin().read_line(&mut guess)
.ok()
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
Rust Basics - features
Rust has many of the expected things:
- Pattern matching (with guards) / destructuring
- Tuples
- if, for/while loops
- Higher order functions
- newtype (working on generalised newtype deriving)
Rust types
- Large variety of "primitive" types (u8, i32, f64, etc.)
- structs
- enums
- Rust types can be parameterised over types and lifetimes (more on lifetimes later)
- No higher-kinded types. Yet.
Be careful reading Rust information online, much of it is about earlier versions.
Rust types
struct Point {
x: i32,
y: i32,
}
fn main() {
let origin = Point { x: 0, y: 0 }; // origin: Point
println!("The origin is at ({}, {})", origin.x, origin.y);
}
fn main() {
let mut point = Point { x: 0, y: 0 };
point.x = 5;
println!("The point is at ({}, {})", point.x, point.y);
}
Rust types
struct Point {
mut x: i32, // not valid
y: i32,
}
enum Message {
Quit,
ChangeColor(i32, i32, i32),
Move { x: i32, y: i32 },
Write(String),
}
Rust types
enum Option<T> {
Some(T),
None,
}
fn takes_two_of_the_same_things<T>(x: T, y: T) {
// ...
}
Traits
struct Circle {
x: f64,
y: f64,
radius: f64,
}
trait HasArea {
fn area(&self) -> f64;
}
impl HasArea for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}
fn main() {
let c = Circle { x=0.0, y=0.0, radius=3.0 };
let a = c.area()
}
Traits
Traits are very heavily used in Rust:
- Core language parts (e.g. Copy, Deref) - more later
- Operator overloading (e.g. Add, Range)
- Collections
From my reading they seem reasonably well designed
- but haven't used them that much
Stolen many ideas from Haskell typeclasses
Traits
Traits can have associates types (for fundeps) and consts
Where clauses are useful for simplification (and more)
impl <A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for YourType {}
impl <A, D> MyTrait<A, D> for YourType where
A: TraitB + TraitC,
D: TraitE + TraitF {}
pub trait Mul<RHS = Self> {
type Output;
fn mul(self, rhs: RHS) -> Self::Output;
}
// Mul<f32> for f32. Output=f32
// Mul<f32> for Cmplx<f32>, scalar multiply. Output=Cmplx<f32>
// Mul<Cmplx<f32>> for Cmplx<f32>, complex multiply. Output=Cmplx<f32>
References and lifetimes
Rust's usage of lifetimes and references is arguably the hardest part, but also what makes the language what it is.
Related to uniqueness typing, either one mutable reference OR zero or more immutable references.
Ownership
- Bindings own their data, and it is freed when the binding goes out of scope.
- When a value is used, the ownership moves
let v = vec![1, 2, 3];
let v2 = v;
println!("v[0] is: {}", v[0]);
error: use of moved value: `v`
println!("v[0] is: {}", v[0]);
Ownership - Copy trait
For some types, that isn't needed. The Copy trait indicates that it is safe to copy the bits without compromising memory safety.
Privative types have it, and structures that do not contain references can safely implement it.
#[derive(Copy)]
struct Point {
x: f64,
y: f64,
}
Ownership - functions
Passing a parameter to a function makes it take ownership
You could pass it back explicitly, but that would be tedious
fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
// do stuff with v1 and v2
// hand back ownership, and the result of our function
(v1, v2, 42)
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let (v1, v2, answer) = foo(v1, v2);
Ownership - borrowing
Rather than do that, use a reference parameter to borrow the value from the caller. Does not de-allocate when a reference goes out of scope.
fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
// do stuff with v1 and v2
// just return the result like in any sensible language
42
}
let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];
let answer = foo(v1, v2);
Ownership - borrowing
Can use &mut to get a mutable reference.
For safety, cannot have a mutable borrow in scope with any immutable borrowings
Borrowings last until the end of the scope.
Ownership - borrowing
These rules prevent iterator invalidation, or use-after-free
let mut v = vec![1, 2, 3];
for i in &v {
println!("{}", i);
v.push(34);
}
error: cannot borrow `v` as mutable because it is also borrowed as immutable
v.push(34);
^
note: previous borrow of `v` occurs here; the immutable borrow prevents
subsequent moves or mutable borrows of `v` until the borrow ends
for i in &v {
^
note: previous borrow ends here
for i in &v {
println!(“{}”, i);
v.push(34);
}
Ownership - lifetimes
All references have a lifetime which describes how long they are valid for, and they cannot be used outside that lifetime. Often implicit, they can be made explicit
However when in structs, lifetimes must be explicit
fn foo(x: &i32) { // implicit
}
fn bar<'a>(x: &'a i32) { // explicit
}
Miscellanea - concurrency
Memory safety for data race prevention helps a lot for doing concurrency safely.
Two important traits:
- Send - things that can be moved to another thread
- Sync - things that can safely be used by multiple threads at once
Miscellanea - memory
A very important thing I have not mentioned yet is dynamic memory allocation. Returning a structure to the caller is common.
Box is an important type which represents a heap-allocated structure. Only one reference at one
Rc is a reference-counted structure
Arc is an atomic reference-counted structure, so Sync
There is no GC now, but one could be added like Rc/Arc
What Rust doesn't have
- Guaranteed tail-call optimisation
- Makes lifecycle handling even more complex
- Makes deterministic description more complex
- ABI incompatibility with C
- Compiler can (and does) optimise using it
- Garbage Collection
- Requires a runtime
- Mostly subsumed by references and Rc/Arc
- People working on library for it, like Rc/Arc
My month with Rust
- Look at the Rust Book and Rust By Example site
- Tried a few trivial things
- Tried to rewrite a simple log-analysis tool in Rust
- Rewritten several times in several languages now. Haskell was the last
My month with Rust
Starting off with the basic tutorials and usual "getting started" material is simple, but by following along you don't have to figure out things by yourself.
You then try a small app yourself, and suddenly you are eaten by a grue the borrow checker.
My month with Rust - what?
src/main.rs:53:23: 53:41 error: type mismatch: the type `fn(
collections::string::String, parser_combinators::primitives::State<_>)
-> core::result::Result<(f64,
parser_combinators::primitives::Consumed
<parser_combinators::primitives::State<_>>),
parser_combinators::primitives::Consumed
<parser_combinators::primitives::ParseError<char>>>
{parseTimestamp}` implements the trait
`core::ops::FnOnce<(collections::string::String,
parser_combinators::primitives::State<_>)>`, but the trait
`core::ops::FnOnce<(parser_combinators::primitives::State<_>,)>` is required
(expected a tuple with 1 elements, found one with 2 elements) [E0281]
src/main.rs:53 (tse).or(unknown).parse_state(input)
My month with Rust - string
My first big problem was with strings - Rust has two types:
- String, which is a heap-allocated string
- &str, which is a slice of characters
When you create a string you'll normally end up with a String, but most things take &str for flexability.
You only need an & character to turn the String into a &String, and the compiler can turn a &String into a &str (deref coercion), but you need to know that.
My month with Rust - errors
Rust has two types of error, failures (Result) and panics.
In Haskell terms, Either and exceptions.
Result is widely used, which is good.
The 'try!' macro automates chaining potentially failing operations, but knowing some FP it feels like it should be generalised (i.e. a Monad), but no Higher-Kinded Types yet
My month with Rust - parser
Several parsing libraries exist:
- parser_combinators based on Parsec
- used Parsec in the haskell version, but struggled due to lack of function composition, and resulting code felt hacky
- nom uses Incomplete/Done/Error state machine
- Fairly verbose
The library ecosystem feels in early 'inflationary' stage.
People figuring out best ideas, dealing with Rust changes
My month with Rust - refs
Map, dictionary, hash, whatever you want to call it, the data structure is more awkward to use in Rust.
Who owns the data, the creator? the map? shared?
So you want to build one up incrementally, using the existing contents along the way? That sounds like mutable and immutable references at the same time...
You need to think clearly about it to write the code
What I've skipped
- Cargo build tool
- Crates for package distribution
- Most of the standard library
- Many language bits
- Lots of detail
Should you look at Rust?
I think Rust is very interesting, and regardless of how well it goes, we will learn about how well some of these ideas work in practice.
The ecosystem is starting to form, we have a big opportunity to get good ideas (from FP or elsewhere) into the Rust world.
Learning any language changes the way you think, why not Rust?
Current disadvantages
- Due to the many changes over time, a lot of information on the Internet is wrong
- Still at the early stages of good practices and library designs
- Some features got pushed back to get 1.0 out
- But they are still being considered
- Tooling still needs work
- 1.1 has a --explain rustc option for more detailed error messages
Rusting on hack night?
Explore rust with game development:
http://jadpole.github.io/arcaders/2015/07/04/arcaders-1-0.html
Choose a simple tool and write it
Rewriting GNU core utils in Rust
https://github.com/uutils/coreutils
Links
http://www.rust-lang.org/
http://doc.rust-lang.org/stable/book/
http://rustbyexample.com/
http://zsiciarz.github.io/24daysofrust/
Neophyte Rustacean
By doctau
Neophyte Rustacean
- 2,548