6 ms·
Pretty surprising -- I had much the opposite experience. On our last product, we decided to start switching from Typescript to Rust on the backend because we g
by Darmani 5mo ago
Pretty surprising -- I had much the opposite experience.
On our last product, we decided to start switching from Typescript to Rust on the backend because we got tired of crashes. I consider that to be one of the greatest technical mistakes I've made ever, as our productivity slowed massively. I'll just share two time-draining issues that only occur in Rust: (1) Writing higher-order functions (e.g.: a function to open a database connection, do something, and then close it -- yes, I know you can use RAII for this particular example), which is trivial in Haskell and TypeScript and JavaScript and C++ and PHP, turned out to be so impossible in Rust [even after asking Rust-expert friends for help], that I learned to just give up and never try, though it sometimes worked to write a macro instead. (2) It's happened many times that I would attempt a refactoring, spend all day fixing type errors, finally get to the top-level file, get a type error that's actually caused somewhere else by basic parts of the design, and conclude the entire refactoring I had attempted is impossible and need to revert everything.
On top of that, Rust is the only modern language I can name where using a value by its interface instead of its concrete type lies somewhere between advanced and impossible, depending on what exactly you're doing.
I came away concluding that application code (as opposed to systems or library code) should, to a first approximation, never be written in Rust.
- IshKebab 5mo agoThat is a very unusual Rust experience. I find "application code" very pleasant to write in Rust. Of course there are things that aren't as ergonomic in Rust as in other languages (e.g. callbacks) but that's true of pretty much any language.
- Darmani 5mo agoI have heard this reaction from others before. One of the Rust expert friends I consulted with told me "I'm not convinced you're not trying to write Haskell-style code in Rust;" I told him the patterns I was struggling with were both trivial and common in Java. The things I found quite difficult or impossible in Rust were to me pretty basic patterns for modularity and removing duplication that it's really shocking that these complaints are not more common. I currently have but two hypotheses for why. First, the second problem I mentioned only comes from using tokio, which causes your top-level program to secretly be using a defunctionalized continuation data type, derived from where exactly in other files you put your await's, that might not be Send. If you're not using tokio, you won't experience that issue. Second...I was kinda told to just give up on deduplication and have lots of copy+pasted code. This raises the very uncomfortable hypothesis that Rust afficionados are some combination of people who came to Rust early and never learned traditional software design and don't know what they're missing, and people who were raised on traditional good software engineering but then got hit with Rust's metaphorical baseball bat of lack-of-modularity over and over until they got used to being hit with a baseball bat as a normal pain of life. I don't like either of these explanations (esp. with tokio seeming quite dominant), so I'm awaiting an explanation that makes more sense. https://xkcd.com/3210/ https://xkcd.com/3210/
- IshKebab 5mo ago> Rust afficionados are some combination of people who came to Rust early and never learned traditional software design and don't know what they're missing This is definitely not the case and is unnecessarily insulting. The truth is that some things are harder in Rust but a) often those things are best avoided anyway (e.g. callbacks), and b) it's worth the trade-off because of the other good things it allows. Surely as a Haskell user of all things you must understand that sometimes making things harder is worth the trade-off. Yeay everything is pure! Great for many reasons. Now how do I add logging to this deeply nested function?
- Darmani 5mo ago> is unnecessarily insulting. I know that it's insulting! And it doesn't make sense, because I generally think Rust programmers are smart people. But right now, it's the only explanation I've got, so it is alas necessarily insulting. So please, please, please give me a better explanation that actually makes sense. > The truth is that some things are harder in Rust but a) often those things are best avoided anyway (e.g. callbacks), and b) it's worth the trade-off because of the other good things it allows. This sounds like the seeds of a better explanation, but it needs a lot more to actually suffice. E.g.: why are callbacks best avoided anyway, when they're virtually required for a large number of important programming patterns? (In more technical language: they're effectively the only way to eliminate duplication in non-leaf-expressions. In even more technical language: they're the way to do second-order anti-unification.) > Surely as a Haskell user of all things you must understand that sometimes making things harder is worth the trade-off. Yeay everything is pure! Great for many reasons. Now how do I add logging to this deeply nested function? And this is a great illustration of the difference. First, you will seldom find Haskell programmers trying to argue that, actually, things like deeply-nested logging that everyone wants are actually "best avoided anyway." Second, you'll actually get a solution if you ask about them -- in this case, to either use MTL-style, to use a fixed alias for your monad stack, or that unsafePerformIO isn't actually that bad. BTW, similar to my unpleasant conclusion for Rust above, I have another unpleasant conclusion for Haskell: Haskell is incredible for medium-sized programs, but it has its own missing modularity features that make it non-ideal for large programs (e.g.: >50k lines). But this is a much smaller problem than it sounds because Haskell is so compact that, while many projects can be huge, very few individual codebases will need to approach that size.
- jvuygbbkuurx 5mo agoMaybe it depends on the application, but web servers are effortless with something like axum. Libraries can do a lot of heavy lifting to expose straightforward coding patterns. Never had any problems like you desribed with database connections and such. In rust with db pools things just work and get closed on drop etc. I would never even consider making a higher order function for that. Only other language that I think gets close to rust ergonomics is Kotlin, but it suffers from having too many possibilities for abstractions.
- pjmlp 5mo agoDepends very much on the market. On my line of work we don't do Web servers from scratch, we use lego pieces like with enterprise integrations. Think Sitecore, Dynamics, Sharepoint, Optimizely, Contentful, SAP, Mongolia, Stripe, PayPal, Adobe, SQL Server, Oracle, DB2,..... Axum offers very little over existing .NET, Java, nodejs SDKs provided by those vendors.
- adastra22 5mo ago(1) Higher order functions are pretty much the same as all the other languages you mentioned, using closure syntax? What was the problem you ran into? (2) In such situations the compiler (type system or borrow checker) is telling you that what you wanted to do has hidden bugs, and therefore refuses to compile. Usually that's a good thing. (3) &dyn Trait
- Darmani 5mo ago(1) Oh sure, the syntax is easy. Getting it to borrow-check is somewhere between insane and impossible. As I said, I've had friends who are actual Rust experts give up trying. (2) No, it stems from a compiler limitation (imposed in large part by the need for static memory layout), not because there's anything intrinsically buggy about doing this. (3) Look up "dyn-compatibility", for the largest, but not the only, problem with doing this.
- yobbo 5mo agoIf your goal is to translate Haskell (or other garbage collected code) pattern-for-pattern into Rust, you will almost certainly burn out.
- Darmani 5mo agoIt seems to be a common reflex of Rust advocates that, whenever an issue with using the language is asked about, the response is "That's just a garbage-collected code pattern" followed by "and therefore you shouldn't want it." It's happened multiple times in this thread. [Edit: and both the times I was thinking of were from you, so need to weaken that conclusion] Aside from having vibes of "I've chosen to get hit weekly in the face with a baseball bat, but have learned to like it, and so should you" it's also seldom true. All three of these examples are also quite easy to do with C and C++. It's not about garbage collection.
- Boxxed 5mo ago"It's possible to write <lang-X> in <lang-Y>" is a common trope, but "It's possible to write <lang-X> in Rust" is painful and borderline impossible in my experience. I don't mean this as a defense of rust, I just think it's why the learning curve is so harsh.
- pjmlp 5mo agoI appreciate Rust for making affine types mainstream, and having at least the C++ community start caring about security, even if half hearted. However I share your conclusion, outside scenarios where having automated resource management as the main approach is either technically impossible, or a waste of time trying to change pervasive culture, I don't see much need for Rust. In fact those that write comments about wanting a Rust but without borrow checker, the answer already exists.
- Darmani 5mo agoI think Rust would be fine for application code if it kept the borrow checker, but had greater allowance for dynamically-sized variables, or even garbage collection. The reason calling things through an interface is so tough in Rust is because doing so requires having a pointer to a value of unknown size, which involves either heap allocation or alloca(), neither of which are very happy in Rust. Many of the other things I complained about are also downstream of this decision. Affine types are useful both in high-level state management as well as in low-level memory management. But it's Rust's focus on static memory layout that really cements it as a low-level systems language, not its inclusion of borrowing. Way back as an undergrad in 2011, I contributed to Plaid, a JVM language whose main feature is based on affine and linear types. I'm one of the very few people in the world who knew what borrowing is before Rust had it. So I know first-hand that borrow-checking is perfectly compatible with garbage collection.
- pjmlp 5mo agoExactly, and that is why after Rust's break into mainstream, several garbage collected languages are trying to mix advanced type systems with their approach to garbage collection (GC, RC, a mix of both, whatever). This is also not strange for those in the Rust community with type systems experience, hence the Roadmap 2026 proposals for a more ergonomic experience. Thus we have Linear Haskell, Swift 6 ownership, D ownership, Koka, Hylo, Chapel, OxCaml, Scala Capabilities, Ada/SPARK proofs, Idris, F*, Dafny,....
- Darmani 5mo ago
- dagi3d 5mo agoThat's pretty interesting. I was thinking about starting a new pet project and was considering doing it in Rust to learn as I never tried anything with it and after some small pocs I had the feeling it was too verbose to my taste, but wasn't sure it was just me and/or my lack of experience with Rust. Still, wonder if it's still worth it to give a shot considering other positive elements of the language.
- Darmani 5mo agoRust is definitely very verbose. I think it's a fine choice -- probably even the best choice -- if you're doing systems code or if performance is your most important feature. If not, I would pass.
- tcfhgj 5mo ago> performance or less ressource hungry software
- mplanchard 5mo agoIt’s worth learning, in my opinion, but I’ve been writing it professionally for the better part of a decade, so my opinion may be a bit skewed. It’s my favorite language to write, and it gets much easier over time. As a first approximation, if you’re doing something and it feels insanely difficult like the GP is talking about, try to think of a different way to do it rather than fighting it. There’s usually a way to do almost anything, but it’s more pleasant to lean into the grooves the language pushes you towards.
- jandrewrogers 5mo agoVerbosity aside, whether or not Rust is a good fit depends on what you are doing. The language design is broadly optimized for low-level application code, like command-line utilities. If that is the use case then you are likely to have a good experience. For high-performance and high-reliability systems code, Rust is much more of a mixed bag. In a systems context it lacks the ability to easily and ergonomically express idiomatic constructs important for safety and performance that are trivial to express in e.g. C++. When you run into these cases it can get pretty ugly. Most people don't write this kind of systems code. What most people call "systems code" is really more like low-level applications code, where Rust excels. It is software like highly-optimized kernel-bypass database engines and similar where the limitations start to show.
- atombender 5mo agoI'm not a Rust expert by any means, but I'm surprised to hear this. In my Rust code, doing anything with a database connection is not at all different from, say, Go or TypeScript. For example, I use the deadpool-postgres crate for database pooling. Getting a connection looks like this: let conn = self.pool.get().await?; Because of RAII, you don't need a higher-order function helper, but if you really wanted to make one: async fn with_conn<T>( &self, f: impl AsyncFnOnce(Object<Manager>) -> Result<T>, ) -> Result<T> { let conn = self.pool.get().await?; f(conn).await } Now you can do: with_conn(|conn| async move { conn.query("SELECT 1") // Or whatever }).await If you know TypeScript, this shouldn't be too difficult to read or write. The gnarliest stuff here is knowing the type signature of the function argument; because of async, it must be AsyncFnOnce, for one, and you need to know that the type that the deadpool crate returns is called Object<Manager> (which doesn't sound like a connection, to be fair). Determining the exact concrete type to match type constraints on is sometimes a chore, but TypeScript is surely no different here! If you don't know Rust too well, the "move" part will be a little mysterious, to be sure.
- Darmani 5mo agoJust investigated -- looks like this works now! Yay! For this family of examples, had been completely stymied by AsyncFnOnce not being released yet. IIRC it had been in the works for several years, was still an experimental feature when I was trying to use it, and I gave up after much frustration at trying to get a version of Rust with experimental features working under devenv (nix). A subtraction then to my frustrations with Rust -- though I'd still be very wary of doing this, having seen how fragile higher-order functions have been in the past.
- atombender 5mo agoThat explains it. I think async closures were stabilized a year ago. Before that, you'd have needed to write out the async signature as non-async with futures (that's what async is syntactic sugar for, anyway). Something like: f: impl FnOnce(Object<Manager>) -> impl Future<Output = Result<T>>