Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

    View full-size slide

  2. #[implementation]
    fn main() {
    //no need to program anymore
    }
    2

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  5. Whoami
    This is my first talk on a Rust
    conference!
    5

    View full-size slide

  6. I want to make you competent at
    reading and writing where
    clauses!
    6

    View full-size slide

  7. 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

    View full-size slide

  8. 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

    View full-size slide

  9. Let’s be generic
    – —
    fn print(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

    View full-size slide

  10. 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::(&P)
    — This calls print::(&S)
    10

    View full-size slide

  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); —
    }
    Both functions are actually different and are created on use.
    A function logically existing and actually being compiled is two
    things.
    11

    View full-size slide

  12. Where is not only on functions
    struct Point where P: Debug {
    x: P,
    y: P
    }
    12

    View full-size slide

  13. Detailed analysis
    fn some_function
    –
    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

    View full-size slide

  14. Detailed analysis
    Because the left hand is an actual type, this works:
    fn takes_into_string(t: T)
    where
    String: From
    {
    let string = String::from(t);
    println!("{}", string);
    }
    14

    View full-size slide

  15. Associated types
    The where clause can be used to constrain the associated type
    of a trait:
    fn debug_iter(iter: I)
    where
    I: Iterator,
    I::Item: Debug
    {
    for item in iter {
    println!("{:?}", iter);
    }
    }
    15

    View full-size slide

  16. Multiple impl Blocks
    You can gradually unlock API based on bounds.
    enum Result {
    Ok(T),
    Err(E),
    }
    impl Result {
    fn is_ok(&self) -> bool {
    // ...
    }
    fn is_err(&self) -> bool {
    // ...
    }
    } 16

    View full-size slide

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

    View full-size slide

  18. Multiple impl Blocks
    impl Result
    where
    T: Default
    {
    fn unwrap_or_default(self) -> T {
    //...
    }
    }
    18

    View full-size slide

  19. API design
    Gradually unlocking features
    based on bounds as a common
    API strategy in Rust.
    19

    View full-size slide

  20. 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

    View full-size slide

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

    View full-size slide

  22. Opting out of Borrowing
    pub fn spawn(f: F) -> JoinHandle
    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

    View full-size slide

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

    View full-size slide

  24. Late bounds
    How do we make sure I keep the
    bound, but don’t have to repeat
    it all times?
    24

    View full-size slide

  25. Late bounds
    struct Wrapper {
    inner: I
    }
    impl Wrapper {
    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(w: Wrapper) -> I
    { w.inner }
    25

    View full-size slide

  26. Late bounds
    impl Wrapper {
    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

    View full-size slide

  27. Late bounds
    impl Wrapper
    where
    Inner: Debug
    {
    pub fn new(inner: Inner) -> Self
    {
    Wrapper { inner: inner }
    }
    pub fn inspect(&self) {
    {
    println("{:?}", self.inner);
    }
    }
    27

    View full-size slide

  28. Late bounds
    fn take_inner(w: Wrapper) -> I
    {
    w.inner
    }
    28

    View full-size slide

  29. Refactoring
    Where clauses are a refactoring
    targets!
    29

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  32. Terrible clauses
    impl Execute
    for Find>
    where R: Relation,
    Repos: Repository,
    Repos::Gateway: ExecuteOne,
    Self: Query
    {
    type FutureType = <::Gateway as ExecuteOne<
    Self::Parameters>>::Future;
    fn execute(&self, repos: &Repos) -> Self::FutureType
    where Repos: Stores {
    ExecuteOne::execute_query(repos.gateway(), &self)
    }
    }
    These things don’t out of the blue and are frequently changed.
    32

    View full-size slide

  33. Advanced Examples

    View full-size slide

  34. Relationships
    Traits and bounds can be used
    to express relationships
    33

    View full-size slide

  35. State Machines
    State machine by @happyautomata
    34

    View full-size slide

  36. State Machines
    trait State { }
    trait TerminalState { }
    35

    View full-size slide

  37. State Machines
    trait TransitionTo
    where S: State,
    Self: State
    {
    fn transition(self) -> S;
    }
    trait Terminate
    where
    Self: TerminalState
    {
    fn terminate(self);
    }
    36

    View full-size slide

  38. 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

    View full-size slide

  39. State Machines
    impl TransitionTo for Start {
    fn transition(self) -> Loop {
    }
    impl TransitionTo for Loop {
    fn transition(self) -> Loop {
    }
    impl TransitionTo for End {
    fn transition(self) -> End {
    }
    impl Terminate for End {
    fn terminate(self) {
    }
    38

    View full-size slide

  40. 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

    View full-size slide

  41. Databases
    pub trait Storage {
    fn query(&self, id: i64) -> Model
    }
    40

    View full-size slide

  42. Databases
    pub trait Stores : Storage { }
    pub trait Storage {
    fn query(&self, id: i64) -> Model
    where
    Self: Stores –
    {
    //....
    }
    }
    – Forcing the implementor to further constrait what the
    database stores
    41

    View full-size slide

  43. Databases
    struct UsersDatabase { /* */ }
    impl Storage for UsersDatabase { /* */ }
    struct User {
    id: i64,
    name: String
    }
    struct Avatar { /* */ }
    impl Stores for UsersDatabase {}
    impl Stores for UsersDatabase {}
    42

    View full-size slide

  44. Databases
    fn main() {
    let db = UserDatabase::connect();
    db.query::(1);
    // db.query::(2);
    }
    43

    View full-size slide

  45. 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

    View full-size slide

  46. One last thing
    45

    View full-size slide

  47. Thank you!
    • https://twitter.com/argorak
    • https://github.com/skade
    • https://speakerdeck.com/skade
    • fl[email protected]
    • https://ferrous-systems.com
    • https://rust-experts.com
    46

    View full-size slide