An Introduction to Rust
Disclaimer
I’m not actually a Rust developer!
There is a lot to cover.
If you get bored or overwhelmed, just yet at me!
The order of the slides may be confusing; its just hard to prioritize and there is just so much I would like to talk about.
History & Goals
- Developed by Mozilla to solve issues with usage of C++
- First announced at the Mozilla Summit 2010
(I was there :-) - Used to have a GC, and a runtime around libuv, and strange sigils for different pointer types
- Hit 1.0 in May, still evolving and stabilizing features
(you basically have to use the nightly for the cool stuff
Basics
- a very low level systems programming language with a lot of high level concepts
- you can use it to develop kernels (some do!)
- compiled; bare-metal performance
- zero-cost abstractions
- curly-brace syntax
- blocks as expressions
- focused on (memory) safety, speed and concurrency
- C FFI!
- you can use any C library
- you can use it to write libraries for any language that can use C-like libraries
- (some use it for ruby/python/node modules)
Cargo
- Official Package Manager / Build Tool
- makes it really easy to create, manage, build, run and test your *crate* (that’s what rust packages are called)
- https://crates.io/ is the official crate repository
- just reference any crate via semver and cargo will download, compile and link it to your crate
- you can also compile and install binaries locally
borrowck
- really simple in theory:
- you have one variable on the stack that owns a val
- you can transfer ownership (move)
- you can lend out (borrow) references:
- any number of read-only references
*OR* - one mutable reference
- any number of read-only references
- think of modern C++ with move semantics
- unsafe code can bend the rules
to create safe abstractions!
------
borrowck
borrowck pitfalls
- you have to think *a lot* about allocations etc
which is a good thing!
but a really annoying nontheless
- (sorry, no example)
a lot of times you have to introduce variable bindings because of borrowcks stupidity.
it simply won’t let you write one-liners sometimes, for no good reason
- there is an RFC for that: https://github.com/rust-lang/rfcs/issues/811
// Hello Rust!
fn main() {
println!("Hello Rust!");
}
Macros
- rust supports different kinds of syntax extensions
- macros or compiler plugins (only for nightly users)
- The "hello rust" example actually contains a macro!
Data Types
- Primitives, Tuples, Arrays (strings) (more later), Structs, Enums
- Pointers/References (more later)
- u8/i8, u16/i16, u32/i32, u64/i64, f32, f64
struct Foo {
foo: (u8, u8),
bar: u16,
}
Enums
- Union Types, Sum Types, Algebraic Data Types
- throw in a generic type parameter and you have a GADT (generalized ADT)
pub enum Option<T> {
None,
Some(T),
}
- Option is like Maybe in Haskell
- There’s also Result<T, E> { Ok(T), Err(E) }
like Either in Haskell
- there are no null pointers in (safe) rust, you use Option<T> instead
Match
impl<T> Option<T> {
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U> {
match self {
Some(x) => Some(f(x)),
None => None,
}
}
}
- you *must* explicitly match enums to use their contents!
- this makes error and None("null-pointer") handling explicit
- If you really don’t want to: Option and Result offer .unwrap()/.expect()
which basically means: just abort the program if I’m wrong, I don’t care!
- There is also try!() for Result
Traits
- they are an extremely powerful mechanism to add functionality to any type of data structure
- More powerful than classical Interfaces, a bit like Typeclasses
pub trait Read {
fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize> { ... }
fn read_to_string(&mut self, buf: &mut String) -> Result<usize> { ... }
fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> { ... }
fn by_ref(&mut self) -> &mut Self where Self: Sized { ... }
fn bytes(self) -> Bytes<Self> where Self: Sized { ... }
fn chars(self) -> Chars<Self> where Self: Sized { ... }
fn chain<R: Read>(self, next: R) -> Chain<Self, R> where Self: Sized { ... }
fn take(self, limit: u64) -> Take<Self> where Self: Sized { ... }
fn tee<W: Write>(self, out: W) -> Tee<Self, W> where Self: Sized { ... }
}
impl Read for Foo {
fn read(&mut self, but: &mut [u8]) -> Result<usize> {
// …
}
}
Method Overloading
trait Foo {
fn foo(&self) -> u32 {
1
}
}
struct Bar<T> {
bar: T,
}
impl Foo for u32 {}
impl<'a> Foo for &'a str {}
impl<T> Foo for Bar<T> {}
fn foo<T>(arg: T) -> u32 where T: Foo {
arg.foo()
}
fn main() {
println!("{}", foo(10));
println!("{}", foo("foo"));
println!("{}", foo(Bar{bar: Bar{bar: "bar"}}));
}
Operator Overloading
use std::ops::Add;
#[derive(Copy, Clone)]
struct Foo;
impl Add for Foo {
type Output = Foo;
fn add(self, _rhs: Foo) -> Foo {
println!("Adding!");
self
}
}
fn main() {
Foo + Foo;
}
How does thread safety work?
- there are two "marker" Traits: Send and Sync
- Send means the Type can be safely sent between threads (opt-out)
- Sync means that a type guarantees thread safety
- T: Sync implies &T: Send, in english:
if a type guarantees thread safety, you can send multiple (read only) references to different threads
impl Send for Arc<T>
impl !Send for Rc<T>
impl Sync for Mutex<T>
DST
- Dynamically Sized Types
- the compiler knows the size (in bytes) of some types at compile time: primitives, structs, enums, fixed-size arrays. Rust can use thin pointers to reference those
- others are only known at runtime: DSTs.
Rust uses fat pointers
(pointer + length / pointer + vtable) for
slices ( &[T] ) and Trait Objects ( &Trait ) respectively
- &str vs String
Static / Dynamic Dispatch
fn<T: Bar> foo(bar: &T) {
// …
}
// vs
fn foo(bar: &Bar) {
// …
}
Closing remarks
- I would love to actually do more Rust
- What I miss most in other languages (JS) since I tried Rust:
- ADTs, Traits, zero-cost-abstractions, and a lot more
- go on and read the awesome rust book!
An Introduction to Rust
By Arpad Borsos
An Introduction to Rust
- 1,713