7 ms·
A pattern in these is code that compiles until you change a small thing. A closure that works until you capture a variable, or code that works in the main threa
by xdfgh1112 2y ago
A pattern in these is code that compiles until you change a small thing. A closure that works until you capture a variable, or code that works in the main thread but not in a separate thread. Or works until you move the code into an if/else block.
My experience with Rust is like this: make a seemingly small change, it balloons into a compile error that requires large refactoring to appease the borrow checker and type system. I suppose if you repeat this enough you learn how to write code that Rust is happy with first-time. I think my brain just doesn't like Rust.
- alfiedotwtf 2y agoI don’t know anyone who has gotten Rust first time around. It’s a new paradigm of thinking, so take your time, experiment, and keep at it. Eventually it will just click and you’ll be back to having typos in syntax overtake borrow checker issues
- stouset 2y agoYour supposition about Rust is correct. I’ll add that—having paid that upfront cost—I am happily reaping the rewards even when I write code in other languages. It turns out the way that Rust “wants” to be written is overall a pretty good way for you to organize the relationships between parts of a program. And even though the borrow checker isn’t there looking out for you in other languages, you can code as if it is!
- farresito 2y agoAs someone who is interested in getting more serious with Rust, could you explain the essence of how you should always approach organizing code in Rust as to minimize refactors as the code grows?
- oconnor663 2y agoIn my experience there are two versions of "fighting the borrow checker". The first is where the language has tools it needs you to use that you might not've seen before, like enums, Option::take, Arc/Mutex, channels, etc. The second is where you need to stop using references/lifetimes and start using indexes: https://jacko.io/object_soup.html https://jacko.io/object_soup.html
- meindnoch 2y ago>and start using indexes So basically raw pointers with extra hoops to jump through.
- nordsieck 2y ago> So basically raw pointers with extra hoops to jump through. That's one way to look at it. The other way is: raw pointers, but with mechanical sympathy. Array based data structures crush pointer based data structures in performance.
- jpc0 2y ago> Array based data structures crush pointer based data structures in performance Array[5] And *(&array + 5) generates the same code... Heap based non-contiguous data structures definitely are slower than stackbased contiguous data structures. How you index into them is unrelated to performance. Effectively pointers are just indexes into the big array which is system memory... I agree with parent, effectively pointers without any of the checks pointers would give you.
- frutiger 2y ago> pointers are just indexes into the big array which is system memory... I’m sure you are aware but for anyone else reading who might not be, pointers actually index into your very own private array. On most architectures, the MMU is responsible for mapping pages in your private array to pages in system memory or pages on disk (a page is a subarray of fixed size, usually 4 KiB). Usually you only get a crash if you access a page that is not currently allocated to your process. Otherwise you get the much more insidious behaviour of silent corruption.
- _dain_ 2y ago>How you index into them is unrelated to performance. Not true. If you store u32 indices, that can impose less memory/cache pressure than 64-bit pointers. Also indices are trivially serializable, which cannot be said for pointers.
- saghm 2y agoI'm fortunate enough not to have to often write code in other languages anymore, but my experience that writing code in ways that satisfies the compiler actually ends up being code I prefer anyhow. I was somewhat surprised at the first example because I haven't run into something like that, but it's also not really the style I would write that function personally (I'm not a big fan of repetitions like having `Some(x)` repeated both as a capture pattern and a return value), so on a whim I tried what would have been the way I'd write that function, and it doesn't trigger the same error: fn double_lookup_mut(map: &mut HashMap<String, String>, mut k: String) -> Option<&mut String> { map.get_mut(&k)?; k.push_str("-default"); map.get_mut(&k) } I wouldn't have guessed that this happened to be a way around a compiler error that people might run into with other ways of writing it; it just genuinely feels like a cleaner way for me to implement a function like that.
- khold_stare 2y agoIsn't that the opposite of the intended implementation? I don't write Rust, but I think your implementation will always return either `None` or the "fallback" value with the `"-default"` key. In the article, the crucial part is that if the first `map.get_mut()` succeeds, that is what is returned.
- brabel 2y agoA great example of how "if it compiles, it runs correctly" is bullshit.
- unshavedyak 2y agoYou're reaching pretty hard there. Your assertion is a massive strawman, the implication seeming to be that "every problem in your logic won't exist if it compiles" - no one thinks you can't write bad logic in any language. Rather it's about a robust type system that gives you tooling to cover many cases at compile time. Even if we ignore logic, Rust has plenty of runtime tooling and runtime issues can still happen as a result. A complaint i often have about Bevy (despite loving it!) is that it has a lot of runtime based plugins/etc which i'd prefer to be compile time. Axum for example has a really good UX while still being heavily compile time (in my experience at least). "If it compiles it works" is still true despite my complaints. Because i don't believe the statement even remotely implies you can't write bad logic or bad runtime code in Rust.
- ajb 2y agoIt sounds like the ideal, then, would be to detect the problematic patterns earlier so people wouldn't need to bang their heads against it.
- tonyarkles 2y agoI had a similar experience with Erlang/Elixir. The primary codebase I work with in $DAYJOB is C++ but structured very OTP-like with message passing and threads that can crash (via exceptions, we’re not catching defaults for example) and restart themselves. Because of the way we’ve set up the message passing and by ensuring that we don’t share other memory between threads we’ve virtually eliminated most classes of concurrency bugs.
- pdimitar 2y agoThat's the main use-case of Erlang/Elixir anyway: eliminate concurrency / parallelism bugs by copying data in the dangerous places, and make sure not to use locks but message passing instead. These two alone have eliminated most of the bugs I've wrote in other languages. So, same experience. And it taught me to be a better Golang and Rust programmer, too.
- rikthevik 2y agoThis is why I think cross-training is so important and I should do more of it. Even something relatively minor, like doing the Advent of Code in elixir (nim last year) has made my python significantly easier to reason about. If you quit mutating state so much, you eliminate whole classes of bugs.
- ngrilly 2y agoThat's also how I tend to program after having read Joe Armstrong's dissertation "Making reliable distributed systems in the presence of sodware errors" and having programmned in Go for a few years.
- ajross 2y ago> It turns out the way that Rust “wants” to be written is overall a pretty good way for you to organize the relationships between parts of a program That's what it promised not to do, though! Zero cost abstractions aren't zero cost when they force you into a particular design. Several of the cases in the linked article involve actual runtime and code size overhead vs. the obvious legacy/unchecked idioms.
- oconnor663 2y ago> vs. the obvious legacy/unchecked idioms You can go crazy with legazy/unchecked/unsafe stuff if you want to in Rust. It's less convenient and more difficult than C in some ways, but 1) it's also safer and more convenient in other ways, and 2) "this will be safe and convenient" isn't exactly the reason we dive into legacy/unchecked/unsafe stuff. And of course the greatest strength of the whole Rust language is that folks who want to do crazy unsafe stuff can package it up in a safe interface for the rest of us to use.
- ajross 2y ago> crazy unsafe stuff The first example in the linked article is checking if a value is stored in a container and doing something different if it's not than if it is. Hardly "crazy unsafe stuff".
- oconnor663 2y agoI think there's an important distinction here. A systems programming language needs to have all the max speed / minimum overhead / UB-prone stuff available, but that stuff doesn't need to be the default / most convenient way of doing things. Rust heavily (both syntactically and culturally) encourages safe patterns that sometimes involve runtime overhead, like checked indexing, but this isn't the same as "forcing" you into these patterns.
- ajross 2y agoAnd I can only repeat verbatim: The first example in the linked article is checking if a value is stored in a container and doing something different if it's not than if it is. Hardly "max speed / minimum overhead / UB-prone stuff"
- kazinator 2y agoWhy would you cling to some cockamamie memory management model, where it is not required or enforced? That's like Stockholm Syndrome.
- joshka 2y agoRust definitely forces you to make more deliberate changes in your design. It took me about 6 months to get past hitting that regularly. Once you do get past it, rust is awesome though.
- brabel 2y agoI suppose you haven't had to refactor a large code base yet just because a lifetime has to change?
- merb 2y agoactually the higher versions of rust actually do need these refactors way less often since more lifetimes can be elided and when using generic code or impl traits you can basically scrap a ton of it. I still sometimes stumble upon the first example tough but most often it happens because I want to encapsulate everything inside the function instead of doing some work outside of it.
- nostradumbasp 2y agoNope. I have worked professionally for several years on what would now be considered a legacy rust code base. Probably hundreds of thousands of lines, across multiple mission critical applications. Few applications need to juggle lifetimes in a way that is that limiting, maybe a module would need some buffing, but not a major code base change. Most first pass and even refined "in production" code bases I work on do not have deeply intertwined life-times that require immense refactoring to cater to changes. Before someone goes "oh your team writes bad code!", I would say that we had no noteworthy problems with lifetimes and our implementations far surpassed performance of other GC languages in the areas that mattered. The company is successful built by a skeleton crew and the success is owed too an incredibly stable product that scales out really well. I question how many applications truly "need" that much reference juggling in their designs. A couple allocations or reference counted pointers go a really long way to reducing cognitive complexity. We use arenas and whatever else when we need them, but no I've never dealt with this in a way that was an actual terrible issue.
- resonious 2y agoMaybe I'm just brainwashed, but most of the time for me, these "forced refactors" are actually a good thing in the long run. The thing is, you can almost always weasel your way around the borrow checker with some unsafe blocks and pointers. I tend to do so pretty regularly when prototyping. And then I'll often keep the weasel code around for longer than I should (as you do), and almost every time it causes a very subtle, hard-to-figure-out bug.
- twic 2y agoI think the problem isn't that the forced changes are bad, it's that they're lumpy. If you're doing incremental development, you want to be able to to quickly make a long sequence of small changes. If some of those changes randomly require you to turn your program inside-out, then incremental development becomes painful. Some people say that after a while, they learn how to structure their program from the start so that these changes do not become necessary. But that is also partly giving up incremental development.
- NotCamelCase 2y agoMy concern is slightly different; it's the ease of debugging. And I don't mean debugging the code that I (or sb else) wrote, but the ability to freely modify the code to kick some ideas around and see what sticks, etc. which I frequently need to do, given my field. As an example, consider a pointer to a const object as a function param in C++: I can cast it away in a second and modify it as I go on my experiments. Any thoughts on this? How much of an extra friction would you say is introduced in Rust?
- resonious 2y agoI would say it's pretty easy to do similar stuff in Rust to skirt the borrow checker. e.g. you can cast a mut ref to a mut ptr, then back to a mut ref, and then you're allowed to have multiple of them. The problem is Rust (and its community) does a very good job at discouraging things like that, and there are no guides on how to do so (you might get lambasted for writing one. maybe I should try)
- IshKebab 2y agoYeah I think this becomes more true the closer your type system gets to "formal verification" type systems. It's essentially trying to prove some fact, and a single mistake anywhere means it will say no. The error messages also get worse the further along that scale you go (Prolog is infamous). Not really unique to Rust though; I imagine you would have the same experience with e.g. Lean. I have a similar experience with a niche language I use that has dependent types. Kind of a puzzle almost. It is more work, but you get lots of rewards in return (including less work overall in the long term). Ask me how much time I've spent debugging segfaults in C++ and Rust...
- deleted 2y ago[deleted]
- ykonstant 2y agoLean is far more punishing even for simple imperative code. The following is rejected: /- Return the array of forward differences between consecutive elements of the input. Return the empty array if the input is empty or a singleton. -/ def diffs (numbers : Array Int) : Array Int := Id.run do if size_ok : numbers.size > 1 then let mut diffs := Array.mkEmpty (numbers.size - 1) for index_range : i in [0:numbers.size - 2] do diffs := diffs.push (numbers[i+1] - numbers[i]) return diffs else return #[]
- binary132 2y agoThat’s not what OP is discussing. OP is discussing corner cases in Rust’s typesystem that would be sound if the typesystem were more sophisticated, but are rejected because Rust’s type analysis is insufficiently specific and rejects blanket classes of problems that have possible valid solutions, but would need deeper flow analysis, etc.
- IshKebab 2y agoYes I know. You get the same effect with type systems that are closer to formal verification. Something you know is actually fine but the prover isn't quite smart enough to realise until you shift the puzzle pieces around so they are just so.
- amelius 2y ago> I suppose if you repeat this enough you learn how to write code that Rust is happy with first-time. But this assumes that your specifications do not change. Which we know couldn't be further from the truth in the real world. Perhaps it's just me, but a language where you can never change your mind about something is __not__ a fun language. Also, my manager won't accept it if I tell him that he can't change the specs. Maybe Rust is not for me ...
- stephenbennyhat 2y ago"Malum est consilium, quod mutari non potest" you might say.
- oconnor663 2y agoMy recommendation is that you do whatever you feel like with ownership when you first write the code, but then if something forces you to come back and change how ownership works, seriously consider switching to https://jacko.io/object_soup.html https://jacko.io/object_soup.html.
- amelius 2y agoIsn't that just reinventing the heap, but with indexes in a vector instead of with addresses in memory?
- oconnor663 2y agoYou could look at it that way. But C++ programs often use similar strategies, even though they don't have to. Array/Vec based layouts like this give you the option of doing some very fancy high-performance stuff, and they also happen to play nicely with the borrow checker.
- amelius 2y agoIt's very basic and not a general solution because the lifetimes of objects are now set equal. And there's no compaction, so from a space perspective it is worse than a heap where the space of deleted objects can be filled up by new objects. It is nice though that you can delete an entire class of objects in one operation. I have used this type of memory management in the context of web requests, where the space could be freed when the request was done.
- dinosaurdynasty 2y agoAnd in C++, those changes would likely shoot yourself in the foot without warning. The borrow checker isn't some new weird thing, it's a reification of the rules you need to follow to not end up with obnoxious hard to debug memory/threading issues. But yeah, as awesome as Rust is in many ways it's not really specialized to be a "default application programming language" as it is a systems language, or a language for thorny things that need to work, as opposed to "work most of the time".
- cogman10 2y agoC++ allows both more incorrect and correct programs. That's what can be a little frustrating about the BC. There are correct programs which the BC will block and that can feel somewhat limiting.
- AlotOfReading 2y agoIn most cases, those "correct" C++ are also usually buggy in situations the programmer simply hasn't considered. That's why the C++ core guidelines ban them and recommend programs track ownership with smart pointers that obey essentially the same rules as Rust. The main difference is that C++ smart pointers have more overhead and a bunch of implicit rules you have to read the docs to know. Rust tells you in (occasionally obscure) largely helpful compiler errors at the point where you've violated them, rather than undefined behavior or a runtime sanitizer.
- stouset 2y agoWhile this obviously and uncontroversially true in an absolute sense (the borrowck isn’t perfect), I think in the overwhelming majority of real-world cases its concerns are either actual problems with your design or simple and well-known limitations of the checker that have pretty straightforward and idiomatic workarounds. I haven’t seen a lot of programs designs in practice that are sound but fundamentally incompatible with the borrow checker. Every time I’ve thought this I’ve come to realize there was something subtly (or not so subtly) wrong with the design. I have seen some contrived cases where this is true but they’re inevitably approaches nobody sane would actually want to use anyway.
- rendaw 2y agoYo, everyone's interpreting parent's comment in the worst way possible: assuming they're trying to do unsound refactorings. There are plenty of places where a refactoring is fine, but the rust analyzer simply can't verify the change (async `FnOnce` for instance) gives up and forces the user to work around it. I love Rust (comparatively) but yes, this is a thing, and it's bad.
- joshka 2y agoYeah, Rust-analyzer's palette of refactorings is woefully underpowered in comparison to other languages / tooling I've used (e.g. Resharper, IntelliJ). There's a pretty high complexity bar to implementing these too unfortunately. I say this as someone that has contributed to RA and who will contribute more in the future.
- redman25 2y agoIf you are trying overly hard to abstract things or work against that language, then yes, things can be difficult to refactor. Here's a few things I've found: - Generics - Too much Send + Sync - Trying too hard to avoid cloning - Writing code in an object oriented way instead of a data oriented way Most of these have to do with optimizing too early. It's better to leave the more complex stuff to library authors or wait until your data model has settled.
- nostradumbasp 2y ago"Trying too hard to avoid cloning" This is the issue I see a certain type of new rustaceans struggle with. People get so used to being able to chuck references around without thinking about what might actually be happening at run-time. They don't realize that they can clone, and even clone more than what might "look good", and that it is super reasonable to intentionally make a clone, and still get incredibly acceptable performance. "Writing code in an object oriented way instead of a data oriented way" The enterprise OOP style code habits also seem to be a struggle for some but usually ends up really liberating people to think about what their application is actually doing instead of focusing on "what is the language to describe what we want it to do".
- QuadDamaged 2y agoWhen this happens to me, it’s mostly because my code is written with too coarse separation of concerns, or I am just mixing layers
- ajross 2y ago> A pattern in these is code that compiles until you change a small thing. I think that's a downstream result of the bigger problem with the borrow checker: nothing is actually specified. In most of the issues here, the changed "small thing" is a change in control flow that is (1) obviously correct to a human reader (or author) but (2) undetectable by the checker because of some quirk of its implementation. Rust set out too lofty a goal: the borrow checker is supposed to be able to prove correct code correct, despite that being a mathematically undecidable problem. So it fails, inevitably. And worse, the community (this article too) regards those little glitches as "just bugs". So we're treated to an endless parade of updates and enhancements and new syntax trying to push the walls of the language out further into the infinite undecidable wilderness. I've mostly given up on Rust at this point. I was always a skeptic, but... it's gone too far at this point, and the culture of "Just One More Syntax Rule" is too entrenched.
- summerlight 2y agoThis is because programming is not a work in a continuous solution space. Think in this way; you're almost guaranteed to introduce obvious bugs by randomly changing just a single bit/token. Assembler, compiler, stronger type system, etc etc all try to limit this by bringing a different view that is more coherent to human reasoning. But computation has an inherently emergent property which is hard to predict/prove at compile time (see Rice's theorem), so if you want safety guarantee by construction then this discreteness has to be much more visible.
- perrygeo 2y ago> make a seemingly small change, it balloons into a compile error that requires large refactoring to appease the borrow checker and type system Same experience, but this is actually why I like Rust. In other languages, the same seemingly small change could result in runtime bugs or undefined behavior. After a little thought, it's always obvious that the Rust compiler is 100% correct - it's not a small change after all! And Rust helpfully guides me through its logic and won't let my mistake slide. Thanks!