Defining Types In Rust
In this post:
- I muck around a bit in some of the confusing corners of Rust's type system.
I recently read a blog post about types and sharing in Rust, which made the point that:
every type gets to pick for itself what it means to share this type
The rest of that section goes over how this works out a high level (some types need very particular behavior, some can just delegate to their contained types). This made me realize that I hadn't really been aware of all of this before. Whatever the types I defined were doing to "pick [...] what it mean[t] to share" them, I had been unaware I was making those decisions. This is bad, and I should do something about it.
Okay, I took the radical step of reading the dang documentation, and I'm starting to get a better handle on things. One thing that confused me for a while was lifetime constraints, but I think I've figured out how to make my intuition go. Here's a snippet of code that compiled using the Rust Playground:
fn longest<'a: 'c, 'b: 'c, 'c>(x: &'a str, y: &'b str) -> &'c str {
if x.len() > y.len() {
x
} else {
y
}
}
Most explanations I read about constraining lifetimes talked about variance, which is technically correct, but didn't land with me in this context. We can see from this sample that, if you constrain one lifetime by another, it must be possible to "cast" the constrained lifetime to the constraining lifetime. In other words, the constraining lifetime must be a subset of the constrained lifetime.
Now, because types can contain lifetimes, constraining a type with a lifetime has the same semantics: the type is valid over the entire scope of the lifetime. In other words, it is valid to construct a reference to the type, that has the lifetime. These statements are (I think) completely equivalent.
In practice, the above gymnastics aren't necessary to defining the lifetimes for that function, because (I think) the variance is already factored in if you constrain everything to the same lifetime. Putting in the explicit-looking things there just adds some irrelevant degrees of freedom.
Oh, wait, borrowing and sharing are distinct but related concepts. I don't know about anyone else, but I'm pretty sure I just learned a valuable lesson about something or other. Point is, if I'm understanding this right, much of this is about sharing data between threads; much of what I've hit messing with generics in Rust has (mercifully) not hit Send/Sync, and instead I've had to worry about whether it makes sense to relax the Sized bound. I might have a better handle on that from writing this post? We'll have to see.