am overwhelmed by the sheer beauty and mature design of this language - it almost reads like Python or as well as any compiled language can.
All those lifetime annotations are not sheer beauty or pretty, sure they are necessary but not nice to look at if you are comparing it to a higher level language.
I think that being able to write down the lifetimes in the language is beautiful.
You don't have to do it, but if you want to do it, being able to do so in a way that's verified and enforced by the toolchain beats doing so in a documentation comment.
Now every time I read a doc string saying that I need to "deepcopy" something in Python for some API usage pattern to work properly I cringe.
That's one of the things that get me about Rust discourse: it seems that "Rust is pain in exchange for performance" is a common misconception. Rust is discipline in exchange for performance and correctness. A GC lets you be relatively worry-free as far as memory leaks go (and even then..) but it doesn't prevent a lot of the correctness problems the borrow checker would.
With a checker you're forced to think: do I really want to pass a copy/clone of this? Or do I want to let that function borrow it? Or borrow it mutably?
While I get the point about data-races, a “GC” doesn’t make/help you leak memory - you have simply postponed the free to a later point in time.
Assume you had some code that takes a file name and calls open on it. One day you decide you want to print that filename before you open it. Naive code will cause the name to “move” to print and unusable to the open in next line. Even though it is perfectly understood by all parties that there is no threading involved and print would finish before the next use of that string. Yes, I can create a borrow or clone, but having to think of it every single line of code even when there is only one thread of execution is really painful
Edit: I get print is a macro, but imagine a detailed logger for this case.
I'd argue the exact opposite. Languages that don't have a concept of ownership, and a borrow checker, and don't explicitly say if they want ownership, a reference, or a mutable reference, force you to keep all of these details in your head.
Here, if I have a `&T` and I try to call a function that has a `&mut T`, the compiler will tell me that's not gonna work - and then I can pick whether I want my function to take a `&mut T`, or if I want to make a clone and modify that, etc.
There's a learning curve, it's a set of habits to adopt, but once you embrace it it's really hard to go back to languages that don't have it! (See the rest of the comments for testimonials)
I think both points are right. There's times when it's useful and desirable to be specific about lifetimes, and there's also times where it's annoying noise.
Bignum arithmetic is an example of the latter. You want to just work with numbers, and in Python you can, but in Rust you must clutter your code with lifetimes and borrows and clones.
Swift's plan to allow gradual, opt-in lifetime annotations seems really interesting, if it works.
Bignum arithmetic actually is pretty simple in Rust if you use the right library. Rug[0] makes using bignums look almost just like using native numbers through operator overloading.
The operator overloading is nice but you still get smacked in the face right away by the borrow checker. Simple stuff like this won't compile ("use of moved value"):
This would work if `Integer` would implement the `Copy` trait. But I guess that type is heavy enough for it to be too expensive, therefore forcing you to explicitly call `clone()`.
I don't see how a GC would do anything with memory leaks. When you have a data structure containing some elements, and you keep a reference to the structure, because you need some data from it, but you let it grow, then you have a leak, GC or no GC. If anything, GC encourages memory leaks by making freeing memory implicit, something a programmer normally doesn't think about. And yet a resize/push operation on a vector (sic!) is just another malloc. One that will get ignored by a GC.
GC protects you against double-free and use-after-free, but memory leaks? Nope.
This is a memory leak in a non-GC language, but not in a GC language.
In a practical sense... is it your personal experience that memory leaks are equally prevalent in GC and non-GC languages? I've spent decades working in each (primarily C++ and Java, but also Pascal, C, C#, Smalltalk...) and my experience is that memory leaks were a _much_ bigger issue, in practice, in the non-GC languages.
This is a memory leak in a GC language as well, depending on what happens to foo in ...code that uses foo... Maybe it will become a field of some class, maybe it will get pushed to some queue, maybe it will get captured in a lambda, etc. You won't even be able to tell just from looking at this code in this one place alone, if you passed this pointer as argument to a function.
It is my experience that when people work with GC languages, they treat the GC as a blackbox (which it is) and simply won't bother investigating: do they have memory leaks? Of course not, they are using a GC after all, all memory-related problems solved, right? Right... With code that relies on free(), I can use a debugger and check which free() calls are hit for which pointers. Even better, I may use an arena allocator where appropriate and don't bother with free() at all. With a GC I'm just looking at some vague heap graph. Am I leaking memory? Who knows... "Do those numbers look right to you?"
Memory management issues are usually symptoms of architectural issues. A GC won't fix your architecture, but it will make your memory management issues less visible.
It is my experience that most memory problems in C come from out-of-bounds writes (which includes a lot more than just array access), not from anything related to free(). A GC doesn't help here.
> In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in a way that memory which is no longer needed is not released.
In most of your scenarios, e.g. "pushed to some queue", the object in question is still needed. Hence, this is not a leak. Presumably, the entry will eventually be removed from the queue, and GC will then reclaim the object.
At any rate, I think we're moving past the point of productive discussion. My experience in practice is that memory leaks are more common / harder to avoid in non-GC languages. Of course a true memory leak (per the definition above) is possible in a GC language, but I just don't see it much in practice. Perhaps your experience is different.
What does memory leak free mean? It has a semantic meaning.
If I have a queue with elements and I won’t be accessing some of it in the next part of the program, is it a leak?
Also, remember that certain GCd languages intern strings, is it a memory leak since it will not necessarily use it anymore?
Well, you would need a way to "tell" the compiler more about your intentions and maybe even explicitly declare rules for what you consider a memory leak in certain scenarios.
To provide an example for a different case: programs written in Gallina have the weak normalization property, implying that they always terminate.
I have at times in the past made the (somewhat joking) observation that "memory leak" and "unintentional liveness" look very similar from afar, but are really quite different beasts.
With a "proper" GC engine, anything that is no longer able to be referenced can be safely collected. Barring bugs in the GC, none of those can leak. But, you can unintentionally keep references to things for a lot longer (possibly unlimited longer) than you need to. Which looks like a memory leak, but is actually unintentional liveness.
And to prove the difference between "cannot be referenced" (a property that can in principle be checked by consulting a snapshot of RAM at an instant in time) and "will not be referenced" (a larger set, we will never reference things that cannot be referenced, but we may not reference things that are reachable, depending on code) feels like it is requiring solving the halting problem.
And for free()-related problems, I've definitely seen code crash with use-after-free (and double-free).
> I have at times in the past made the (somewhat joking) observation that "memory leak" and "unintentional liveness" look very similar from afar, but are really quite different beasts.
Both situations prevent you from reusing memory previously used by other objects, which isn't being utilized for anything useful at that point. The distinction is valid formally, but from a practical point of view sounds rather academic.
> And to prove the difference between "cannot be referenced" (a property that can in principle be checked by consulting a snapshot of RAM at an instant in time) and "will not be referenced" (a larger set, we will never reference things that cannot be referenced, but we may not reference things that are reachable, depending on code) feels like it is requiring solving the halting problem.
As usual, looking for a general solution to such a problem is probably a fool's errand. It's much easier to write code simple enough that it's obvious where things are referenced. Rust's lifetime semantics help with that (if your code isn't all that simple, it will be apparent in the overload of punctuation). If not Rust, then at least it would be good if you could check liveness of an object in a debugger. In C you can check whether a particular allocation was undone by a free() or equivalent. I'm not aware of any debugger for e.g. Java which would let me point at a variable and ask it to notify me when it's garbage collected, but it sounds like something that shouldn't be too hard to do, if the debugger is integrated with the compiler.
You can write Rust code with almost no lifetine annotations. That's heavily dependent on the domain, of course. But for relatively simple function signatures they are usually inferred correctly, and you can often just clone instead of requiring references.
Pretty sure if you’re deserializing a structure with borrowed references you need lifetime annotations. Copying things around to pacify the compiler hardly seems elegant.
It’s not just performance, but you now have to go back and update every instance of that struct to reflect the change in ownership of that member if you can even change the member in the first place (e.g., you can’t change the type of a member of the struct itself is defined in a third-party package). Moreover, some instances of your struct might be in a tight loop and others might not, but now you’re committing to poorer performance in all cases. Maybe you can box it (although IIRC I’ve run into other issues when I tried this) but in any case this isn’t the “elegance” I was promised. Which is fine, because Rust is a living, rapidly-improving language, but let’s not pretend that there is an elegant solution today.
Libraries can be (and some actually are) designed with that in mind, with COW (copy on write( types that can be either borrowed or owned. Speaking of performance, kstring (of liquid) goes one step further with inline variants for short strings.
I want to say that seems like a valid concern but in practice I've seen it come up only rarely.
Most languages just clone all day long, it's not that bad, rust clones (like most languages) are just to the first reference counted pointer after all.
Eg cloning a string leads to an extra allocation and a memcopy.
If you want to get a similar performance profile to GC languages, you have to stick your types behind a `Rc<T>>/Arc<T>` or `Rc<RefCell<T>> / Arc<Mutex<T>>` if you need mutability.
But modern allocators hold up pretty well to a GC, which amortizes the allocations. The extra memcopying can be less detrimental than one might think.
“Yes and no” is too generous; most languages clone only very infrequently (basically only for primitives where there is no distinction between a deep and shallow copy). For complex types (objects and maps and lists) they pass references (sometimes “fat” references, but nevertheless, not a clone).
in JavaScript, one is just using the variable name as a "holder" of some value. One doesn't have to designate that that variable is being passed by reference. If one wanted to actually copy that object, they'd have to devise a mechanism to do so. In Rust, if someone doesn't specify, using the & symbol, that something is a reference, it'll end up moving the value.
Basically all I was saying is one can not approach writing Rust with a Java/JavaScript mindset. (That a variable is just a bucket holding a value). Care needs to be taken when referencing a variable as it may need to be moved, copied/cloned or referenced. In the case of copying, another memory allocation is done. So if someone approaches Rust from the standpoint of "this word represents a value, and I'm going to use it all over the place", they can find themselves blindly allocating memory.
Oh, by “reuse variable names”, you simply mean that values aren’t moved, i.e., that JS lacks move semantics or an affine type system. That’s a bit different to “reusing variable names”, which you can do in Rust as well provided the variable holds a (certain kind of?) reference.
I agree it's not pretty. It took me several passes to just understand what was going on there. But ownership is something I've never come across before and it doesn't exist in Python (not to an average user anyway), so it seems a little unfair to compare Rust and Python based on that feature.
However when you take a high level feature like overriding operators which can be done elegantly in Python, for a complied language, Rust's way is quite concise, readable and to my eyes quite pretty.
All those lifetime annotations are not sheer beauty or pretty, sure they are necessary but not nice to look at if you are comparing it to a higher level language.