I don’t think I really understood rust’s ownership semantics until I figured out what all the variants of self meant:
self
mut self
&self
&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.
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.
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
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++.
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.
I don’t think I really understood rust’s ownership semantics until I figured out what all the variants of
self
meant:self
mut self
&self
&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 itfn close(self)
so that if anyone tries to do something like: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.
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()
consumesself
then returns aResult<Scanner<Advanceable>, Scanner<EOF>>
instance. You can’t callscanner.advance()
on aScanner<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.This insight can be made broader: design patterns are a coping mechanism for missing language features in general.
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:Result:
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 thatconst
or rather its absence is in C++.Yeah, it’s
&mut self
which has that property and corresponds to the absence ofconst
, notmut self
.Here is how I’d compare them:
The syntax used in
a
andb
has been available since C++11. It gives similar behavior to yourobject.close()
Rust example: it forces you to instead writestd::move(object).close()
. Of course, the C++ compiler isn’t as helpful in making sure you don’t useobject
again after.Senior rust engineer is a thing now.