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.

1c88d7906e3ffa450aedff2f5f1d1299?s=128

Florian Gilcher

March 30, 2019
Tweet

Transcript

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

    2019 CEO and Rust Trainer Ferrous Systems GmbH 1
  2. #[implementation] fn main() { //no need to program anymore }

    2
  3. 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
  4. 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
  5. Whoami This is my first talk on a Rust conference!

    5
  6. I want to make you competent at reading and writing

    where clauses! 6
  7. Background

  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. Where is not only on functions struct Point<P> where P:

    Debug { x: P, y: P } 12
  14. 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
  15. 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
  16. 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
  17. Patterns

  18. 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
  19. 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
  20. Multiple impl Blocks impl<T, E> Result<T, E> where T: Default

    { fn unwrap_or_default(self) -> T { //... } } 18
  21. API design Gradually unlocking features based on bounds as a

    common API strategy in Rust. 19
  22. 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
  23. 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
  24. 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
  25. 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
  26. Late bounds How do we make sure I keep the

    bound, but don’t have to repeat it all times? 24
  27. 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
  28. 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
  29. 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
  30. Late bounds fn take_inner<I>(w: Wrapper<I>) -> I { w.inner }

    28
  31. Refactoring Where clauses are a refactoring targets! 29

  32. Refactoring Don’t start out very generic, refactor towards it! 30

  33. Genericness Finding the right level of genericness is important! 31

  34. 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
  35. Advanced Examples

  36. Relationships Traits and bounds can be used to express relationships

    33
  37. State Machines State machine by @happyautomata 34

  38. State Machines trait State { } trait TerminalState { }

    35
  39. State Machines trait TransitionTo<S> where S: State, Self: State {

    fn transition(self) -> S; } trait Terminate where Self: TerminalState { fn terminate(self); } 36
  40. 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
  41. 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
  42. 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
  43. Databases pub trait Storage { fn query<Model>(&self, id: i64) ->

    Model } 40
  44. 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
  45. 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
  46. Databases fn main() { let db = UserDatabase::connect(); db.query::<User>(1); //

    db.query::<String>(2); } 43
  47. 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
  48. One last thing 45

  49. Thank you! • https://twitter.com/argorak • https://github.com/skade • https://speakerdeck.com/skade • florian.gilcher@ferrous-systems.com

    • https://ferrous-systems.com • https://rust-experts.com 46