Intro to Rust for Substrate Developers Or: how I learned to stop - - PowerPoint PPT Presentation

intro to rust for substrate developers
SMART_READER_LITE
LIVE PREVIEW

Intro to Rust for Substrate Developers Or: how I learned to stop - - PowerPoint PPT Presentation

Intro to Rust for Substrate Developers Or: how I learned to stop worrying and love lifetimes Maciej Hirsz Software developer @ Parity Technologies Ltd. maciej@parity.io | @MaciejHirsz Who is this for? Coming C / C++ or Python / Ruby / JS


slide-1
SLIDE 1

Or: how I learned to stop worrying and love lifetimes

Maciej Hirsz Software developer @ Parity Technologies Ltd. maciej@parity.io | @MaciejHirsz

Intro to Rust for Substrate Developers

slide-2
SLIDE 2

Who is this for?

  • Coming C / C++ or Python / Ruby / JS
  • Completely new or beginner at Rust
  • Want to work on Substrate modules or ink!
  • Might have something for intermediate folks
  • Discover the unknown unknowns
slide-3
SLIDE 3

What we are going to cover here

  • Rust philosophy
  • Rust primitives and value types
  • Error handling
  • Implementing methods
  • Trait system
  • Lifetimes

A COVER, GET IT?

slide-4
SLIDE 4

What we are NOT going to cover here

  • Closures
  • Multithreading, Mutexes, MPSC message passing
  • Unsafe Rust
  • Macros
  • How types are represented in memory and more
slide-5
SLIDE 5

Rust philosophy

Performance Safety

C, C++ Java JS, Python

slide-6
SLIDE 6

Rust philosophy

Performance Safety

C, C++ Java JS, Python

slide-7
SLIDE 7

Rust philosophy

Performance Safety

C, C++ Java JS, Python

slide-8
SLIDE 8
  • Safe
  • Concurrent
  • Fast
  • Pick Three

Rust philosophy

http://leftoversalad.com/c/015_programmingpeople/

slide-9
SLIDE 9

Rust philosophy

  • No Runtime overhead, no GC, C FFI
  • Zero-Cost abstractions (like C++)
  • Unique Ownership model (RAII)
  • Will hurt your feelings
  • Will empower you

David Baron, Mozilla SF

slide-10
SLIDE 10

Rust philosophy

fn bunch_of_numbers() -> Vec<u32> { let mut nums = Vec::new(); for i in 0..10 { nums.push(i); } nums } fn main() { let nums = bunch_of_numbers(); match nums.last() { Some(&0) => println!("Last number is zero"), Some(n) => println!("Last number is {}", n), None => println!("There are no numbers"), } }

slide-11
SLIDE 11

Rust philosophy

fn bunch_of_numbers() -> Vec<u32> { let mut nums = Vec::new(); for i in 0..10 { nums.push(i); } nums } fn main() { let nums = bunch_of_numbers(); match nums.last() { Some(&0) => println!("Last number is zero"), Some(n) => println!("Last number is {}", n), None => println!("There are no numbers"), } } (re-)allocation move

  • btain ownership

deallocation

slide-12
SLIDE 12

Rust philosophy

fn bunch_of_numbers() -> Vec<u32> { let mut nums = Vec::with_capacity(10); for i in 0..10 { nums.push(i); } nums } fn main() { let nums = bunch_of_numbers(); match nums.last() { Some(&0) => println!("Last number is zero"), Some(n) => println!("Last number is {}", n), None => println!("There are no numbers"), } }

slide-13
SLIDE 13

Rust philosophy

fn bunch_of_numbers() -> Vec<u32> { let mut nums = Vec::with_capacity(10); for i in 0..10 { nums.push(i); } nums } fn main() { let nums = bunch_of_numbers(); match nums.last() { Some(&0) => println!("Last number is zero"), Some(n) => println!("Last number is {}", n), None => println!("There are no numbers"), } } allocation

  • btain ownership

move deallocation

slide-14
SLIDE 14

Rust philosophy

fn bunch_of_numbers() -> Vec<u32> { (0..10).collect() } fn main() { let nums = bunch_of_numbers(); match nums.last() { Some(&0) => println!("Last number is zero"), Some(n) => println!("Last number is {}", n), None => println!("There are no numbers"), } }

slide-15
SLIDE 15

Rust philosophy

