I agree that Rust can look pretty weird to an untrained developer when lifetimes get involved. But, in Rust's defence, I haven't seen any other language write down lifetimes more concisely.
The underscore could've been a name if the name mattered, which would be required in many languages. Rewriting it to <'something>) may help readability (but risks introducing bugs later by reusing `something`).
Many C-derived languages are full of symbol soup. A group like <?,?>[]) can happen all over Java, for instance. Many of these languages have mixes of * and & all over the place, C++ has . and -> for some reason, making for some pretty unreadable soup. The biggest additions I think Rust added to the mix was ' for lifetimes (a concept missing from most languages, unfortunately), ! for a macro call (macro invocations in many other languages aren't marked at all, leaving the dev to figure out if println is a method or a macro), and ? to bubble up errors. The last one could've been a keyword (like try in Zig) but I'm not sure if it makes the code much more readable that way.
If you know other programming languages, the symbols themselves fall into place quite quickly. I know what <'_> does in Rust for the same reason I know what <T, R> T does in Java, while a beginner or someone who hasn't learned past Java 6 may struggle to read the code. Out of all the hurdles a beginning Rust programmer will face, the symbols are probably your least concern.
As for books, the Rust book on the Rust website is kept up to date pretty well. There are books for programmers coming from various other languages as well.
The language itself hasn't changed much these past few years. The standard library gets extended with new features, but a book a few years old will teach you Rust just fine.
In many cases, changes to the language have been things like "the compiler no longer treats this as broken (because it isn't)" and "the compiler no longer requires you to write out this long definition because it can figure that stuff out itself". I'd recommend running a tool called "clippy" in your IDE or on the command line, if you can leverage a modern language feature for better legibility, clippy will usually suggest it.
> I agree that Rust can look pretty weird to an untrained developer when lifetimes get involved. But, in Rust's defence, I haven't seen any other language write down lifetimes more concisely.
Can you do a lot better? I don't think so and it wouldn't help that much.
The truth is that most of the time we want to rely on some inferred lifetime annotations, but will obviously need an escape hatch from time to time.
Rust doesn't waste a lot of typing around the annotations, but if you were to improve Rust, you'd improve the implicit inference, not the syntax for being explicit.
> Can you do a lot better? I don't think so and it wouldn't help that much.
I think Rust could do a lot better inferring lifetimes if the compiler would be allowed to peek into called function instead of stopping at the function signature - e.g. if it had a complete picture of the control flow of the entire code base (maybe be up to a point that manual lifetime annotations could be completely eliminated?).
IMHO it's not unrealistic to treat the entire codebase as a single compilation unit, Zig does this for instance - it just doesn't do much so far with the additional information that could be gained.
It's a dangerous option: Rust already has long compile times, expanding the space it has to analyze would only increase that. Not to mention you'd be much more dependent on the implementation details of a given function, and it'd become very messy. The fact that lifetimes have a specifiable interface is probably one of the main things that makes Rust's approach work at all.
Rust has similar rules about type inference (of which lifetimes are a subset) at the function level as well. I think this was a lesson learned the hard way by Haskell, which does allow whole-program type inference, and how programmers working in it quickly learned you really want to specify the types at the function level anyway
> Not to mention you'd be much more dependent on the implementation details of a given function
Hmm, but wouldn't that already be the case since the manual lifetime annotation must match what the function actually does? E.g. I would expect compiler errors if the 'internal' lifetime details of a function no longer match its manual lifetime annotations (is it actually possible to create incorrect lifetime annoatations in Rust without the compiler noticing?)
Higher compile times would be bad of course, but I wonder how much it would add in practice. It's a similar problem as LTO, just earlier in the compile process. E.g. maybe some time consuming tasks can be moved around instead of added on top.
(not OP)
I love rust, bu I just think that using ' for lifetime was a huge mistake, and using <> for templates (rather than something like []) was a medium mistake.
There is something about how the brain is wired, that using ' for lifetime, just triggers the wrong immediate response to it.
Something like this would look so much nicer IMHO [$_], compared to this <'_>.
I completely disagree that [$_] looks nicer than <'_>.
I cannot imagine using syntax that’s largely reserved across languages for array indexing for such a completely unrelated topic.
This comes from ML (as in SML or OCaml), where 'a reads "alpha" and is a type parameter.
> using <> for templates (rather than something like []) was a medium mistake
I never get this take. Array indexing is done with []. This would just confuse the hell out of me.
re: try keyword in Rust - this is actually a thing on nightly, although instead of bubbling up errors directly, it creates a scope (within which ? is usable) that evaluates to a Result.
> Many C-derived languages are full of symbol soup.
C is not full of symbol soup though.
It is more full of symbol soup than Pascal or Modula 2, and back in the day when C was taking over other such languages, there were lots of complaints about C's syntax being like "line noise" and whatnot.
Rust takes it to a whole new level though.
Yeah, I wonder what people are referring to when they say C is full of symbol soup. I mentioned it in my other comment that C is not, just like how Common Lisp is not due to the parentheses, its syntax is pretty simple.
Rust destructors are interesting.
- You can't export a reference to the thing you are dropping. You can do that in C++. This prevents "re-animation", where something destroyed comes back to life or is accessed beyond death. Microsoft Managed C++ (early 2000s), supported re-animation and gave it workable semantics. Bad idea, now dead.
- This is part of why Rust destructors cannot run more than once. Less than once is possible, as mentioned above.
- There's an obscure situation with Arc and destructors. When an Arc counts down to 0, the destructor is run. Exactly once. However, Arc countdown and destructor running are not an atomic operation. It is possible for two threads to see an Arc in a strong_count == 1 state just before the Arc counts down. Never check strong_count to see if you are "the last owner". That creates a race condition.[1] I've seen that twice now. I found race conditions that took a day of running to hit. Use strong_count only for debug print.
- A pattern that comes up in GUI libraries and game programming involves objects that are both in some kind of index and owned by Arcs. On drop, the object should be removed from the index. This is a touchy operation. The index should use weak refs, and you have to be prepared to get an un-upgradable Weak from the index.
- Even worse is the case where dropping an object starts a deletion of something else.
If the second deletion can't be completed from within the destructor, perhaps because it requires a network transaction, it's very easy to introduce race conditions.
[1] https://github.com/rust-lang/rust/issues/117485 https://github.com/rust-lang/rust/issues/117485
> - You can't export a reference to the thing you are dropping. You can do that in C++. This prevents "re-animation", where something destroyed comes back to life or is accessed beyond death. Microsoft Managed C++ (early 2000s), supported re-animation and gave it workable semantics. Bad idea, now dead.
>
> - This is part of why Rust destructors cannot run more than once. ...
This is a very backwards way to describe this, I think. Managed C++ only supported re-animation for garbage collected objects, where it is still today a fairly normal thing for a language to support. This is why these "destructors" typically go by a different name, "finalizers." Some languages allow finalizers to run more than once, even concurrently, but this is again due to their GC design and not a natural thing to expect of a "destructor."
The design of Drop and unmanaged C++ destructors is that they are (by default) deterministically executed before the object is deallocated. Often this deallocation is not by `delete` or `free`, which could perhaps in principle be cancelled, but by a function return popping a stack frame, or some larger object being freed, which it simply does not make sense to cancel.
> Never check strong_count to see if you are "the last owner".
This made me think of the `im` library[0] which provides some immutable/copy on write collections. The docs make it seem like they do some optimizations when they determine there is only one owner:
> Most crucially, if you never clone the data structure, the data inside it is also never cloned, and in this case it acts just like a mutable data structure, with minimal performance differences (but still non-zero, as we still have to check for shared nodes).
I hope this isn't prone to a similar race condition!
[0] https://docs.rs/im/15.1.0/im/index.html https://docs.rs/im/15.1.0/im/index.html
The way to do this while avoiding race conditions seems to be `Arc::into_inner` or `Arc::get_mut`; for instance, the docs for `Arc::try_unwrap` mention a possible race condition, and recommend using `Arc::into_inner` to avoid it: https://doc.rust-lang.org/std/sync/struct.Arc.html#method.try_unwrap https://doc.rust-lang.org/std/sync/struct.Arc.html#method.tr...
Managed C++ is pretty much around, kind of, as it got replaced by C++/CLI in .NET 2.0, is still used by many of us instead of dealing with P/Invoke annotations, is required by WPF infrastructure, and currently is on C++20 support level.
> is it actually possible to create incorrect lifetime annoatations in Rust without the compiler noticing?
In safe rust, no.
Full inference is one of those things that seems like a no brainer, but there are a number of other more subtle tradeoffs that make it a not great idea. Speed was already mentioned, but it’s really downstream from tractibility, IMHO. That is, lifetime checking is effectively instantaneous today, and that’s because you only need to confirm that the body matches the signature, which is a very small and local problem. Once you allow inference, you end up needing to check not just the body, but also the bodies of every function called in your body, recursively, since you no longer know their signatures up front. We tend to think of compiler passes as “speed” in the sense of it’s nice to have fast compile times, but it also matters in the sense of what can practically be checked in a reasonable time. The cheaper a check, the more checks we can do. Furthermore, remember that Rust supports separate compilation, which is a major hindrance to full program analysis, which is what you need to truly infer lifetimes.
Beyond complexity arguments, there’s also more practical ones: error messages would get way worse. More valid programs would be rejected if the inference can’t figure out an answer. Semver is harder to maintain, because a change in the body now changes the signature, and you may break your callers in ways you don’t realize at first.
Sure, safety is a relative moving target. There’s no way to prevent race conditions unless you have proofs. And then there’s no way to enforce that your proof is written correctly. It’s turtles all the way down. Rust is a Paretto frontier of safety for AOT high performance languages. Even for race conditions I suspect the tools Rust has for managing concurrency-related issues make it less prone to such issues than other languages.
The problem is you’re creating a hypothetical gold standard that doesn’t exist (indeed I believe it can’t exist) and then judging Rust on that faux standard and complaining that Rust chooses a different standard. That’s the thing though - every language can define whatever metrics they want and languages like C/C++ struggle to define any metrics that they win vs Rust.
> For example you cannot get memory leaks in a language without mutation, and therefore without cyclic data structures
This does not follow. Without any mutation of any kind, you can’t even allocate memory in the first place (how do you think a memory allocator works?). And you can totally get memory leaks without mutation however you narrowly define it because nothing prevents you from having a long-lived reference that you don’t release as soon as possible. That’s why memory leaks are still a thing in Java because there’s technically a live reference to the memory. No cycles or mutations needed.
> So truly safe Rust (Rust without any unsafe at all) is leakfree, I think?
Again, Box::leak is 100% safe and requires no unsafe at all. Same with std::mem::forget. But even if you exclude APIs like that that intentionally just forget about the value, again nothing stops you from retaining a reference forever in some global to keep it alive.
What is a type system except a bunch of proofs? You can encode some program correctness properties into types. Elevating the ones you happen to be able to encode and calling them "safety" and the rest "correctness" is just marketing.
I am not creating a gold standard because as far as I am concerned, it is all just correctness. There aren't morally more and less important correctness properties for general programs: different properties matter more or less for different programs.
>Without any mutation of any kind, you can’t even allocate memory in the first place (how do you think a memory allocator works?).
data L t = E | C t (L t)
data N = Z | S N
nums Z = E
nums (S n) = C (S n) (nums n)
You cannot express a reference cycle in a pure functional language but they still have allocation.
However I don't know why I brought this up, because you can also eliminate all memory leaks by just using garbage collection - you don't need to have immutable and acyclic data structures.
>Again, Box::leak is 100% safe and requires no unsafe at all. Same with std::mem::forget.
#[inline]
pub fn leak(b: Box<T>) -> &'static mut T {
unsafe { &mut *Box::into_raw(b) }
}
They are implemented using unsafe. There is no way to implement Box without unsafe.
If you retain a reference in a global then it is NOT a memory leak! The variable is still accessible from the program. You can't just forget about the value: its name is right there, accessible. That is not a memory leak, except by complete abuse of terminology. The concept of "inaccessible and uncollectable memory, which cannot be used or reclaimed" is a useful one. Your definition of a memory leak seems to be... any memory usage at all?
the unsafety is because of the lifetime laundering not because the operation is unsafe. The compiler doesn’t know that the lifetime of the underlying memory because static and decoupled from the lifetime of the consumed Box
And while we’re at it, please explain to me how this hypothetical language that allocates on the heap without mutable state exists without under the hood calling out to the real mutable allocator somewhere.
> If you retain a reference in a global then it is NOT a memory leak!
> Your definition of a memory leak seems to be... any memory usage at all?
It’s just that you’re choosing to define it as not a memory leak. Another definition of memory leak might be “memory that is retained longer than it needs to be to accomplish the intended goal”. That’s because users are indifferent to whether the user code is retaining the reference and forgetting about it or the user code lost the reference and the language did too.
So from that perspective tracing GC systems even regularly leak memory and then go on a hunt trying to reclaim it when they’ve leaked too much.
More importantly as has been pointed out numerous times to you, memory safety is a technical term of art in the field (unlike memory leaks) that specifically is defined as the issues safe Rust prevents and memory leaks very clearly do not fall under that very specific definition.
>the unsafety is because of the lifetime laundering not because the operation is unsafe.
You have missed the point. I said you can't leak memory in safe Rust. That is true. Box::leak isn't safe Rust: it uses the unsafe keyword. This is half the problem with the stupid keyword: it confuses people. I am saying that it requires the trustme keyword and you are saying it isn't inherently incorrect. Rust uses "unsafe" to mean both. But in context it is quite clear what I meant when talking about Box::leak, which you falsely claimed could be written in safe Rust.
>And while we’re at it, please explain to me how this hypothetical language that allocates on the heap without mutable state exists without under the hood calling out to the real mutable allocator somewhere.
What does the implementation have to do with anything? We are talking about languages not implementations. This isn't a difficult concept.
>It’s just that you’re choosing to define it as not a memory leak. Another definition of memory leak might be “memory that is retained longer than it needs to be to accomplish the intended goal”.
That isn't the definition. I am using the only definition of the term that any serious person has ever used.
>That’s because users are indifferent to whether the user code is retaining the reference and forgetting about it or the user code lost the reference and the language did too.
Users are completely irrelevant. It is logically impossible to ever prevent "leaks" that are just the storage of information. That isn't a leak, it is the intentional storage of information by the programmer. So it is a completely useless concept if that is what you want to use. It might be a useful concept in application user experience design or something but we are talking about programming languages.
On the other hand, "memory leaks" is a very useful concept if you use the actual definition because it is almost difficult to even conceive of a memory management strategy that isn't concerned with preventing memory leaks (proper). The "short lived program; free nothing" strategy is the only one I can think of, a degenerate case.
>More importantly as has been pointed out numerous times to you, memory safety is a technical term of art in the field (unlike memory leaks) that specifically is defined as the issues safe Rust prevents
No, it isn't! That is the definition that Rust people choose to use, which nobody used before 2015ish and is only widely used because Rust captured mindshare. It isn't some definition that predated Rust and which Rust magically fell right into.
Go back and look at mailing list threads, forum posts, papers, anything before Rust tried to steal the term "safety". It referred (and properly still refers) to programs. When people complained about manual memory management, the big complaint was that big C++ GUI programs (in particular) leaked memory like sieves. Nobody was particularly concerned about data races except the people implementing concurrency primitives in standard libraries etc. C++ didn't even have a defined memory model or standard atomics. Everyone was relying on x86's strong memory model in code all over the place. The big concern was avoiding manual memory management, memory leaks, and data corruption.
"Safe" didn't mean "has no data races but might have race conditions, has no use after free but might have memory leaks, and might have overflow bugs and SQL injections and improper HTML sanitisation". That would be a truly stupid definition. It meant "correct". The fanatical Rust community came along and tried to redefine "safe" to mean "the things we prevent". Rust's definition makes sense for Rust but it is Rust-specific because it is downstream of what Rust is capable of enforcing. Nobody would a priori come up with the particular subset of correctness properties that Rust happens to enforce and call them "safety". It is transparently a posteriori.