1. 29
  1.  

  2. 16

    I don’t think I really understood rust’s ownership semantics until I figured out what all the variants of self meant:

    1. self
    2. mut self
    3. &self
    4. &mut self

    Specifically the first two, which ensure if you call that class method it will be the last method you can call on that class instance! That was when it really clicked for me, and I started thinking of methods or functions consuming their parameters. This can be very useful if you’re trying to make invalid state unrepresentable, or even invalid state transitions unrepresentable! Like if you have a method like .close() or something you can make it fn close(self) so that if anyone tries to do something like:

    object.close();
    object.do_something_else();
    

    the borrow checker will block this at compile time! This transformed ownership semantics in my mind from an annoying bookkeeping thing I did to appease the compiler into a powerful tool I could use to keep myself from writing bugs.

    1. 4

      To expand on this I learned this technique (and more advanced variants) from this post on “the typestate pattern”: http://cliffle.com/blog/rust-typestate/

      It wasn’t too long ago I speculated here on this very website that programming design patterns were just a cope for the language not having sum types. Shortly thereafter I ran into an obvious shortfall with rust’s sum types, which is that they can’t restrict what functions can be called depending on the enum value! There is an RFC for this, but until then you have to use the design pattern in the blog post.

      I’ve been making use of it while writing some scanner code, so scanner.advance() consumes self then returns a Result<Scanner<Advanceable>, Scanner<EOF>> instance. You can’t call scanner.advance() on a Scanner<EOF> instance. I will say it makes your code very verbose and kind of unreadable with tons of pattern matches, but it’s already uncovered some bugs that would have languished for a while.

      1. 3

        I speculated [..] that programming design patterns were just a cope for the language not having sum types.

        This insight can be made broader: design patterns are a coping mechanism for missing language features in general.

      2. 2

        Note that 1/2 and 3/4 are not really analogous in the way that your list might suggest. (Maybe you already know this! Just thought I’d point it out.) The first two are identical from the caller’s perspective, and differ only by the callee giving itself permission to mutate self. The last two are really different types: (3) is a shared reference, and (4) is a unique reference.

        You can see this if you try writing mut self in a trait:

        trait Foo {
            fn foo(mut self);
        }
        

        Result:

        error: patterns aren't allowed in functions without bodies
         --> src/lib.rs:2:12
          |
        2 |     fn foo(mut self);
          |            ^^^^^^^^ help: remove `mut` from the parameter: `self`
          |
          = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
          = note: for more information, see issue #35203 <https://github.com/rust-lang/rust/issues/35203>
          = note: `#[deny(patterns_in_fns_without_body)]` on by default
        
        1. 1

          Oh yeah that’s a good point. One thing that also confuses me about mut self is that it seemingly isn’t transitive in the same way that const or rather its absence is in C++.

          1. 2

            Yeah, it’s &mut self which has that property and corresponds to the absence of const, not mut self.

            Here is how I’d compare them:

                     Rust            |          C++
            -------------------------|-------------------------
            impl Foo {               | struct Foo {
                fn a(self) {}        |     void a() const&& {}
                fn b(mut self) {}    |     void b() && {}
                fn c(&self) {}       |     void c() const {}
                fn d(&mut self) {}   |     void d() {}
            }                        | };
            

            The syntax used in a and b has been available since C++11. It gives similar behavior to your object.close() Rust example: it forces you to instead write std::move(object).close(). Of course, the C++ compiler isn’t as helpful in making sure you don’t use object again after.

      3. 1

        Senior rust engineer is a thing now.