fn bunch_of_numbers() -> Vec<u32> { (0..10).collect() } fn main() { let nums = bunch_of_numbers(); match nums.last() { Some(&0) => println!("Last number is zero"), Some(n) => println!("Last number is {}", n), None => println!("There are no numbers"), } } allocation + move

  • btain ownership

deallocation

slide-16
SLIDE 16

Intermission

Questions so far?

slide-17
SLIDE 17

Primitives

  • Boolean type: bool (true, false)
  • Unicode codepoint: char (4 bytes, '❤')
  • Unsigned integers: u8, u16, u32, u64, u128, usize
  • Signed integers: i8, i16, i32, i64, i128, isize
  • IEEE floating point numbers: f32, f64
slide-18
SLIDE 18

Choosing the right number type

  • Need floating point? f64, use f32 for games
  • Need a length or index into array? usize
  • Need negative integers? Smallest usable: i8 - i128
  • No negative integers? Smallest usable: u8 - u128
  • Bytes are always u8
  • isize is rarely used (pointer arithmetic)
slide-19
SLIDE 19

BLOCKCHAIN ACHTUNG!

f64 and f32 are verboten! They are not deterministic across platforms.

slide-20
SLIDE 20

Simple value types

  • Array (sized): [T; N] eg: [u8; 5]
  • Tuple: (T, U, ...), eg: (u8, bool, f64)
  • “Void” tuple: (), default return type
slide-21
SLIDE 21

Slices

  • Similar to arrays, but “unsized” (size unknown to compiler)
  • [T] eg: [u8], in practice mostly: &[u8]
  • String slice: str, in practice mostly: &str
slide-22
SLIDE 22

Slices

let mut foo = [0u8; 5]; foo[1] = 1; foo[2] = 2; let bar = &foo[..3]; // [u8] length of 3 println!("{:?}", bar); // [0, 1, 2]

slide-23
SLIDE 23

Type inference

let foo = 10; let bar: &str = "Hello SubZero"; let baz: &[u8; 13] = b"Hello SubZero"; let tuple: (u8, bool) = (b'0', true); let heart: char = '❤';

slide-24
SLIDE 24

Type inference

let foo = 10u32; // Would default to i32 let bar = "Hello SubZero"; let baz = b"Hello SubZero"; let tuple = (b'0', true); let heart = '❤';

slide-25
SLIDE 25

Structs

struct Foo; // 0-sized struct Bar(usize, String); // Tuple-like struct Baz { // With field names id: usize, name: String, // Owned, growable str }

slide-26
SLIDE 26

Structs

let baz = Baz { id: 42, name: "Owned Name".to_owned(), }; // Access fields by names println!("Id {} is {}", baz.id, baz.name); // Id 42 is Owned Name

slide-27
SLIDE 27

Enums

  • Like structs, but value is always one of many variants
  • Stack size is largest variant + tag
  • Values accessed by pattern matching
slide-28
SLIDE 28

Enums

enum Animal { Cat, Dog, Fish, } let animal = Animal::Dog;

slide-29
SLIDE 29

Enums

enum Number { Integer(i64), // Tuple-esque variants Float { // Variant with fields inner: f64 }, } let a = Number::Integer(10); let b = Number::Float { inner: 3.14 };

slide-30
SLIDE 30

Enums

// Match expression match a { Number::Integer(n) => println!("a is integer: {}", n), Number::Float { inner } => println!("a is float: {}", inner), } // If-let if you want to check for a single variant if let Number::Float { inner } = b { println!("b is float: {}", inner); }

slide-31
SLIDE 31

Intermission

Questions so far?

slide-32
SLIDE 32

Error handling

  • Rust differentiates between errors and panics
  • Errors are explicit, Rust will force you to handle them
  • Panics cause thread to shut down unexpectedly and are

almost always result of assumptions being violated

  • When coding for Substrate your code should never panic,

there are tools to check for that

slide-33
SLIDE 33

Error handling

  • Rust uses two built-in types to handle errors
  • Option is either Some(T) or None and replaces null
  • Result is either Ok(T) or Err(U) and replaces what

would be exceptions in other languages

