Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

The power of the where clause

The power of the where clause

A beginner-level description of how the where clause in Rust works and what it can be used for.

Florian Gilcher

March 30, 2019
Tweet

More Decks by Florian Gilcher

Other Decks in Programming

Transcript

  1. The power of the where clause Florian Gilcher Rust LATAM

    2019 CEO and Rust Trainer Ferrous Systems GmbH 1
  2. Whoami • Florian Gilcher • https://twitter.com/argorak • https://github.com/skade • CEO

    https://asquera.de, https://ferrous-systems.com • Rust Programmer and Trainer: https://rust-experts.com • Mozillian • Previously 10 years of Ruby community work 3
  3. Whoami • Started learning Rust in 2013 • Mostly out

    of personal curiosity • Co-Founded the Berlin usergroup • Organized RustFest and OxidizeConf • Project member since 2015, mostly Community team 4
  4. A simple example #[derive(Debug)] – struct Point { x: i32,

    y: i32 } fn print_point(p: &Point ˜) { println!("{:?}" —, p) } – Point implements Debug — The println macro requires the printed value to implement Debug ˜ We pass Point as a concrete type 7
  5. What about squares? #[derive(Debug)] struct Square { x: i32, y:

    i32, width: i32 } – fn print_square(s: &Square) { println!("{:?}", s) } – That’s very repetitive. Computers are good at repetitive! 8
  6. Let’s be generic – — fn print<S>(s: &S) where ˜

    S: Debug { println!("{:?}", s) } – The function is generic over a type variable S — It takes a reference to a value of the type S ˜ S can be any type, as long as it implements Debug. This is called a trait bound. 9
  7. Let’s be generic fn main() { let p = Point

    { x: 0, y: 0 }; let s = Square { x: 0, y: 0, width: 0 }; print(&p); – print(&s); — } – This calls print::<Point>(&P) — This calls print::<Square>(&S) 10
  8. Let’s be generic fn main() { let p = Point

    { x: 0, y: 0 }; let s = Square { x: 0, y: 0, width: 0 }; print::<Point>(&p); – print::<Square>(&s); — } Both functions are actually different and are created on use. A function logically existing and actually being compiled is two things. 11
  9. Detailed analysis fn some_function<T,E> – where T: Trait, T: OtherTrait,

    — E: Trait + OtherTrait { } – In the where clase, T is an actual type — Types can be constrained multiple times Read: ”for every type pair T and E, where T is Send + Sync and E is Send” 13
  10. Detailed analysis Because the left hand is an actual type,

    this works: fn takes_into_string<T>(t: T) where String: From<T> { let string = String::from(t); println!("{}", string); } 14
  11. Associated types The where clause can be used to constrain

    the associated type of a trait: fn debug_iter<I>(iter: I) where I: Iterator, I::Item: Debug { for item in iter { println!("{:?}", iter); } } 15
  12. Multiple impl Blocks You can gradually unlock API based on

    bounds. enum Result<T, E> { Ok(T), Err(E), } impl<T, E> Result<T, E> { fn is_ok(&self) -> bool { // ... } fn is_err(&self) -> bool { // ... } } 16
  13. Multiple impl Blocks impl<T, E> Result<T, E> where E: Debug

    { fn unwrap(self) -> T { match self { Ok(t) -> t, Err(e) -> panic!("Error: {:?}", e), } } } 17
  14. Multiple impl Blocks impl<T, E> Result<T, E> where T: Default

    { fn unwrap_or_default(self) -> T { //... } } 18
  15. The thread API fn main() { let vec = vec![1,2,3,4];

    let handler = std::thread::spawn(move { vec.iter().count() }); let number = handler.join().unwrap(); println!("{}", number); } 20
  16. The thread API pub fn spawn<F, T>(f: F) -> JoinHandle<T>

    where F: FnOnce() -> T, – { /*...*/ } — pub fn spawn(f: &'a i32) -> JoinHandle<&'a i32> pub fn spawn(f: std::slice::Iter<'a, T>) -> JoinHandle<usize> – This is the closure — I’m cheating a little, this is the closure capture type The compiler checks all potential situations! 21
  17. Opting out of Borrowing pub fn spawn<F, T>(f: F) ->

    JoinHandle<T> where F: FnOnce() -> T, – F: Send + 'static, — T: Send + 'static { //... } – This is the closure — Send is one of Rust concurrency guards — ’static asks for the type to own all data This is not cheating! 22
  18. Late bounds struct Wrapper<I> where I: Debug { inner: I

    } fn take_inner<I>(w: Wrapper<I>) -> I where I: Debug – { w.inner } – This is necessary because the type mandates item, it isn’t useful to the function 23
  19. Late bounds How do we make sure I keep the

    bound, but don’t have to repeat it all times? 24
  20. Late bounds struct Wrapper<I> { inner: I } impl<I> Wrapper<I>

    { pub fn new(d: I) -> Self where I: Debug { Wrapper { inner: d } } pub fn inspect(&self) where I: Debug { println!("{:?}", self.inner); } } fn take_inner<I>(w: Wrapper<I>) -> I { w.inner } 25
  21. Late bounds impl<I> Wrapper<I> { pub fn new(d: I) ->

    Self – where I: Debug { Wrapper { inner: d } } pub fn inspect(&self) — where I: Debug { println!("{:?}", self.inner); } } – new ensures that Wrapper only ever can be constructed with Debug types 26
  22. Late bounds impl<Inner> Wrapper<Inner> where Inner: Debug { pub fn

    new(inner: Inner) -> Self { Wrapper { inner: inner } } pub fn inspect(&self) { { println("{:?}", self.inner); } } 27
  23. Terrible clauses impl<Repos, R, ReturnType, PK> Execute<Repos, R, ReturnType> for

    Find<PK, SelectFrom<R>> where R: Relation, Repos: Repository, Repos::Gateway: ExecuteOne<ReturnType, Self::Parameters>, Self: Query<QueryMarker=One, ReturnType=ReturnType> { type FutureType = <<Repos as Repository>::Gateway as ExecuteOne< Self::Parameters>>::Future; fn execute(&self, repos: &Repos) -> Self::FutureType where Repos: Stores<R> { ExecuteOne::execute_query(repos.gateway(), &self) } } These things don’t out of the blue and are frequently changed. 32
  24. State Machines trait TransitionTo<S> where S: State, Self: State {

    fn transition(self) -> S; } trait Terminate where Self: TerminalState { fn terminate(self); } 36
  25. State Machines struct Start; impl State for Start {} struct

    Loop; impl State for Loop {} struct Stop; impl State for Stop {} impl TerminalState for Stop {} 37
  26. State Machines impl TransitionTo<Loop> for Start { fn transition(self) ->

    Loop { } impl TransitionTo<Loop> for Loop { fn transition(self) -> Loop { } impl TransitionTo<End> for End { fn transition(self) -> End { } impl Terminate for End { fn terminate(self) { } 38
  27. State Machines fn main() { let initial = Start; let

    next: Loop = initial.transition(); let next: Loop = next.transition(); //next.terminate() //let next: Start = next.transition(); let next: End = next.transition(); next.terminate() } The setup is a little involved, but usage is straight-forward and safe! https://hoverbear.org/2016/10/12/rust-state-machine-pattern/ 39
  28. Databases pub trait Stores<Model> : Storage { } pub trait

    Storage { fn query<Model>(&self, id: i64) -> Model where Self: Stores<Model> – { //.... } } – Forcing the implementor to further constrait what the database stores 41
  29. Databases struct UsersDatabase { /* */ } impl Storage for

    UsersDatabase { /* */ } struct User { id: i64, name: String } struct Avatar { /* */ } impl Stores<User> for UsersDatabase {} impl Stores<Avatar> for UsersDatabase {} 42
  30. Conclusion • Getting comfortable with the power of the where

    clause is important • Exactly picking which constraints you need where is key • There’s creative patterns of interplay 44