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/
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?
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.
Look up "callback hell". Basically they encourage spaghetti.
> you'll actually get a solution if you ask about them
You got solutions to your problems didn't you? Macros are a perfectly reasonable thing to use in Rust, even if they are best avoided where possible. Exactly like unsafePerformIO.
If you were expecting Rust to work perfectly in every situation... well it doesn't. GUI programming in particular is still awkward, and async Rust has more footguns than anyone is happy with.
Despite that it's still probably the best language we have for a surprisingly large range of domains.
Ah. I think you're confusing the general idea of a callback with one particular style of use. "callback hell" refers to the deep indentation that occurs when trying to program in monadic style in languages without syntactic support for monads. It was mostly solved by adding async/await syntax, aka syntactic support for the continuation monad. "Callback hell" is not spaghetti in any deep sense, merely syntactically cumbersome.
But a "callback" is a more general term, sometimes a synonym for "function parameter," sometimes for more narrow kinds of function parameter (e.g.: void function, invokable once). Many people will refer to the function argument of the `map` function as a callback, but no-one would refer to that as "callback hell."
Callbacks are quite universal, and most uses do not lead at all to callback hell. I've engaged with this topic a little bit at https://us16.campaign-archive.com/?u=8b565c97b838125f69e75fb... , above the header "Serious Business."
> You got solutions to your problems didn't you?
Mostly no :(
And when I did, I largely got it by figuring stuff out myself, while being told by multiple Rust experts that I either shouldn't care about the verbosity and lack of modularity, or that if I have a problem like "using the interface instead of the implementation" it must be because I'm a Haskeller.
Well, my ultimate solution was to start working on a new product, and to not use any Rust, except for some performance-heavy libraries. With the first product, the market had changed too much by the time we were ready for prime-time, and I'd put somewhere between 25% and 70% of the reason for that delay on our choice to start building new parts of the backend in Rust.
> Macros are a perfectly reasonable thing to use in Rust, even if they are best avoided where possible. Exactly like unsafePerformIO.
Good comparison!
> Despite that it's still probably the best language we have for a surprisingly large range of domains.
I agree with this. I just don't agree that that list of domains has a very large intersection with the set of applications.
Many things are plainly not permitted, either because the borrow-checker isn't clever enough, or the pattern is unsafe (without garbage collection and so on).
Many functional/Haskell patterns simply can not be translated directly to Rust.
A deeply-baked assumption of Rust is that your memory layout is static. Dynamic memory layout is perfectly compatible with manual memory management, but Rust does not readily support it because of its demands for static memory layout.
A very easy place to see this is the difference in decorator types between Rust and other languages like Java. Java's legacy File/reader API has you write things like `new PrintWriter(new BufferedWriter(new FileWriter("foo.txt")))`, where each layer adds some functionality to the base layer. The resulting value has principal type `PrintWriter` and can be used through the `Writer` interface.
The equivalent code in Rust would give you a value of type `PrintWriter<BufferedWriter<FileWriter>>` which can only be passed to functions that expect exactly that type and not, say, a `PrintWriter<BufferedWriter<StringStream>>`. You would solve this by using a template function that takes a `T where T: Writer` parameter and gets compiled separately for every use-site, thus contributing to Rust's infamous slow build times.
It would be perfectly sane, and desirable for application code, to be able to pass around a PrintWriter value as an owned pointer to a PrintWriter struct which contains an owned pointer to a BufferedWriter struct which contains an owned pointer to a FileWriter struct. You could even have each pointer actually be to a Writer value of unknown size, and thus recover modularity.
In Rust, there is sometimes a painful and very fragile way to do this: have each writer type contain a Box<&dyn Writer>, effectively the same as the Java solution above. This works, except that, if one day you want to add a method to the Writer trait that breaks dyn-compatibility, then you will no longer be able to do this, and will need to rewrite all code that uses this type.
Mostly, this works out well enough: dyn compatibility pretty much just insists your methods can in fact work with just a reference to an unknown variant of the type.
But I see the forums (and I also trued some toy stuff at times) plagued with rigidity problems that in C++ have obvious solutions.
For example, I am not going to fight a borrow-checker all the stack up to get a 0.0005% perf improvement, if sny, when I can use smart pointers.
I am not going to use Result everywhere when I can throw an exception and get done with it instead of refactoring all the stack up for the intermediate return types (though I use expected and optional and like them, but it is a choice depending on what I am doing).
I am not going to elaborate safe interfaces for my arrays of data I need to send to a GPU: there is no vslue in it and I can get it wrong snyway, it os ceremony. I assume this kind of code is unsafe by nature.
I find C++ just more flexible. Yes, it has warts, but I use all warnings as errors, clang tidy and have a lot of flexibility. I use values to avoid any trace of dangling and when it is going to get bad, I can, most of the time, switch to smart pointers.
I really do not get why someone would use Rust except for very niche cases like absolutely no memory unsafety (but this is not free either, as some reports show: you need to really be careful about reviewing unsafe if your domain is unsafe by nature or uses bindings to keep Rust invariants or you write only safe code, in whcih case, if memory safety is critical, it does give you something).
But I do not see Rust good for writing general application code. At least not compared to well-written C++ nowadays.
In no way I am saying it is useless. I just see niche uses for it compared to alternatives.
Rigidity is a trade off: it can make initial development slower but refactors significantly easier, just as an example.
I don’t think any of your examples show it to be niche. It operates well in most of the space where C++ is a good option, and a bit beyond that (embedded, firmware, but also higher level things where you want performance but don’t want to worry about memory safety).
Well, at the cost of having a straight jacket. Result without option for exception handling is an example. You need to refactor all the way up if you notice that suddenly when refactoring you needed a Result bc a new error appears that could not happen before or you need to preventively spam Result everywhere since the start. You need to handle those all the stack up. The borrow checker is also rigid. I do know why it exists. I understand its value. I am just talking about the toll it imposes while coding, and wondering if it is a good default (I think most of the time it is not, but when you need it, it is invaluable, however these cases are a minority).
Another insight is that when you really go low-level, most of the time you are working with unsafe interfaces probably. At that time, you are using unsafe and now you have to satisfy Rust's borrow checker. How? By hand. So you lost part of the value proposition.
Can you recover it? Yes. How? By reviewing that code. But if I have to review that code, what is better from choosing a language (in this situation I mean, there are situations where Rust is the better choice) where I can understand the invariants in unsafe code better and anyway I have linters and a lot of established guidelines that are not difficult to follow? And by not difficult to follow I mean they are embedded in tooling like clang-tidy, not that I can follow because I know a lot.
So for me it is not so obvious at all, especially in the presence of quite a few unsafe blocks. If you want it safe, at that time, you are starting to compete with other unsafe languages: you need human review anyways... if there is tooling in Rust for unsafe blocks (I can imagine there could be something), that improves things competitively for Rust in unsafe blocks. But if you need careful review, you are stuck again in the non-magical real world: things are safe if you checked absolutely everything.
> Rigidity is a trade off: it can make initial development slower but refactors significantly easier, just as an example.
That is certainly true. It is also true that in areas where you put this extra effort and quickly refactor, it makes things more difficult.
Refactoring, if you mix it with unsafe, needs a much more strict review than just pretending things are safe because you refactored and put things behind an unsafe interface and present it as safe.
I am not convinced at all this is what you need in most scenarios. The productivity impact is relatively high IMHO.
OTOH, if I really want correctness (real correctness!) but not absolute full speed, I think I can reach to Ocaml (very practical) or Haskell (this one is also a bit too rigid actually sometimes).
So I am left in a situation where Rust just seems to be appealing for places where the most absolute memory safety is needed. But memory safety is still a composed characteristic of a running program: you have to take into account unsafe interfaces, bindings, etc.
So the only way to get real safety is anyways to review everything (if that is what you really want to deliver), probably proving your code, which anyway requires human intervention. Did we ever (even if less often) see crashes for invariant violations in code advertised as safe in Rust? Certainly yes. I acknowledge it is usually an improvement, but still not a guarantee.
So if it is not a guarantee and I can reach other tools where anyway the guarantee is there through GC or other mechanisms and where it is not I am equal to Rust, then, why bother?
Probably the only place where I see Rust appealing is where you need both max. performance and absolute memory safety (but you will still need the kind of reviews I mentioned if you spam unsafe and interact with bindings anyway). Those are niche cases, not the norm.
I see like a suboptimal choice to write much of the application code in Rust, even when you need speed, compared to C++. C++ has very good tools for compile-time programming, expression templates, good warnings and linters, a big ecosystem and it is way more voluble (exceptions and results can be used, invariants in unsafe code are easier to follow since a borrow checker does not need to be satisfied "by hand").
So I am not sure at all Rust is the reply for a more or less mainstream general-purpose application language.
I think that Rust is valuable in things like OS hardened interfaces, etc. But even there CVEs were found! Right? https://news.ycombinator.com/item?id=46302621
There is no magic bullet here, but I do know that when coding in Rust, the productivity toll I am paying is not negligible and I can reach for tools and techniques that make me very close or equal to that productivity.
This was one of the example approaches I gave. This works...at first. The problem is that, if you want to add a new function to the Writer trait which makes Writer no longer dyn-compatible, such as, say, any async function, then you can no longer write `Box<dyn Writer>` and need to rewrite all code that uses it.
(although you can dig under the hood and specify a pinned-down Future type, covering one kind of awfulness with another)
FWIW I write primarily rust, and I do not agree with the advice given in your second point, so I’d take it with a grain of salt were I you.
Oh please. I came to Rust late, after using plenty of other languages. I have never had a problem with “modularity”. You express surprise that these issues haven’t been talked about more, well the null hypothesis is that you are just not very good at Rust and it hasn’t clicked for you - that’s fine, I doubt I would be very good at Haskell. But don’t insult us, and don’t assume your experience is de facto everyone’s and we’re just in denial. It’s incredibly arrogant.
Please respect the fact that I do not understand how one can like Rust without giving up caring about modularity.
I have pretty good reason for thinking this is a possibility. Directly, because the Rust experts I asked for help did in fact advocate giving up modularity. Indirectly, in that I've seen something similar but much stronger in a different language. That culminated in a conversation with several of the creators of that language, in which they argued their language was great for generic programming...while revealing some pretty basic holes in their understanding of generic programming (while calling me a "Haskell weenie," among other abuse). I am very confident in my conclusion about this other language community and not interested in getting into the details of this event. I am just sharing where I'm coming from, in not immediately dismissing this idea as too absurd to consider even when I have no other hypothesis.
I acknowledge that you feel personally insulted by me having a hypothesis that requires a large group of people behave in ways I consider strange. I have evidence for that hypothesis, and have been unable to find a better one. I hope you can see how the comment I am responding to crosses an extra line and is directly personally insulting.
I would very much like to come away from this discussion no longer believing that Rust programminh is at odds with modularity. I have shared some fairly basic and detailed criticisms, the ones I still remember after a year out of the language. Perhaps your can play a role in offering solutions to them -- or admit that these are indeed problems your hadn't noticed before or had gotten used to