  • We can propagate errors using the ? operator
slide-34
SLIDE 34

Error handling

enum Option<T> { Some(T), None, } enum Result<T, U> { Ok(T), Err(U), }

slide-35
SLIDE 35

Error handling

fn add_numbers(numbers: &[i32]) -> i32 { let a = numbers[0]; let b = numbers[1]; a + b }

slide-36
SLIDE 36

Error handling

fn add_numbers(numbers: &[i32]) -> i32 { let a = numbers[0]; // can panic! let b = numbers[1]; // can panic! a + b // can panic (debug build) } // or do wrapping addition (release build)

slide-37
SLIDE 37

Error handling

fn add_numbers(numbers: &[i32]) -> Option<i32> { let a = numbers.get(0)?; // `get` returns Option<i32> let b = numbers.get(1)?; // ? will early return on None a.checked_add(b) // returns None on overflow }

slide-38
SLIDE 38

Error handling

fn add_numbers(numbers: &[i32]) -> i32 { let a = numbers.get(0).unwrap_or(0); // 0 for None let b = numbers.get(1).unwrap_or(0); // 0 for None a.saturating_add(b) // Caps to max value on overflow }

slide-39
SLIDE 39

Error handling

use std::io; use std::fs::File; fn read_file() -> Result<String, io::Error> { let mut file = File::open("./test.txt")?; let mut content = String::new(); file.read_to_string(&mut content)?; // Err early returns Ok(content) }

slide-40
SLIDE 40

Error handling

use std::io; use std::fs::File; fn read_file() -> io::Result<String> { // Alias type let mut file = File::open("./test.txt")?; let mut content = String::new(); file.read_to_string(&mut content)?; // Err early returns Ok(content) }

slide-41
SLIDE 41

Error handling

use std::io; use std::fs::File; fn read_file() -> Option<String> { // Result to Option let mut file = File::open("./test.txt").ok()?; let mut content = String::new(); file.read_to_string(&mut content).ok()?; Some(content) }

slide-42
SLIDE 42

Intermission

Questions so far?

slide-43
SLIDE 43

Implementing methods

  • Using impl keyword
  • Can be done for local enums and structs
  • Can have multiple impl blocks for any type
  • Each block can have different trait bounds for generics
slide-44
SLIDE 44

Implementing methods

struct Duck { name: String, } impl Duck { fn new(name: &str) -> { // Convention, there are no constructors Duck { name: name.into() } } fn quack(&self) { // equates to `self: &Duck`, must be the first argument println!("{} quacks!", self.name); } }

slide-45
SLIDE 45

Implementing methods

let bob = Duck::new("Bob"); // associated function bob.quack(); // automatically borrows Duck::quack(&bob); // same as above, explicit borrow

slide-46
SLIDE 46

Implementing methods

impl Duck { fn borrowing(&self) { } fn mut_borrowing(&mut self) { } fn taking_ownership(self) { // drops the instance } }

slide-47
SLIDE 47

Implementing methods

struct Duck<Name> { name: Name, } impl<Name> Duck<Name> { fn new(name: Name) -> { Duck { name } // Shorthand syntax, like JS ¯\_(ツ)_/¯ } }

slide-48
SLIDE 48

Implementing methods

// Implement for Duck with a Name, // where the Name implements Display impl<Name: Display> Duck<Name> { fn quack(&self) { println!("{} quacks!", self.name); } }

slide-49
SLIDE 49

Implementing methods

// create a Duck<&str> let bob = Duck::new("Bob"); bob.quack(); // &str implements Display // so this is cool

slide-50
SLIDE 50

Intermission

Questions so far?

slide-51
SLIDE 51

Trait system

  • Add methods and associated functions to types
  • Extremely expressive when combined with generics
  • Only active when imported in-scope
  • You can implement your traits to external types,
  • Or external traits to your types
slide-52
SLIDE 52

Trait system

Some built-in traits:

  • Default: create the type with default value
  • From: create a type from another type
  • Into: convert self into another type
  • Display: provide formatting for “pretty” terminal printing
  • Debug: provide formatting for debug terminal printing
slide-53
SLIDE 53

Trait system

struct Foo(usize); impl Default for Foo { fn default() -> Foo { Foo(0) } }

slide-54
SLIDE 54

Trait system

struct Foo(usize); impl Default for Foo { fn default() -> Foo { Foo(usize::default()) // Use Default for usize } }

slide-55
SLIDE 55

Trait system

struct Foo(usize); impl Default for Foo { fn default() -> Foo { Foo(Default::default()) // Have compiler find impl } }

slide-56
SLIDE 56

Trait system

// Replaces all code from previous slide #[derive(Default)] struct Foo(usize);

slide-57
SLIDE 57

Trait system

// From is one of the most useful built-in traits trait From<Other> { // `Other` is generic, `Self` is a special type fn from(other: Other) -> Self; // No function body, although default implementation // could be provided }

slide-58
SLIDE 58

Trait system

enum Count { Zero, One, Two, Many, }

slide-59
SLIDE 59

Trait system

impl From<u32> for Count { fn from(n: u32) -> Count { match n { 0 => Count::Zero, 1 => Count::One, 2 => Count::Two, _ => Count::Many, } } }

slide-60
SLIDE 60

Trait system

use std::io; struct MyError; impl From<io::Error> for MyError { fn from(err: io::Error) -> MyError { MyError } }

slide-61
SLIDE 61

Trait system

use std::fs::File; // ? automatically convert errors! fn read_file() -> Result<String, MyError> { let mut file = File::open("./test.txt")?; let mut content = String::new(); file.read_to_string(&mut content)?; Ok(content) }

slide-62
SLIDE 62

Trait system

#[derive(Debug)] struct Dummy; // Static dispatch fn debug<T: Debug>(val: T) { println!("{:?}", val); } debug(Dummy);

slide-63
SLIDE 63

Trait system

#[derive(Debug)] struct Dummy; // Dynamic dispatch fn debug(val: &dyn Dummy) { println!("{:?}", val); } debug(&Dummy);

slide-64
SLIDE 64

Trait system

trait Duck: Display { fn quack(&self) { println!("{} quacks!", self); } } impl Duck for str {} "Bob".quack();

slide-65
SLIDE 65

Trait system

trait Duck: Display { // trait bound: Self must impl Display fn quack(&self) { // default implementation println!("{} quacks!", self); // ok because self } // impls Display } impl Duck for str {} // use default quack method "Bob".quack(); // Bob quacks!

slide-66
SLIDE 66

Intermission

Questions so far?

slide-67
SLIDE 67

Lifetimes

  • Bane of newcomers
  • There is no analog in any other mainstream language
  • Main source of “fighting the borrow checker”
  • Once you grok it, you will be able to write code that in C

would equate to magic, with complete confidence!

slide-68
SLIDE 68

Lifetimes

  • All references/borrows (&) have a lifetime
  • By default those lifetimes are anonymous
  • Rust is typically good enough at working with anonymous

lifetimes via elision (similar to inference)

  • Examples often name lifetimes 'a, 'b, 'c…
  • If you have more than one lifetime, this is a horrible idea!
slide-69
SLIDE 69

Lifetimes

impl Duck { // Borrow self with anonymous lifetime, // return a string slice with anonymous lifetime. // Rust will figure out that those are the same. fn name(&self) -> &str { &self.name } }

slide-70
SLIDE 70

Lifetimes

impl Duck { // Borrow self with lifetime 'a, // return a string slice with lifetime 'a. fn name<'a>(&'a self) -> &'a str { &self.name } }

slide-71
SLIDE 71

Lifetimes

impl Duck { // Define 'short and 'long lifetimes // 'long lifetime has to “outlive” the 'short lifetime // Borrow self with lifetime 'long, // return a string slice with lifetime 'short. fn name<'short, 'long: 'short>(&'long self)

  • > &'short str {

&self.name } }

slide-72
SLIDE 72

Lifetimes

There is a special 'static lifetime that can save you a lot of time: // No need to define any lifetimes // in the struct definition struct Duck { name: &'static str, } let bob = Duck { name: "Bob" }; // All literals are 'static

slide-73
SLIDE 73

Compiler

  • Why doesn’t the stupid compiler let me do my thing?
  • The only way to fight the borrow checker is to realize:
  • YOU CAN’T WIN!
  • Compiler is smarter than you.
  • Yes, really.
slide-74
SLIDE 74

Compiler

  • The compiler is your friend.
  • It’s a very good friend. It’s a very honest friend.
  • It might be Dutch.
  • Eventually writing code that satisfies the compiler

becomes second nature.

  • As a result, you will become a better programmer.
slide-75
SLIDE 75

Questions?

maciej@parity.io @MaciejHirsz