5 ms·
The first code snippet, which is as simple as it gets, perfectly illustrates why Rust is extremely annoying to work with. I understand why you need the into_ite
by diath 1y ago
The first code snippet, which is as simple as it gets, perfectly illustrates why Rust is extremely annoying to work with. I understand why you need the into_iter bit and why the borrow checker complains about it, but the fact that even the simplest "for x in y" loop already makes you wrestle the compiler is just poor ergonomics.
- baq 1y ago> wrestle the compiler This is quite literally a skill issue, no offense. 'wrestle the compiler' implies you think you know better; this is usually not the case and the compiler is here to tell you about it. It's annoying to be bad at tracking ownership and guess what: most people are. The ones who aren't have decades of experience in C/C++ and employ much the same techniques that Rust guides you towards. If you really know better, there are ways to get around the compiler. They're verbose and marked unsafe to 1) discourage you from doing that and 2) warn others to be extra careful here. If this is all unnecessary for you - and I want to underscore I agree it should be in most software development work - stick to GC languages. With some elbow grease they can be made as performant as low level languages if you write the critical parts in a way you'd have to do it in Rust and will be free to write the rest in a way which doesn't require years of experience tracking ownership manually. (Note it won't hurt to be tracking ownership anyway, it's just much less of an issue if you have to put a weakref somewhere once a couple years vs be aware at all times of what owns what.)
- 59nadir 1y agoNo, GP can just not use Rust, they don't have to use GC languages to have something that makes sense and doesn't force you to always have a debate with the compiler about even simple things. If they used Odin (or Zig) they could've looped through that dynamic array no problem, in fact: package example import "core:fmt" main :: proc() { xs: [dynamic]int append(&xs, 1, 2) for x in xs { fmt.println(x) } fmt.println(len(xs)) } It is ridiculous that Rust complains even about the simple for loop and to say that this somehow comes down to "Well, everyone would do it this way if they cared about memory safety" is just not really true or valuable input, it sounds like what someone would say if their only systems programming experience came from Rust and they post-rationalized everything they've seen in Rust as being how you have to do it. My tips to people who maybe feel like Rust seems a bit overwrought: Look for something else, check out Odin or Zig, they've got tons of ways of dealing with memory that simply sidestep everything that Rust is about (because inherently Rust and everything that uses RAII has a broken model of how resources should be managed). I learned Odin just by reading its Overview page (https://odin-lang.org/docs/overview/ https://odin-lang.org/docs/overview/) and trying stuff out (nowadays there are also good videos about Odin on YouTube), then found myself productively writing code after a weekend. Now I create 3D engines using just Odin (and we in fact use only a subset of what is on that Overview page). Things can be simple, straight forward and more about the thing you're solving than the language you're using.
- mjburgess 1y agoRust, in many ways, is a terrible first systems programming language. To program a system is to engage with how the real devices of a computer work, and very little of their operation is exposed via Rust or even can be exposed. The space of all possible valid/safe Rust programs is tiny compare to the space of all useful machine behaviours. The world of "safe Rust" is a very distorted image of the real machine.
- masklinn 1y ago> Rust, in many ways, is a terrible first systems programming language. Contrariwise, Rust in, in many way, an awesome first systems programming language. Because it tells you and forces you to consider all the issues upfront. For instance in 59nadir's example, what if the vector is a vector of heap-allocated objects, and the loop frees them? In Rust this makes essentially no difference, because at iteration you tell the compiler whether the vector is borrowed or moved and the rest of the lifecycle falls out of that regardless of what's in the vector: with a borrowing iteration, you simply could not free the contents. The vector generally works and is used the same whether its contents are copiable or not.
- mjburgess 1y agoNone of these "issues" are systems issues, they're memory safety issues. If you think systems programming is about memory saftey, then you're demonstrating the problem. Eg., some drivers cannot be memory safe, because memory is arranged outside of the driver to be picked up "at the right time, in the right place" and so on. Statically-provable memory saftey is, ironically, quite a bad property to have for a systems programming language, as it prevents actually controlling the devices of the machine. This is, of course, why rust has "unsafe" and why anything actually systems-level is going to have a fair amount of it. The operation of machine devices isnt memory safe -- memory saftey is a static property of a program's source code, that prevents describing the full behaviour of devices correctly.
- dralley 1y ago
- diath 1y agoThis is yet another issue with Rust, nowhere in my post have I mentioned C++, I made no effort comparing the two languages, I just pointed out poor developer ergonomics in Rust and you're instigating a language flame war as if you took valid criticism as a personal attack; you can do better than that.
- ninkendo 1y agoMy 2¢, it’s perfectly reasonable to bring up other languages in defense of criticism, because it explains why these decisions were made in the first place. GP literally said that rust isn’t a good fit for you if you’re in a position to use a GC. The comparison to C++ is important because it’s one of very, very few contemporary languages that also doesn’t require a GC/refcounting everywhere. So it’s useful to compare to how C++ does it. Yet another issue with people who criticize rust: they don’t want anyone to defend rust, and complain loudly about anyone defending rust as being a literal problem with the language. You can do better than that.
- Ygg2 1y ago> poor developer ergonomics I don't think it's poor developer ergonomics. The compiler tells you "Hey, try adding &x at this position". It's unfamiliarity with Rust's type system and syntax sugars. <HYPERBOLE> I'll take 1000 compiler errors over a single non-deterministic bug that only happens on ARM at 6 o'clock on Blue Moon when Mercury is in Orion (it's UB). And I'd ritually sacrifice my first unborn werekid to the Dark Compiler Gods for a compiler error, that actually suggests a correct fix. </HYPERBOLE>
- mjburgess 1y agoThat systems languages have to establish (1) memory saftey, (2) statically; (3) via a highly specific kind of type system given in Rust; and (4) with limited inference -- suggests a lack of imagination. The space of all possible robust systems languages is vastly larger than Rust. It's specific choices force confronting the need to statically prove memory saftey via a cumbersome type system very early -- this is not a divine command upon language design.
- kobebrookskC3 1y agosure, rust is not the final answer to eliminating memory safety bugs from systems programming. but what are the alternatives, that aren't even more onerous and/or limited in scope (ats, frama-c, proofs)?
- mjburgess 1y agoMy preference is to have better dynamic models of devices (eg., how many memory cells does ram have, how do these work dynamically, etc.) and line them up with well-defined input/output boundaries of programmes. Kinda "better fuzzing". I mean, can we run a program in a well-defined "debugging operating system" in a VM, with simulated "debugging devices" and so on? I dont know much about that idea, and the degree to which that vision is viable. However it's increasingly how the most robust software is tested -- by "massive-scale simulation". My guess is it isnt a major part of, say, academic study because its building tools over years rather than writing one-off papers over months. However, if we had this "debuggable device environment", i'd say it'd be vastly more powerful than Rust's static guarantees and allow for a kind of "fearless systems programming" without each loop becoming a sudoku puzzle.
- Ygg2 1y ago> The space of all possible robust systems languages is vastly larger than Rust. The space of all possible CVE is also vastly larger outside of Rust as well. My biggest takeaway from Rust isn't that it's better C++. But that it's extremely fast (no runtime limited GC) and less footgunny Java.
- shakna 1y ago> It's annoying to be bad at tracking ownership and guess what: most people are. The ones who aren't have decades of experience in C/C++ and employ much the same techniques that Rust guides you towards. You wouldn't need to do that here, in SPARK, or Oberon, or just about any other memory safe language. This is unique to Rust, and their model - and it absolutely is not required for safety. It's not a skill issue. It's a language design problem.
- kobebrookskC3 1y agodoesn't spark do something inspired by rust to get safe dynamic memory allocation? https://docs.adacore.com/spark2014-docs/html/ug/en/source/access.html https://docs.adacore.com/spark2014-docs/html/ug/en/source/ac... what does oberon do?
- shakna 1y agoSPARK has had it before Rust existed. However, gnatprove doesn't require you the programmer to change anything. The compiler does the work to ensure safety, not you. Oberon is similar. The typesolver will determine if something is safe, without the need for explicitly borrowing anything.
- kobebrookskC3 1y ago> Oberon is similar. The typesolver will determine if something is safe, without the need for explicitly borrowing anything. so what does oberon do to prevent you from resizing a possibly reallocating array while holding a reference into it?
- shakna 1y agoIt doesn't allow those operations to occur at the same time. If you can't meet compile time guarantees, then it does not compile. Pointers aren't the same as in C. A pointer has an explicity type, not just a size. A pointer cannot change where it is located, whilst in scope anywhere else. If you then make your pointer local, then it will get cloned. As there's no concept of a void pointer, every type supports cloning, and so your thread-local variable will have nothing to do with the parent any longer. So if you try to grab a local reference, to something in another thread, you'll get a copy, or a compile time error if you don't copy it. If you try to modify something you're looping over, it won't compile at all. However, in all of this, there's no extra syntax. The compiler can deal with what is permitted. The programmer can just write. This is multithreaded (by compiler switch): module example610a; type Vector = array * of integer; var i, n: integer; a: Vector; begin a := new Vector(n); for i := 0 to len(a) - 1 do write("a[",i:2,"]: "); read(a[i]) end; writeln; for i := 0 to len(a) - 1 do write(a[i]:3); end; writeln; end example610a.
- pron 1y ago> 'wrestle the compiler' implies you think you know better; this is usually not the case and the compiler is here to tell you about it. Well, yes and no. The way type systems work to soundly guarantee that some program property P holds is by guaranteeing some stronger property Q, such that Q => P. This is because type systems generally enforce what we call "inductive invariant", i.e. a property that is preserved by all program statements [1], while most interesting program properties are not inductive. To give an example, suppose we're interested in the property that a program produces an even number; an inductive invariant that implies that property is one that makes sure that the outcome of all computations in the language are even. A program that satisfies the latter property obviously satisfies the former, but the converse isn't true. Similarly, the way Rust guarantees that all programs don't have, say, use-after-free, is by enforcing a stronger property around ownership. So all safe Rust programs don't have use-after-free, but many programs that don't have use-after-free don't satisfy the stronger ownership property. This means that sometimes (and this is true for all sound type systems) you have to "wrestle" the compiler, which insists on the stronger property, even though you know that the weaker property -- the one you're interested in -- holds. In other words, sometimes you do know better than the compiler. That is not to say that the approach where the compiler enforces stronger invariants is always right or always wrong, or even right or wrong most of the time, but that "wrestling the compiler" is something that even the most skilled programmers confront from time to time. [1]: This is because inductive invariants are compositional, i.e. they hold for some composition of program terms t and s iff they hold for t and s and their composition operator, and type systems want to be compositional.
- 0xdeafbeef 1y ago#include <vector> #include <iostream> int main() { std::vector<int> vec = {1, 2, 3}; vec.reserve(3); std::cout << "Initial capacity: " << vec.capacity() << std::endl; std::cout << "Initial data address: " << (void*)vec.data() << std::endl; int* ptr = vec.data(); std::cout << "Pointer before push_back: " << (void*)ptr << std::endl; std::cout << "Value via pointer before push_back: " << *ptr << std::endl; std::cout << "\nPushing back 4...\n" << std::endl; vec.push_back(4); std::cout << "New capacity: " << vec.capacity() << std::endl; std::cout << "New data address: " << (void*)vec.data() << std::endl; std::cout << "\nAttempting to access data via the old pointer..." << std::endl; std::cout << "Old pointer value: " << (void*)ptr << std::endl; int value = *ptr; std::cout << "Read from dangling pointer (UB): " << value << std::endl; return 0; } ./a.out Initial capacity: 3 Initial data address: 0x517d2b0 Pointer before push_back: 0x517d2b0 Value via pointer before push_back: 1 Pushing back 4... New capacity: 6 New data address: 0x517d6e0 Attempting to access data via the old pointer... Old pointer value: 0x517d2b0 Read from dangling pointer (UB): 20861
- scoutt 1y agoYou are not fighting the C++ compiler or showing why the C++ compiler might be annyoing. You are introducing a bug by poorly using a library (which has nothing to do with writing and compiling C++). Ergonomics I believe are fine? I'm struggling hard trying to understand what or if your comment has anything to do with GP's comment. Perhaps you wanted to tell that the Rust compiler might have stopped you from producing a buggy program, but again, it has nothing to do with GP's comment.
- genrilz 1y agoI think 0xdeafbeef is roughly recreating the first code snippet from the article (which is one of the things diath is complaining about) in C++ to show that the compiler should produce an error or else undefined behavior could occur on resize.
- PaulDavisThe1st 1y agoYou opted to use features of std::vector that are documented to be unsafe (notably ::data()). This is the actual C++ translation of the opening code in TFA: #include <vector> #include <iostream> int main() { std::vector<int> vec = {1, 2, 3}; for (auto const & i : vec) { std::cout << i << std::endl; } } It is possible to use C++ to write unsafe code! Amazing! Some people want a language where this is not possible! Great!
- masklinn 1y ago> I understand why you need the into_iter bit and why the borrow checker complains about it The borrow checker is not really involved in the first snippet (in fact the solution involves borrowing). The compiler literally just prevents a UAF.
- 59nadir 1y agoThe compiler isn't protecting them from anything in this particular example, we can all see this is valid code. It's provably valid code, just not provable by the Rust compiler.
- masklinn 1y agoLiterally nothing in your comment is correct. In the Rust snippet vector is moved into the loop, and thus freed. There are situations where you hit the limits of the borrow checker, plenty of them. This is not one of them. Again, the borrow checker is not even involved in the original snippet.
- 59nadir 1y ago> In the Rust snippet vector is moved into the loop, and thus freed. Do you see any code in the snippet that requires it to be? This could be a simple read-only borrow because the actual logic of the program requires only that, and the value could live on happily after the loop. Literally nothing in this snippet requires anything else, you've just sort of assumed that the way Rust does it is the only thing that makes sense. It's not about what Rust currently does, really, it's about what it ought to do.
- x-yl 1y agoAre you suggesting Rust should automatically insert the borrow annotation because it is able to see that a borrow is sufficient? That would be quite unintuitive and make it ambiguous whether a for loop is borrowing or consuming the iterator without reviewing the body. I'd strongly argue that it should unambiguously do either one or the other and not try and read the author's mind.
- throwawaymaths 1y agoi think really the problem is not the compiler so much but that you need to be thinking about what `for` desugars to.
- dbdr 1y agoThe desugaring is an explanation of what's happening under the hood, but you don't need to think about that. Your mental model can simply be: for _ in x // Consumes (moves) x and iterates over the values in x for _ in &x // Borrows x and iterates over references to the values in x
- throwawaymaths 1y agodepends on what `into_iter` does in your type impl ive always felt that there is something a bit strange about a keyword reaching into the stdlib (rust is not the only language that does this), especially if it's only for sugaring.
- chrismorgan 1y agoThe alternative is duck-typing, hard-coding a method name, like Python’s __dunder__ methods. That’s just as strange. And for Rust, definitely unacceptable. I think C++’s range-based for loops work this way, basically hard-coding .begin() and .end(). Oh, and only working on array-like things due to hard-coding ++ as the operator to go to the next item, so it’s less powerful than Rust. It’s not a stable part of Rust, but you can provide your own definitions of lang items, not using even libcore. But they will need to be quite similar to the official one as the compiler will pretty much hard-code aspects of its definition. An alternative view of it is that the standard library is reaching into the compiler. This feels just as accurate as the other way round. The two can’t reasonably be separated. A good fraction of Rust’s standard library is stuff that needs to be there, couldn’t be anywhere else.
- throwawaymaths 1y ago> An alternative view of it is that the standard library is reaching into the compiler Well, no not really, because it's a keyword (I don't know enough about rust internals to know if there are special parser rules for `for`)? You could put `for` in the stdlib instead. As rust has macros, you could in principle implement it as a macro.