Rust Is Hard, Or: The Misery of Mainstream Programming
hirrolot.github.io
hirrolot.github.io
The "hard" part about Rust is you have to "unlearn" some of the basic mechanisms like scope and ownership that you bring from other languages. It doesn't allow you to build quick and easy solutions that allows you to lie to yourself (or your boss.) You have to spend more time upfront to compile and fix all warnings, but in my experience so far, fixing these at the right time (i.e. before compilation) is better than fixing them after someone writes a Github issue. I can't say it eliminates all bugs, but my trust level to my Rust code is around 10x more than my Python code for the same problem. (And I'm writing Python since 2002.)
Rust makes an argument about safety but only for a select few types of errors and the cost seem rather high.
I've only experienced waiting ~10 minutes at most (on decent dev machine) and it frustrated me to no end, but even more serious projects sometimes take hours. Maybe concepts help with this?
And the "jUsT dOnT uSe HaLf tHe LaNgUaGe" argument holds as much as a leaky bucket, because it's hard to agree on what the good parts are in a large team, unless its strictly enforced and audited somehow
yes the cargo and tools etc are great, but I can set up a full c++ build env quickly too, not as good but enough for daily coding.
GUI and Game dev spaces in Rust are very inovativne and in how they solve some problems, and I'm fairly enthusiastic about both spaces, but I'd be reluctant to recommend them for production, because they're still immature/developing compared to anything in C++
The pain comes from async. Over time, I came up to this conclusion: if someone tells me that Rust is nice and you only need to change your mind, this person doesn't write async code. Or he/she writes very-very straightforward, if not primitive async code and doesn't touch HOFs, traits, and similar stuff at all.
However, when you write networking code, you typically use async. The worst role is being an async library author (me).
Personally if I had to write async code that required anything other than the absolute minimum possible latency, I'd prefer to write Go, and I say that as someone who thinks Go's lack of generics was an absolutely terrible idea.
Your opinion is valid, but I would say, if you aren't juicing for the best performance, you can adopt easier patterns to async. There are comments on this post detailing how to go about doing that. Or yea use another language if you want.
Can I do this without having to wrap half the libraries in the ecosystem if I want to use them without worrying about async? C# has that issue: the ecosystem buys heavily into async, so it can be hard to avoid it.
IMO, when it comes to concurrent, it's a matter of picking your poison:
Threaded Rust: No overhead of a GC, but overhead of context switches and multiple stacks.
NodeJS: No overhead of context switches and multiple stacks, but the overhead of a highly optimized GC. (And I suspect that the GC can do tricks like run when the process is waiting on all tasks.)
Some real data would be very interesting.
Some numbers: https://web-frameworks-benchmark.netlify.app/result?l=rust,g...
Is it really an overhead if it happens in parallel? You have real parallelism, not only concurrency.
IE, of you're doing a web server with a thread or process for each incoming web request, you're blocking and context switching. If you have to have locks, you're also blocking and context switching.
This is why async programming models are common, they move the logic of blocking and context switching into the language and runtime, where the compiler can juggle more concurrent tasks in a single thread. It's just harder to do in Rust because, to oversimplify, things that are in stack memory in a threaded environment are now on the heap. In C#/NodeJS, this difference is transparent, but in Rust it's not.
Async code is a pain in almost any language. Certainly any language that differentiates between async code and non-async code has the async code be a pain.
But this is of course not a very safe way to write software.
I also don't find debugging Rust macros that fun, yet most likely the answer will be that the macro author didn't took enough care, and Rust is great to write gigantic DSL macros.
To be sure the fact the diagnostics aren't required does not forbid them from being provided, but it does mean you'd need to know whether you've been provided with such diagnostics and how effective they actually are. Unless the answer is "I have diagnostics and they are 100% effective" you're in the same situation.
> I also don't find debugging Rust macros that fun
Which kind? I don't find debugging the declarative macros too hard, they are after all just expanding what you wrote according to some simple rules, and you can ask the compiler to show that expansion to you.
Procedural macros present unlimited potential for exciting debugging because now you're essentially modifying the compiler at runtime. A C++ pre-processor macro can cause some nasty problems but it's not going to run a different compiler... [Technically Mara's nightly-crimes only runs the same compiler with different flags, but it could run a different one if she'd needed to do that]
Function colouring is not the only problem with async code. The difference is that concurrency in other high-level languages usually don't break down polymorphism and other language features. Also, they don't push you to deal with lifetimes, which is a serious issue in Rusty async.
Writing async code in C# is a lot easier to me than in Rust. Unfortunately, I didn't have a chance to write async in functional languages, such as Haskell or F#, because they are well-known for elegant concurrency.
https://docs.microsoft.com/en-us/dotnet/fsharp/language-refe...
Anyway, I am sorry that you have to use async.
Of note re networking: My observation is that the Rust Async ecosystem only covers TCP and higher. There are loads of Async TCP, HTTP etc libs, but nothing that can do anything lower than that! At that point, you're looking at perhaps `socket2`, and `smoltcp`; the latter, of note, also works on embedded, and goes lower than TCP, despite its name.
My best guess is that a lot of people are using Rust for TCP and HTTP level web programming, eg servers, where spawning 100s or more IO-bound processes at once makes sense; the area where Async shines. Why I'm confounded: #1: Rust excels at low-level programming; ie it's one of a select group capable of this (Along with C, C++, ADA, and zig) #2: Web application programming in Rust has a long way to go to get to the level of Django and Rails. It only has Flask analogues.
Neither of those goals are served by the existing Async-based ecosystem; it occupies a spot in between.
1. Async for trivial things is straightforward and easy
2. Rust actively discourages some async patterns to protect you from some memory misuse edge cases
It does limit your freedom in writing code that eg. relies a lot on async callbacks, but there's a reason. The first time I did a massive async project (think a rust binary maxing all cores executing the largest possible number of async fns doing various things in parallel - from fetching data online to running tensorflow) I came at it all wrong and wrote something that would have worked in node or haskell but that was a pain to compile in rust.
After days of pain I understood what rust wanted and nowadays I use the same pattern and it's fairly easy for me. Just another tool in the shed.
Ideally you want code without unused variables, implicit type casts, ... in a repository. But when you are locally testing out code you're in progress of writing, it is very unproductive if you have to care about unused variables because you're commenting out one line to see the difference, or change casts everywhere because you change the type something temporarily. It'd be nice to only do the work of cleaning up such issues in the final version of your change.
These checks are enabled by default in many environments, build tools or scripts for them etc..., it's not trivial to disable it or requires full recompile.
So I wish a language or build environment would use the concept of "development time" vs "submitting time" for different sets of warnings-as-errors.
Speaking of warnings, something I appreciate about Rust is you only see warnings for your own code and not for your dependencies so you can crank up the warnings to whatever level you want without being blocked by dependencies (minus macros). Granted, there could be times where looking at high risk warnings for dependencies could be important.
I hate it with a passion. Please respect my mental flow, if I'm trying some ideas out, it's just rude to stop me in my tracks to tell me I forgot to comment out a variable. Who cares.
I will fight anyone who thinks this is a good idea.
Personally I don't get the big deal with unused variables being errors, from either direction. I'm not sure what it accomplishes to make them errors and I'm not sure why people complain so much about having to comment them out.
So the only way to find out is to compile and then have the compiler tell you that it refuses to continue because there's an unused variable at line X.
So now you needed to do not one but 2 compiles, to do something that should have taken 1 compile, which also costs time.
But it's then also possible that due to commenting out line X, there's now yet another unused variable (or more) somewhere. Etc... (Unless the language offers a way to fake-use the variable, like (void) cast in C++. Then at least you only have one wasted compile and not recursively more)
And then if you want to enable the original line that you commented out again, you need to also remember to uncomment out those other lines.
Repeat this process many times a day when really in progress of developing something, and due to the total time it costs for this silliness it's just a bad tool, not a good tool for efficient programming in the flow.
The goal of the unused variable as error is to prevent bad submitted code, but that should not cost you time during development. Only enforce that check at the end, not during.
Zig actually does have this:
_ = my_variable;You can write perfectly fine solutions in a GC language without lying to yourself.
> my trust level to my Rust code is around 10x more than my Python code for the same problem
Python as a dynamically typed language is not the mark. Rust has a great static type system, but other GC languages offer that as well.
As a programmer that hasn’t worked with Rust, what does that mean.
A quick and easy solution is still a solution, which is what most bosses want.
I care about speed and correctness but Rust makes me also care about ownership and lifetimes even when I don't really want to care about that stuff. I've written Rust for years now and this still occasionally can really slow me down. Rust is still a very nice language so I just deal with it :) no pain no gain.
I would love a Rust-with-GC or something in that spirit that is similar to Rust but automatically figures out lifetimes as much as possible and GCs whatever it cannot figure out automatically. I've looked at languages like Nim and read about research languages like Koka so I'm optimistic something will emerge from this space I really like.
I don't think one can care about speed and correctness without caring about lifetimes and ownership. Even if you do not care about memory ownership and use garbage collection, there are plethora of other things that you take care of. For example, if you've juts sent an RPC/HTTP request, and a user closed a particular window, should you abort the request or not? What if the user have closed all windows except one? Can they actually close window X without doing something in window Y first? What should happen if you forgot to abort the request and now the callback is called, but the original caller is gone?
However, when performance is key, then Rust is by far the easiest solution to get it performant and still correct.
My response was focussed on the part of the challenges of async logic. And what OP essentially said is that Rust does not only help performance because of the ownership model (vs. garbage collection), but also because it makes it easy to come up with correct and performant solution to async-related problems.
My point was that, when ignoring the performance benefits of ownership model vs. garbage collection, a language like Haskell or Scala makes a performant and correct solution for async problems easier than Rust does - at least at the current point in time.
I hope it's more clear now.
For example, Rust's unusual 'a syntax for lifetime parameters is Ocaml's syntax for generics because lifetimes are a special class of generics.
I love Haskell but it has its own flavor of problems. I've meant to brush up a bit on OCaml; I've heard it has interesting features in its module system that Haskell does not have (maybe what you call "higher order modules").
You are not the only one, this is also my experience.
And if you think about it: This is fine. There is a reason why we are not using TLA+ or format proofs framework when we want to do quick prototyping: Safety has a cost like everything else.
It can be a run-time cost (in case of ARC or GC languages) or a mental effort / productivity cost but it is still a cost.
Swift could be a lovely general purpose language. I like the balance swift strikes between ease of use, expressiveness and performance. Like, swift has an equivalent of Option - but there’s syntax sugar for it. Rust has String / &str / Etc. To get a SSO string you need to pull in an external crate. Swift just has a built in, good, general purpose SSO string as part of the language.
But adding random half baked features to swift seems to be on the promotion path at Apple. Nothing is well documented. The language doesn’t feel stable and there isn’t much of a broad community like there is with rust, javascript and python. It’s a pity!
My only real issue with Go is that I feel the language part itself is simplistic and doesn't have the kind of type/trait/typeclass/similarthing like Rust or Haskell has. In Haskell I especially like doing DSLs using monads, e.g. I did a integer-problem-to-SAT-problem DSL and another DSL for NetHack playing AI. In Go making these DSLs is more pain. Although I think that would be true for Rust as well, just not as much.
On any team or in a community your most likely to have a range of skill levels across members. A language can be many things, a powerful tool or a strong barrier.
When I first heard about Rust I thought we were on the verge of getting some real advancements across a much larger community of developers and scope of projects. Instead I think we're headed further down a path of at least 3 major groups of PL (scripting, memory-managed, precise semantics). Who knows, maybe it's for the best. If so we better get busy improving the inter-op.
The best summary I can give of Rust is that it was designed by people who learned from the mistakes of the programming languages that came before it.
The best summary I can give of Go is to quote Rob Pike's response [0] to a request for syntax highlighting in the Go playground:
> Syntax highlighting is juvenile. When I was a child, I was taught arithmetic using colored rods (http://en.wikipedia.org/wiki/Cuisenaire_rods). I grew up and today I use monochromatic numerals.
[0] https://groups.google.com/g/golang-nuts/c/hJHCAaiL0so/m/kG3B...
Go has nil where Rust has Option.
Go has weird not-quite-tuple returns & if err != nil where Rust has Result.
Go has no real enum concept, where rust has its powerful enums and matching constructs.
Go has generics, but only after a decade of pressure from users (and even then, they are much much less useful than Rust's type system).
I like Go and I feel very productive in it, but its commitment to simplicity is dogmatic in many ways and it very much is missing milestone advancements in PLs from the past several decades. It could easily have been made in the 90s.
Throw in great compile to JS capabilities with small bundle sizes and the ability to map Typescript typings into the language so you can seamlessly consume well typed libraries, and man. Killer language, right there.
Viewed through this lens, the resistance on the part of the creators to changes that compromise these values even a little bit makes sense. Generics take time to get used to and any code base that makes extensive use of them will take longer to get up to speed in, even if it enables you to move faster later on.
That talk has really shaped how I look at Go. I think it solves problems that Google has (really large projects built by teams that have a ton of turnover) really well. But as with a lot of things that emerge from Google, it’s a solution to a problem that not too many other companies face. The ones that do will get an awesome tool that’s proven to work. But the ones for whom it’s 90% of what they need are going to get a lot of pushback getting that last 10% accepted because it already does almost exactly what Google needs it to do and any departure from that will be, in their minds, counterproductive.
It just tantalizes me because of how close it is to my ideal general-purpose programming language. There is a large middle ground between the minutes-long compile times of Rust and the seconds-long compile times of Go.
* Option? I don't mind if err != nil, but sure, it would simplify some things.
* Result and real tuples? awesome.
* real enums? sign me up.
If that was all added to go I wouldn't mind at all. But what does any of that have to do with impl<'a, H, Fut, Tail> Execute<'a> for Dispatcher<H, Tail>
where
H: Fn(&'a Update) -> Fut + Send + Sync + 'a,
Fut: Future<Output = ()> + Send + 'a,
Tail: Execute<'a> + Send + Sync + 'a,
{> "Go is a good alternative to Rust because it is easy to write performant, concurrent code"
> "Go is not very comparable to Rust. I won't describe why, here's a quote by Rob Pike and I'll strongly imply it's because its creators deliberately avoided complexity, even where useful."
> "You did not explicitly criticize anything about Go, therefore it must not have flaws"
Then I came in and described exactly where I feel Go ignores the state of the art in PLs.
> But what does any of that have to do with (Rust-shaped vomit)
Go doesn't need all those type system gymnastics because it does not have the problem of the borrow checker to deal with and doesn't promise to avoid segfaults for you.
- Go has nil where Rust has Option.
- Go has weird not-quite-tuple returns & if err != nil where Rust has Result.
- Go has no real enum concept, where rust has its powerful enums and matching constructs.
And the point on generics coming late is unfair. All programming languages got major features introduced late (for example async/await in Rust).
But I admit I'd love sum types, a less verbose error handling (but to be honest I'm not sure exactly how), and that I missed generics.
The more I think about it, what I really want is something with the ML feeling of Rust, but in the space that Go occupies (good performance GC languages). Go frustrates me because it nails the runtime and tooling side of things but falls far short of it in the other ways I mentioned.
The usual objection is that it requires a .NET runtime, whereas Go produces a single self-contained executable. But .NET can produce self-contained executables these days.
That is why I find Rust and Go to be dissimilar. Rust is a language full of good ideas, almost all of which didn't originate in Rust. Go feels like it was designed with a very specific brief - "we want C but with GC and easy concurrency" - but whose designers otherwise had an NIH syndrome-like aversion to good ideas and common sense.
I am not saying that Go and Rust are similar or not. I don't really agree with GP's comment where Go should be in the running for a Rust-with-GC/slightly-higher-level-Rust, but the frankly dismissive Rust Good therefore not similar to Go Bad is not justified. Although the conclusion regarding similarity is probably correct.
Also, this is a little off topic, but I have seen multiple people say that Go 'was designed with a very specific brief - "we want C but with GC and easy concurrency"' and then go on to complain about the lack of things like generics, destructuring match syntax, functional concepts, etc. But, all of those discussion, including your comment, start out with what seems to be an acknowledgement that Go's design had a very specific target. I agree that Go is basically the fulfillment of the brief you gave, i.e. C with GC and easy concurrency. So, when the target is C with two unique things, why does everyone then seem confused that Go doesn't have all of these extra 'good ideas and common sense'.
I'm not trying to turn this into a thread on Go's merits or lack thereof. I just don't understand why so many people seem to think that Go somehow didn't do exactly what it set out to do. You don't have to think what they decided to design was a worthwhile language, but to expect a lion to be a shark, or an apple be a steak, is just illogical.
The reason I like and recommend Rust is the number of decisions it gets right which are completely orthogonal to the borrow checker. It's clear that a lot of thought was put into the unexciting parts of the language. That's why I like using it even though I don't particularly need the borrow checker and I'd be happy with GC. Some examples off the top of my head:
- Expression-oriented nature makes code easy to write and nice to read
- Compilation errors are as clear and helpful as possible
- Comprehensive yet skimmable API documentation
In short, Rust is a nice language outside of the borrow checker.A lot of the things which strike me as nice about Rust can't be said about Go. For that reason I think Go is a surprising suggestion for someone who likes Rust but doesn't care about ownership and lifetimes.
There are a number of minor but valid frustrations I encounter when writing Go which are completely orthogonal to the brief of "C but with GC and easy concurrency". I'd understand if these problems were a result of the language's goals but often they seem to exist for no particular reason. In that regard I think Go is quite dissimilar to Rust.
I hope that explains my viewpoint more clearly :)
There is an ongoing work for borrow checker for D language, and pardon the pun but I believe D is borrowing just the right amount features from Cyclon and Rust for safer compiled software without overly complex programming syntax.
[1]Origins of the D programming language:
That said, they all don't give you what rust gives you and have their own troubles.
The truth is, either you care about life times and ownership, or you really don't care much about speed and correctness. Not saying that targeted at you as a person, but the royal "you". Even in c/c++ I have to think about that stuff, there's just no tools for it .
Fwiw you can import crates to have a gc in rust. To me it defeats the purpose. Wishing you the best on your search
* The compiled programs run pretty fast.
* Compiling is pretty fast too (compared to Rust or Haskell).
* It's easy for me to bind to whatever C library I have even if nobody made nice Nim packages for it. This one was important for the project I was working on.
* There is a GC, but if you choose the proper GC it will be based on reference counting (or if you are compiling to JavaScript, it'll use JavaScript's GC). It means I don't have to care about cleaning up resources. My memory may be wrong but I think Nim also tries to remove reference counting checks when it can. I haven't ever checked in the compiled code does it do a good job at this.
* The language rarely complains that something in my code is wrong; there's no borrow checker complaining. Even with little experience I was able to write some quite complicated code. It's like Nim wants to do its best to compile and run my crappy code.
* It's easy to read. Maybe because it looks so much like Python and I have lots of Python experience. Nim does not want to complain about your code unless it has to. Despite being statically typed, you don't have to write type definitions that much.
There are things I don't like as much:
* The story for running threads in Nim is not great. You can run OS-level threads but it's a bit janky (you'll have to now care what data can be shared). It wouldn't stop me from writing multi-threaded software though if I really needed threads.
* The documentation could be a bit better. I think Nim project should take this giant page: https://nim-lang.org/docs/manual.html and reorganize it and make sure the language is easy to read and find stuff. I often have trouble finding documentation on some language feature. I think the project is acknowledging this right in the first paragraph "This document is a draft! Several of Nim's features may need more precise wording. This manual is constantly evolving into a proper specification."
This one is harder to pinpoint into specific examples but the language feels a bit immature. I feel like there's bunch of half-baked features that were thrown in on a whim idea. And obviously the package ecosystem isn't as wide as in more established programming languages. I looked it up and Nim has apparently existed since 2008; I'm sure it used to be way more immature ;)
Despite the negatives, I think Nim is an amazing tinkerer's language. I can very quickly write programs that will be speedy and easy to read. I'm not sure I'd start a very complicated large project in Nim though. I am bullish that Nim will mature and its userbase will grow, fixing some of the warts.
In a few months I plan to take a long vacation and work on a video game with my friend. There is a high chance that I'm going to use Nim for this project; to make something that runs both in browser and also natively.
I've been following the nature of code and making a little 2d platformer at the same time using nim + raylib. Compiling to wasm I found to be even simpler than when doing the same thing with C++. It's really nice being able to compile to native or wasm with a single compiler flag.
Nim is indeed going in the right direction with ORC/ARC
Downside is that it's not convenient if you just want one standard library and runtime because it targets all of them. You have to justify the trade-offs involved in that, but it's a good secret weapon.
I think F# might fit this role.
Oh and I almost forget to point out that it can just use basically one of the largest ecosystems, and can also compile to js or native (for the latter there is scala native as well as graal)
But since it is interoperable with Java and that exposes low-level primitives of concurrency, it can’t really be made guaranteed data-race free.
I think the best answer is Scala. It has an awesome community/libraries/ecosystem. See this for example https://news.ycombinator.com/item?id=31601040#31604573
There are other great answers, like F#, Haskell or OCaml, but these are not as mainstream and that comes with its challenges.
With Scala you get and awesome IDE, surprisingly wide variety of libraries and the whole Java world as a backup.
It puts the reference counting in the type system. Like the default string type and arrays (=vectors) are reference counted. And for speed you could use manual memory management
Any string literal is like an arc<string>. E.g. in Delphi you can now write string concatenation like:
var a = 'bcd';
var b = 'xyz';
var c = a + b;
which corresponds to Rust like let a = Arc::new(String::from("bcd"));
let b = Arc::new(String::from("xyz"));
let c = format!("{}{}", a,b)As for nowadays, I'd say there are three things that help people to bounce:
1. The big name (Delphi) is proprietary and Free Pascal's documentation feels like it hasn't caught up with various lessons that were learned about how to document a toolchain and standard library since the early 2000s.
(And that's before you discover that, apparently, the API documentation is manual enough that the official stance on the Free Vision TUI component's documentation is "go find a copy of the Turbo Vision book from the Borland Pascal manuals", and that the Free Pascal Wiki either neglected to mention one or two classes when they were saying what is yet to be reimplemented or neglected to mention additional restrictions present in the DPMI port.)
2. Similar to with Ada, the ecosystem people are normalized into as they learn programming is leaning more and more strongly on the C-descended syntaxes, making Wirth-style syntaxes feel more alien.
I have to admit, compared to something like Rust, there's a certain off-puttingness to having so much verbosity and ceremony in the structure of the block constructs. To exaggerate a bit to get the point across, it's sort of like Pascal expects me to remember all the layouts and lines for what an empty tax form looks like well enough to draw it from memory before filling it out... and that sense of discomfort doesn't go away if I delegate it to my code snippets tool.
It lends an ambient sense that there's an iceberg of structure I don't understand and Pascal expects me to remember how many separate peaks it should have poking out of the water and where they should be, without me understanding the topography of the submerged portion.
(And yet I still recommend it as the Java/C# equivalent for DOS retro-hobby computing, since it's safer than C and has a much richer library of bundled functionality and comparable performance.)
3. The Free Pascal APIs are aggressively 90s and don't have enough examples to break you from your 2010s-and-beyond expectations. (I was almost tearing my hair out over how to get status updates from their zip extraction code before I realized that I was fixated on the Qt/GTK/DOM/etc. idea of using some kind of signal.connect(my_callback) function rather than using subclassing to set an event handler.)
Just, in general, it's an experience that's alien to current language trends and growing more so, the documentation available before you're committed enough to pay for it is wanting, and if you're willing to pay hobbyist prices, you need to do your own research on what exists to pay for.
It's an embeddable scripting language with the goal of being a Rust-like language that supports hot reloading of functions AND data. To achieve the latter, it uses GC'ed memory such that memory can easily be mapped when the memory's type changes.
It's still in early development but maybe one day will serve your needs :)
I have spent most of my career in C, C++, Objective-C and have recently tried Zig, which I enjoyed, possibly because of it's brevity, and I read through the Jakt programming language docs, which was much more familiar to me but I think if you only used Rust with ARC it would be a simple language to adopt so that's not really a fair comparison.
I think what I really need is a project rather than learning resources, with time being our greatest enemy, I need motivation to get over the hump so to speak.
(Like I said, I might not be smart enough, or I might just be profoundly lazy).
Thanks for your reply.
Moreover, a lot of the libraries in rust needs you to understand the type system thoroughly sometimes to use them, and some of them use the typesystem to enforce some specific behavior (without telling you :-)). For example, the logger mechanism in rust makes it very hard to dynamically change the logger implementation at run time but it doesn't explain why (in the end, I think it has to do with thread safety, but I'm not sure).
It took me about the equivalent of 30 full days to get there. Although I'm an experienced programmer (C++,assembly,Java,python,R), the last 15 years have been python 99%. Had I continued with C++ all those years, I'm sure I'd understand rust much better.
So, just keep going and really listen to the borrow checker. The thing is really smart and sometimes, after a lot of pain, you understand that your own mental model was wrong :-)
Mainly because logger is a static value, if you really want to change your logger at runtime, then you have to pay a little bit for synchronization (i.e. locking with mutex), other than that, it's not that hard.
Did you think that such a group of people, who place their love for the language above practical things like readability, would come up with a new language that cared about the user-experience[1]?
As we see all the time WRT to programming languages, readability is more important than the more abstract stuff.
> I hear all the arguments about CVEs and wonder if rust will reduce the number of these problems simply by disabling the programmers that create them.
That sounds like a different way of saying "the global number of CVEs will be reduced by reducing the number of applications written". After all, if you "disable" the programmers who write systems programs, they aren't going to get magically replaced by programmers who don't write systems programs.
You're just gonna have fewer programmers.
[1] A language is an interface between a user and their problem. If this is not highest priority of a language design, the language may never take off in any meaningful sense. See Monads.
Elaborate?
I got that, but the point is still very much relevant. If places switched to Rust from C++ (or Go, or C), they'd write less software because there will never be as many people who understand Rust as there are people who understand Go.
If it was at all easy to pick up there'd be a fewer stories from people who tried learning it and abandoned the effort.
I cannot think of any person who reported abandoning Go because it was too hard to learn.
Readability is an incredibly subjective thing. For example, APL might look like an absolute mess to most people but to the people who know it, it's incredibly easy to read. All "readability" means in this context is that it's familiar to you.
In my opinion, the only truly hard to read languages are languages that require you to keep large amounts of context in memory while reading a specific part of the codebase, be it due to lack of support for abstractions for proper encapsulation, messy overloading, insane inheritance hierarchies, or something else.
EDIT: Readability also requires you have a codebase that's well written, and even that's subjective.
But if we use that as a bar ("you have to know it"), then all languages are equally readable.
Pretty much all the popular languages converge on similar syntax an/or grammar.
You can say that it is because that particular set of syntax is the first, or you can say that languages that didn't have similar syntax was abandoned by developers.
There is an equal evidence/lack of evidence for either claim.
There is a balance between verbosity and conciseness that makes code more or less readable to different people.
If you have few experts who are maintaining a codebase over a longer time, then verbosity gets in the way. If you have a wider skill distribution and fast turnover, then you want verbosity.
Additionally I think there are more objective features. Like ambiguity, simplicity, formatting, naming, visual hierarchy...
How would you compare it to Typescript? Having become a recent convert, I feel the same way going back to Javascript now
You often end up with obscure issues that a good type system should prevent. Maybe because the third party typings for a package are faulty or incomplete, maybe because the compiler just gave up on a complex expression and fell back to any, or because the developer just gave up and used `as any`.
Typescript is also really constrained by keeping JS compatibility. A prime example is the lack of real sumtypes. Instead you have to use objects with a string tag and do if/switch on that tag to disambiguate the concrete type, which is incredibly awkward when compared to proper compiler sum type support.
I'd much rather use Typescript than Javascript, but it's far from perfect.
I don't know why but haven't seen many posts on how the combination of Rust's trait system, implicit conditional returns and strict enforcement of im/mutability of state allows one to handle significantly higher amount of states the application can take in the same amount of code. Other languages usually have you writing thousands of lines of verbose error handling and state validation code and traits allow for a superior way to compose behavior over standard interfaces / abstract classes.
Is your software more performant and does this translate to additional customers or reduced costs? Is your software more stable and does this result in reduced support costs or happier customers?
Testing effort is likewise not a decisive matter. In fact it's quite likely that what one wins time-wise on the testing side is lost on the development side.
That said, I think Go's simplicity has a lot of advantages over Rust for a great many use-cases. Imo Rust almost feels like a prototype for a great language which will finally get ownership-based memory management right. The goals of the language are admirable, and the tooling is great, but it's such a complex language once you start getting into topics like lifetimes and async.
I think it shines for certain use-cases where the tradeoffs it makes genuinely add value, but in a lot of ways I think it's more of a niche language for people who love esoteric programming topics rather than something with the potential to go truly main-stream.
What are the differences between an ADT (plus pattern matching i’d reckon?) in Rust/Swift vs the equiv in Go (tagged interfaces + switch statement)?
One has exhaustive matching at compile time, the other has a default clause (non exhaustive matching), although there’s an important nub here with respect to developer experience; it would be idiomatic in Go to use static analysis tooling (e.g. Rob Pike is on record saying that various checks - inc this one - don’t belong in the compiler and should live in go vet). I’ve been playing with Go in a side project and using golint-ci which invokes https://github.com/nishanths/exhaustive - net result, in both go and rust, i get a red line of text annotated at the switch in vscode if i miss a case.
Taking a step back, there isn’t a problem you can solve with one that you can’t solve with the other, or is there?
To take a step further back, why incomplete?
Also with Rust for instance you have more robust pattern matching. So you don't have to match only on type, you can match on complex criteria (i.e. foo is a Circle, with foo.radius < 10).
I find this type of programming very expressive and easy to reason about.
Yeah me neither, i’ve just been dabbling for fun recently. I’ve been dabbling with rust for longer
>> the Go way seems to have more boilerplate
…
switch t := s.(type) {
case Circle:
if t.radius < 10 {
fmt.Println("the rust syntax for this is sweeter")
}
…
https://go.dev/play/p/s4TTZeo7GseDefinitely less boilerplate in the rust version, but i don’t think i go along with it being incomplete or less clear.
Go: https://go.dev/play/p/s4TTZeo7Gse
Rust: https://play.rust-lang.org/?version=stable&mode=debug&editio...
For me, i reckon the Go type machinery is more verbose in this case, e.g. the isShape method to tag Circle as a shape - that just feels awkward to me.
The cyclomatic complexity is identical for both though.
Also I prefer with Rust-style sum types that the declaration happens in one place. In the Go version your Shape types could be scattered around all over the place, which makes it harder to parse and understand if you're reading unfamiliar code.
edit: e.g. https://go.dev/play/p/wDO5J8CElSC
enum Shape {
None,
Circle(radius: usize),
}
In your version we've lost the Shape concept, to make it more explicit, what's the idiomatic go version of: enum Shape {
Circle(usize),
Rectangle{length: usize, breadth: usize},
}
https://play.rust-lang.org/?version=stable&mode=debug&editio...In Go code will typically "offer" concrete types, and "accept" abstract interfaces. Abstractions over concrete types are expressed by consumers who want to operate on those types, rather than producers who provide implementations of them. So your Shape would probably end up an interface defined by whatever consuming code wanted to treat Circles and Rectangles equivalently.
E.g. if that means implementing low level things like cryptography APIs, or being able to define the exact memory layout of objects in memory or access raw pointers or write ASM inline then obviously that disqualifies ruby, python and js but it would mean Go qualifies.
The article is about a “messenger bot” for example and nobody’s complaining that it’s wrong to write such a thing in Rust.
The reality is that Rust is competing with Java and Go and Python for many things and its poor ergonomics are hobbling it.
I realize that Rust's compiler might catch more concurrency problems, but your comment is written as if you feel "blind" even writing single threaded code.
It's very common in Rust to do a big refactor or write a program from scratch and have it work on the first or second try, which may release a good amount of dopamine for some.[0]
Not saying the language is perfect, but it can feel pretty good, to the point of being addictive, while also freeing to know you can think less about the variants and let the compiler do it for you, and the reliability of the created program is usually pretty amazing. It can also be annoying, specially when you're learning it and try doing something in a way that's just not viable in an easy way (e.g. manually creating a Linked List).
[0] https://old.reddit.com/r/rust/comments/uixup6/just_wanted_to...
I really enjoyed Haskell when I tried it years ago (2004) and I definitely agree that Rust is much more apropos to the mainstream.
Love it!
people_who_want_to_keep_using_the_language / people_who_have_used_it_in_the_past_year
Since Rust is so early in its adoption and there are few jobs, many of the responders are hobbyists who are into Rust, like it and want to keep using; not necessarily people who use it for work. And since you only take into account the people who have used it in the past year, you ignore all the people who have tried Rust before that and didn't like it.As an extreme example, let's say I created a language in 2020, the entire software community tried it and they absolutely hated it and unanimously rejected it as the worst language ever but it still has 2 users (my mom and I) who want to keep using it - that languages would score 100% Most Loved on stackoverflow's survey even though it was hated by the overwhelming majority of people who tried it.
With this formulation you create the strange effect that if people hate a language enough to stop using it, the language's most loved metric goes up after a year.
For most people who program for a living, the hype over Rust means little. They need to use what is in the industry right now. Often, that is a tried and true language that is relatively easy to learn and use.
Having a complex programming language which limits you and controls how you use it does not sound appealing to those who need a feature done, fast.
This is so interesting to me. I'm a Rust programmer by trade (as in - I'm not a hobbyist, I actually write Rust for work). We've found that, while the feature work is a bit slower in Rust than in other languages the company used to use (mostly Python), they tend to require a lot less maintenance down the line (less bugs, easier refactoring), and so it ends up canceling out a bit.
I also never find that Rust limits or controls me in my day to day work. There are some things you cannot do easily, sure, but those things tend to not matter in application code. Linked lists are hard, graphs are hard, but when writing an app, I just use an existing dependency like petgraph or std::collections::LinkedList instead of reimplementing those datastructures. And if you need to write those things, it's not like it's impossible, you just need to drop down to unsafe rust, and ideally figure out some safe way to expose that API.
Really, the biggest disadvantage of Rust is that its learning curve makes onboarding newcomers much harder if they don't have experience in the language. For Python/Go/JS/Ruby you don't really have this problem, even if a potential recruit doesn't have experience in the language, they will probably be able to pick it up as they go without too much trouble. But in Rust, it can take a fairly significant amount of time to get up to speed and stop fighting the compiler, in the order of several months.
A lot of programs written in Rust would almost certainly be better off being written in Kotlin. The one in the article is a good example. Why are they writing a messenger bot in Rust? That doesn't seem like the sort of use case Rust was targeted at. It's also a modern language with a lightweight syntax and pretty good static typing, great refactoring tools etc, but it's way more user friendly. There's no borrow checker because it's GCd, compile times are much faster (at least for the JVM version) etc.
You can also use Kotlin/Native or GraalVM native-image to produce standalone executables that don't need a full JVM, if that's a requirement for your use cases. The native images are astounding. They can start faster than programs written in C and their memory usage is also way less than a typical JVM app. Downside is of course compilation time but you can avoid that by just developing on the normal JVM and then AOT compiling at the end when it's time to release.
AFAICT Rust is a general purpose programming language. People can use it how they want. How do you learn a recent programming language? "hey boss I'm going to write production code in a new language that I haven't learned. Lol YOLO"
Now if we were talking about some other "new languages" not naming names, you can memory leak kilobytes writing hello world... Yikes.
Dynamic languages and GC'd languages cover most of what our employers are paying us for: web apps, backend services. It is ironic to build super safe software, then deploying them on kubernetes, written in a GC'd language prone to nil dereference errors.
Rust has its place, but it's a very small place. These days computers are so fast there's companies worth billions built on Ruby and Python, if you go native with a GC language you'll cover 98% of your needs.
Rust IMHO has gone too far towards safety at the expense of developer ergonomics and pragmatism. We're so expensive that adopting a subpar language that is easier to grok is often a savvier business decision.
Indeed. I'm a Rust dev somewhere it actually matters (video dev - we have to process 60 frames a second, for hours/days on end, with completely predictable performance, without crashing once) and I'd never pick it if I just wanted to build a webapp where nothing really matters and if the whole thing falls over we can just put it in a retry loop.
There's plenty of code out there that /cannot/ be written in Ruby/Python/Go/C#. Rust solves some really hard problems for people working on that type of code. But it's not what most people on HN are working on.
I think the problem is that a lot of web developers do not really understand that this type of code exists, and that languages which do not target web development can be exciting.
But without even going as far as frontend web applications, stuff like backend services, system services, GUI applications, CLI applications, etc., don't actually need all that much memory safety and control over allocations.
My understanding is that Rust was designed to be a safe(er) language to write low-level system code than C/C++, not to compete with Python/Ruby/Java for web development applications.
I'm complaining about everybody on programming forums, including HN, seeing Rust as the replacement for most, if not all languages. My opinion is that Rust's ideal problem space is much narrower than people think.
>It is ironic to build super safe software, then deploying them on kubernetes, written in a GC'd language prone to nil dereference errors.
Nil dereference errors don't demonstrate an absence of memory safety. If the runtime checks for a nil reference before dereferencing and then raises an exception, that's perfectly safe.
I don't think the null handling in rust is about memory safety so much as it is their choice in error handling. If you come across a null pointer dereference and your code doesn't handle it, that's a hidden bug. Triggering an exception and handling it is a valid approach, but there are trade-offs. If no code handles the nil exception for example then it's no better than crashing as you would in C. Wrapping things up in try catch can lead to missing recoverable errors or unexpected states. The choice to disallow null is one to avoid common bugs, not one for memory safety, as far as I understand it.
> One common example is modifying a vector element by reference. If you grab a reference to an item, push something else to the vector, then try to access through that reference, the vector may have re allocated during the push and you're either no longer looking at valid memory because it's been freed, or you're looking at a stale copy of the element.
There aren't that many languages which both use GC and allow you to take references to elements of a vector. Go would be one example. I can assure you that you cannot access undefined memory regions by doing this in Go. (What would happen is that the original allocation backing the vector would be retained in addition to the new allocation.)
>If no code handles the nil exception for example then it's no better than crashing as you would in C
C programs are not guaranteed to crash when a pointer has an invalid value. Dereferencing a NULL pointer will reliably cause a crash if you're running the code on top of a modern general purpose operating system with memory protection, but invalid pointers (which don't necessarily have to be NULL) have the potential to access arbitrary memory regions without raising any kind of error.
Current implementation of slices and interfaces in Go is not memory safe in presence of data races:
https://blog.stalkr.net/2022/01/universal-go-exploit-using-d...
Nope, unsafe isn't a way to do dependent typing in Rust.
This is something that escapes a lot of people nowadays.
Everyone uses a browser and they are inherently unsafe.
Meaning that Firefox's gains with Rust will eventually be lost given its market adoption is slowing reaching zero.
https://docs.google.com/document/d/e/2PACX-1vRZr-HJcYmf2Y76D...
https://www.chromium.org/Home/chromium-security/memory-safet...
But yes you are right, anytime you have two or more accesses to a data structure you do have to think a little harder about it. Usually you just want to use a reference to that object, sometimes it makes sense to clone it, sometimes you want to pass it by value and return it by value. In the case of doubly linked lists you probably want to drop into unsafe code.
I get it though, GC languages are more zen in that regard.
Libraries are typically optimized for generic use, so are often more complex, slow, and difficult to extend with new features (both coding wise or the features might not be suitable / have trade offs for other people).
Yes making data structures is important, and the reality of that is, there are people in the ecosystem creating fancy ones which can be leveraged for your use case.
If that doesn't exist then yea you have to write them. Sometimes that's difficult(you need to use unsafe), sometimes it's easyish and you can use all safe(I did this for a graph type). It can be done(lots of people are doing it), but I think what it shows is, data structures can be difficult to do correctly.
In a worst case scenario you could write it in asm inside rust if you really need a low level control, or write it in C/unsafe rust and ffi or just write it in.
Basically worst case scenario in rust is writing something closer to C.
That isn't to say a narrow ecosystem is necessarily bad, it can often be good if a particular language is used for a specific domain as all the libraries and data structures will likely be a better fit for your problems. Going outside of those constraints can often be very punishing in those languages though.
My advice would be not to judge any language on how easy it is to implement a doubly linked list. 99% of code you write will likely not be that, and that particular data structure exposes a lot of choices and trade offs at a time when youd be better served learning other parts of the language.
IMHO Rust's lifetime analysis should consider borrows only at the point of dereference, not just whenever you encounter an & symbol. They already do that with raw pointers - you can have as many pointers as you want but you only need to mark `unsafe` if you actually dereference one. So at least someone on the team knows how to make that work functionally.
I'm sure you know this already, but for the people reading this: first of all "unsafe" is a very rare, and it's only "less safe" more than fully "unsafe"; ie it's still safer than C++.
And the second thing is that, often you don't have to drop into unsafe Rust, it's possible to achieve most stuff without it, but might incur a small performance cost (idk if some of those can be optimised away by compilers or not).
Python to Rust is a pretty radical swing from one approach to language design to another. I'd expect you could solve most of the maintenance problems with Python by switching to something statically typed but with garbage collection (e.g., C#, Java, or go), without incurring the costs of moving to Rust.
When a lot of what you're doing is interacting with low-level platform APIs, you end up having a lot of those unmanaged objects. After a certain point, the upsides of using a GC kind of disappear because you still have a lot of places you have to worry about those objects.
Of course, this can be worked around by providing managed wrappers around those unmanaged object, but at a certain point, it becomes easier to just drop down to an unmanaged but safe language (like Rust) that model the unmanaged resources more accurately. In my experience, it's somewhat easier to provide Safe Rust wrappers around your average Windows API than it is to provide a C# Managed Wrapper around the same.
---
And yes, go is special because it has very smol stacks, so doing any kind of FFI on it is a bad idea.
I'm genuinely curious as to what would make FFI in Rust easier than in C#, assuming an apples-to-apples comparison (i.e. use of "unsafe" and associated features in both cases"). Most complexity in P/Invoke shows up when you try to use it to automatically map data to managed data types; but in modern C#, you might as well just use raw pointers, stackalloc, spans etc.
...so requiring `use of "unsafe"` is often akin to forbidding Cargo/pip/NPM/etc. and then calling the language un-productive or faulting a procedural/imperative language for performing badly when you code as if you're writing Haskell.
https://www.hobofan.com/rust-interop/
https://areweextendingyet.github.io/
Using the ecosystem rather than reinventing it is a core tenet of Rust's value proposition.
Rust really does not limit you. Anything possible in C or C++ can be done in Rust if you're willing to use unsafe code.
Most of that difference is in things that happen at compile time, so this is not a question of Turing completeness.
Do you have any examples?
The heart of C++ is destructors: a bit of code that you can write and the compiler will ensure runs when code goes out of scope/is deleted. You can do this in C by remembering to manually call the right code when doing clean up, but it is easy to forget and thus error pron. (I think Rust has this too?)
C++ gives you the ability to do a virtual base class interface, which - as most people know - just means it writes a vtable behind the scene for you. Sometimes people write a vtable by hand in C: it is just a struct of function pointers, but the syntax to do it in C++ is a lot nicer. If you need an interface of some sort the win goes to C++ because the syntax is a lot nicer. (I'm not sure what Rust does about interfaces, I think it has something)
C++ gives you control over copying structs. In C structs are only copied member wise, if the struct has a pointer you need to keep track of both copies so you don't free it early. In C++ you write a copy function that will make a copy of the pointer. You can do this in C by remembering to call the right function when copying a struct, but the default is the wrong thing. (I'm not sure what rust has here, but at the very least the borrow checker will stop you from making a mistake)
C++ gives you move objects - a way to express that a struct is going out of scope, but only after a different one is taking over the contents. This is a variation of the previous, except that you know the original doesn't need valid data anymore and so you just copy pointers and null them in the original. C doesn't have this concept, you can get around it with use of pointers and manual copying of structs in the right places, but the code is ugly. (again, I'm not sure what rust does, if nothing else the borrow checker should allow the compiler to make some optimizations on this lines)
There are a lot more areas where C++ gives you syntax to write correct code that C does not. Rust intentionally doesn't have some of them (class inheritance has been abused often, but I still find it useful enough in a few cases that I think rust is wrong for throwing it out), and in other cases has come up with a better syntax. Overall I don't know enough about Rust to judge it, but I'll take C over C++ anyday.
Note, the above is about the advantages of C++ over C. C++ has a lot of warts that are out of scope for that discussion. I am not claiming C++ is perfect. If you are starting a new project you should seriously consider your language options - including some not mentioned here)
I'm somewhat confused about why it was downvoted - there is nothing there controversial at all.
[1] In my own toy language, I'm kinda leaning towards objects with destructors. Definitely without the footguns in C++, but destructors nonetheless.
Did you mean the opposite?
Rust doesn't have class inheritance, no, but often combining traits and delegating to a "parent" field works. Although sometimes that can be considerably more cumbersome, yeah.
With the C++ approach I've found myself overthinking the problem many times. But with the insight that pointers are just data, and memory management can be independent, it turns out that programming with raw pointers need not be hard. There is no problem passing a pointer without "move semantics" shenanigans.
I'm not saying that it has to be this way, but there is a reason why something like unique_ptr (that can be quite tedious to use in my experience) has become so popular in many projects - IMO it is overused, often leading to more typing work instead of less.
If one is sufficiently used to this way of modelling, it can come as a surprise that memory management needn't be so hard, and doesn't require smart pointers or anything, if the program design (global flow of data etc.) is sufficiently clear.
There is no sense, in C++, in which a naked pointer implies or suggests ownership. It is the opposite: the continued validity of a pointer value depends on ownership maintained elsewhere. Nowadays libraries keep track of ownership. Most commonly the library delegates that responsibility to Standard std::unique_ptr, for familiarity. Not doing that indicates something different in play.
(It is, in fact, UB even in C to copy the value of a pointer to an object that does not exist anymore. This is the case regardless whether it had pointed to or into a stack or a heap object.)
In C, there is no way to express library ownership of a pointer. So, in C there is always potential for confusion about ownership.
I give you one advice: Think twice before writing this way again. You are not putting enough effort into understanding what I'm trying to say, and coming across as an arrogant prick.
https://news.ycombinator.com/item?id=31494099
Admittedly, I didn't make my point sufficiently clear, but this isn't even about being right or wrong. (And, once more, I understand what you say. Stop belittling me.)
This sounds as insane to me as a carpenter who works with power tools saying. "Having safety measures which limits you and controls how you you have to do certain things does not sound appealing to those who need a house build, fast."
It's not good, and when a bridge falls over the results are a bit more... dramatic and undeniable - but it's not really my impression that other branches of engineering are particularly immune from corner-cutting and deliberate under-estimation.
Edit: though I'll say, just having someone "at random" say they expect better in their industry is an anecdotal evidence that it is at least a bit better. Also, you get to work outside. Truly, grass is greener on the other side of the road.
In case you haven't seen it, there's been some interesting progress on that front:
Believe it or not industry has and is continuing to adopt rust. Microsoft, AWS, government agencys in Europe, the Linux kernel itself (the only other language allowed there is C - think about it). It's really not all hype, people like it because it solves a class of problems from never happening while still performing very well.
I invite you to join the community and learn it. It's a lot of fun once you get proficient with it.
This is the ideal, but not always the reality. The Rust borrow checker isn't perfect, so sometimes it rejects code that should work perfectly fine. Hopefully as the language matures (Polonius, GATs, HRTBs…) these cases will become rarer.
Come to think of it, an even simpler example is a mutable iterator for a typical data structure. Such iterators usually require 'unsafe' code to implement, even though they are perfectly safe to use. https://stackoverflow.com/questions/63437935/in-rust-how-do-...
A lot of people don't seem to realize how easy it is to get UB in other languages. The C spec for example claimed not having a trailing newline at the end of a file was UB...
I also don't think this was an example of code that should compile but doesn't. Rusts' ownership rules make this task hard, but there are lots of people using doubly linked lists in rust... Yes they used unsafe code to do it, and yes it does compile .
The question was show us a case where the borrow checker was wrong.
I'm fully aware of this. That's why I put 'unsafe' in quotes.
However, it's surely fair to point out that you can't implement doubly linked lists or mutable iterators in Rust without giving up Rust's usual guarantee of memory safety. If this guarantee is not actually that big of a deal, as you appear to be suggesting, then why all the hype about it from Rust advocates?
>The question was show us a case where the borrow checker was wrong.
No, that was not the question. OP just asked for 'rejected code that should work perfectly fine'. There are of course loads of examples of such code. Perhaps the simplest is the case of mutable iterators that I mentioned. A correctly implemented mutable iterator is perfectly safe, but rejected by Rust's borrow checker unless certain parts of the implementation are marked unsafe.
And no, memory safety is a huge deal, it is just that the borrow checker cannot verify the soundness of certain code, meaning you have to provide the guarantees normally given to you outside 'unsafe' blocks.
Yes, this means that a few data structures require 'unsafe', but you should be creating safe wrappers around these structures; 'unsafe' won't propagate up your code and poison everything.
And I was just proving examples of such code for someone who asked. Honestly, some Rust folks get so defensive it makes them very prone to misinterpret simple factual statements about Rust as criticism.
Apparently you don’t disagree with any of the factual statements that I’m making. You just have some vague unsubstantiated feeling that I don’t ‘get’ Rust.
You're adding other claims and statements that make me question if you actually understand the thing you are criticising.
I will if the Rust community agrees on a lighter, less complex language core.
"Microsoft, AWS, government agencys in Europe, the Linux kernel itself (the only other language allowed there is C - think about it)."
Again, they are using only a subset of the language. Linus allowed it in the kernel once devs agreed only to use the Rust core features. They even had to make a new memory allocator, IIRC.
They also didn’t have to write a new allocator; they did extend the interface of the “alloc” library (which sits between core and std) which was then also accepted upstream.
It's also kind of normal to you know tweak a language so it integrates with an OS better... Even if they did change their allocator, who cares?
I think that a lot of complexity in Rust comes from many different ways to handle memory. You have your Arcs, Boxes, Cells, etc. Then you have a core "alloc" library.
Maybe if everyone agreed on a single reliable method to handle heap memory (since using stack is easy), Rust wouldn't be so hard to use.
The syntax also could be improved. Seeing <Box(T)> or whatever nested three or more levels deep hurts my eyes.
Boxes, cells, arcs, etc. exist for different purposes and if you'd take two hours to actually read about them and their uses, you'd understand why they exist.
> Having a complex programming language which limits you and controls how you use it does not sound appealing to those who need a feature done, fast.
See, I don't think Rust makes that _harder_ - I think it surfaces the real complexity, makes is undeniable. So far in my - by now 20 year old - career, when I've seen most teams faced with complexity that might either slow down a project, or be swept under the rug, the latter was always chosen.
There are _business reasons_ for that, software projects don't live in a vacuum - but those reasons are... _depressing_ at best - by far the biggest one I've seen is that a correctly estimated project would never be approved. Let's just say that figuring that out is a cognitive hazard that can easily increase risk of burning out - literally the sort of knowledge you're better off ignoring.
So ultimately my line of thinking is, Rust's problem in the business sense is basically the same as when you try to code in it: it _tells you that you're trying to sweep something under the carpet_. It tells you that your feature's cost is under-estimated. And to be fair: what can you do about it? The feature wasn't truly estimated by you. It was, almost certainly, estimated by your boss to be "one sprint at most," before he even asked you, and "please estimate this" has an expected answer, and giving any other one has a high social cost. And goddess protect you if your answer might call the entire business model into question.
I don't think any of us can change this, nor that the software engineering workforce is ready to push for changing this. Because even if we convince _our boss_ - the investors will just pick someone else, after all.
--
Anyhow, now you'd be excused in expecting me to advocate some sort of gatekeeping - but no, what I like about Rust is that it attempts to make programming more accessible in a _very different way_ to how many high-level programming languages did. For example:
- it's not _just_ a systems programming language. It doesn't divide programming into "stuff for anyone" and "eldritch monstrosities for the Chosen Ones" - eldritch stuff is just around the corner, in the same language, - it doesn't try to hide the complexity - it tries to give you tools to _explore it_, - and, expanding on the latter, its community values documentation and teaching over restricting use cases.
I don't think Rust is entirely _successful_ at that - but I think that the different model for accessibility is, if anything, more interesting than the borrow checker. I don't think we really need a single language - a garbage collector can be performant and useful, for example, and does it really _have to_ actually result in sweeping complexity under the rug?
Currently Rust is not as complete or well defined as Ada/SPARK or MISRA C/C++ combined with commercial analyzers (Astree for example). At the same time Rust is "too sound and rigorous" for mainstream programmers.
Rust advocates are in the losing battle of of forcing mainstream to adopt "it does not compile unless computer says its ready" style. Reliability and soundness comes with a cost.
Most software is not worth of the cost. When you adopt subscription model, fixing bugs is where the money is. If your app works flawlessly without updates, Apple removes it after few years.
If I was doing something high level, I would probably use Ocaml which has nicer features. If I wanted to write safe code, Ada offers a better experience with the availability of SPARK for parts I would probably end up wanting to prove. If I just want to write concurrent code with the certainty I could find someone to maintain it, Go seems the way to go.
Rust seems to want to be a replacement for Ada but seems to actually attract programmers used to high level language chasing the hype.
What’s the story with Rust and verification nowadays?
HPC, HFT, GPGPU, LLVM/GCC infrastructure, GUI, console SDKs, drivers SDKs, security certification ,....
I not not seeing Fermilab or CERN doing real time beam processing in Julia anytime soon.
Or stuff like having graphical shader debugging tools for Julia.
It is in progress but likely several years to go before a MVP is released. Ferrocene [1] is the primary effort that I am aware of and that seems to be making progress towards a verified rustc suitable for safety critical work.
Ferrous Systems is working with AdaCore and just released the Ferrocene Language Specification [2] to formally document the Rust subset that Ferrocene will use.
[1] https://ferrous-systems.com/ferrocene/
[2] https://ferrous-systems.com/blog/ferrocene-language-specific...
Gotta say SPARK does seem to provide much of what it claims. Interesting that NVIDIA seemed to be using it for their secure bits.
"From the data obtained, we can make the following key observations. First, there are 9 out of 72 rules for which violations were observed that perform significantly better (α = 0.05) than a random predictor at locating fault-related lines. The true positive rates for these rules range from 24-100%. Second, we observed a negative correlation between MISRA rule violations and observed faults. In addition, 29 out of 72 rules had a zero true positive rate. Taken together with Adams' observation that all modifications have a non-zero probability of introducing a fault, this makes it possible that adherence to the MISRA standard as a whole would have made the software less reliable."
[1] https://repository.tudelft.nl/islandora/object/uuid:646de5ba...
Dunno what you mean. I like Rust. I think it’s by far the best language in its niche, which contains exactly two languages: Rust and C++.
If what you’re doing doesn’t fit into that niche, that’s fine! Don’t use Rust.
Who is forcing anyone to do anything?
Year - Most Loved Pct. - Rank
2015 - 73.8% - 3
2016 - 79.1% - 1
2017 - 73.1% - 1
2018 - 78.9% - 1
2019 - 83.5% - 1
2020 - 86.1% - 1
2021 - 86.98% - 1Not claiming that this explains the entire increase in the percentage (I have no way of knowing) but I don't think this data contradicts my reasoning either.
I find Rust perfect for hobby projects because:
1. Squashing bugs that show up after the code successfully compiles (or, in Python's case, appears to work) is a big drain on motivation.
2. The ecosystem's focus on API stability means that, once it works the way I want, the costs of ensuring "it built/ran yesterday, so it should build/run today too" will be minimized.
3. I don't have to write as many unit tests to feel I can trust it with my data.
TL;DR: Once you're on the same wavelength as the compiler, Rust is great for projects where your motivation is purely intrinsic.
(Well, that and the compiler has been constantly improving. Things especially got much better after non-lexical lifetimes landed.)
It's pretty trivial to see that Rust adoption is increasing, so you can discount (1).
Is this weighted somehow by overall popularity?
Rust really is a great programming language that solves a lot of other problems other languages don't. It does so by introducing a pretty clever paradigm.
The rust community is the least toxic programming community I've ever seen. Leaps and bounds away from go lang, python, and light years away from c, etc.
Like use your head, is vba therefore the language which everyone tried and loved and had to move on from? You're trolling yourself lol. Rust is great but yes you have to learn it and yes that is tricky at first. Join a rust chatroom or discussion board and give it a whack.
Were you around when Scala was the end all be all of programming shiny things?
"Early on, Scala rode a wave of hype that frankly surprised even me: hype around pushing syntactic boundaries, hype around reactive architectures, hype around functional programming, hype around the Apache Spark project. Much of that hype has since died down, and there was a period of backlash and negativity both within the Scala community and outside of it. Since then, even the backlash has faded, and what is left is a reasonable, boring language steadily advancing and providing a great platform for general software engineering."
source: https://www.lihaoyi.com/post/TheDeathofHypeWhatsNextforScala...
Not every concern needs to be solved at source level. My brief crit of both Scala and Rust is that they do not take a modular approach and instead have opted for a 'comprehensive' approach that necessarily entails (arguably) unreasonable levels of complexity at syntactic and semantic layers of source code.
I think rust differs from scala in what it is trying to do. I also think rusts' complexity is backed by functionality that defines it's paradigm. Scala on the other hand has a few too many 'i think this would be nice to support' features in it that make it messy to deal with.
Rust imo isn't messy in that regard. Does scala still have it's place, sure. Does rust have it's place, yes.
Also if you read the posts on here, there's a lot less hype going for rust than the other way around. It's success isn't really due to it's hype, it's because people like what it does for them. Lots of people hate on it, usually baselessly.
You needn't use your real name, of course, but for HN to be a community, users need some identity for other users to relate to. Otherwise we may as well have no usernames and no community, and that would be a different kind of forum. https://hn.algolia.com/?sort=byDate&dateRange=all&type=comme...
Rust's "pretty clever paradigm" lifts a large set of low-level concerns into the application domain, and consequently makes them the responsibility of the application programmer.
This makes sense for a subset of programming contexts, where the application programmer needs to have and assert specific positions in those domains, in order to produce viable programs.
The problem is that, overall, very few programming contexts actually benefit from this level of specificity. An HTTP service implementing a business-level capability absolutely does not give a shit about ownership lifetimes. The fact that Rust "solves" this category of issues is completely irrelevant to this class of program.
...
I spent a lot of time reading r/rust, various blog posts before going beyond helloworld in it.
To me it's in the same category as Scala (ZIO) or TypeScript. Both are very powerful, a joy to work with them, until the low-leven limitations hit in (JVM or JS).
However I don't interpret that as a sign of oh we need to go back to assembly, but more like, okay, we need to accept that our tools and the problems we use them on are not in harmony (yet).
Though I might argue that one of Scala's biggest issues in my eyes, performance, is mostly _not_ a JVM limitation.
For example, a Scala for-loop calls functions on each iteration, and immutable containers need to be garbage collected for each modification. Both of these could be compiled away in many common cases (like C++ or Rust do, but Scala's compiler doesn't, unless they've changed that in the last couple of years).
Which is a shame because I did a hobby project with it and it was super fun and liberating compared to old java.
Well, they didn’t break the existing language, so not sure. It depends on what do you find readable/difficult. Because contrary to the usual opinion on the language, I believe it is not “difficult” - it has much fewer exceptional rules than Java for example. Sure, some features are more expressive and that comes with big responsibility, but to actually answer your question, some implicit usage is indeed cleaned up making it much less magical-looking.
But scala 3 has been a very huge update, including having a new compile flag that will turn to language “null-safe” (so a nullable String will have String | Null as a type). So it might be worth it to have a new look at the language.
And for TS I don't think I have to mention the quality of the underlying JS ecosystem :)
On the other hand, of course Rust is still on 1.x, GAT is still on the roadmap (though the RFC recently entered into final comment period (or how it's called), yaay!, and then a bunch of legitimate criticism was submitted..., so it might still take year(s))
If you ever read, I think it was Modern C++? Where they point out the huge "trick" in C++ was someone figuring out you could make a compile time branch by declaring arrays of size 0 or some such nonsense. They then wrapped that feature in a template so you could then try to use it. At some point they formalized it so it's no longer based on arrays of size 0 but still, read any boost library and look at how unreadable it is. It might be nice to use, but it shouldn't be so hard to write!
But, I think the fact that it is hard to write brings joy indirectly to many programmers. They get a dopamine hit for "solving the puzzle of finally getting their cryptic incantation to work". Maybe they write a bitset class. It takes 500-1000 lines of code. Or maybe they make a lighter weight fixed size array and again it's 1000+ lines of template code. They forget that the goal of coding is shipping, not having fun solving the incantation puzzle.
It doesn't seem like it take 1000+ lines to do these things.
The author of a library with wide usage spending hours refining a method/class/function to it's most optimzied form may actually be worth the time because that effort is amortized over hundreds or even thousands of applications. The ROI is much higher in that case.
Determining when it's correct to make that tradeoff is an important part of the job. Boost probably makes the correct tradeoff. But your average C++ application dev might need to make a completely different one.
I feel like this article is written from a perspective of a library author that always starts with going for zero allocations generic API. In production apps you very rarely do that, usually it's plenty fast anyway.
This type of thing comes up of often when people try the language. They re-implement some part of a program, originally written in a fast GC language, and then wonder why it’s slower.
I feel like the power of the language comes through in specific workloads, or when you take the additional time to avoid naive code. That’s why it is so verbose and rich, it gives you more control. And this is something that advocates often clearly state.
Like I posted earlier, you don't do this everywhere, just some sporadic use where it makes life a lot easier. It doesn't have to be all or nothing.
Was going to post the same comment. Most of my hobbyist Rust programming has been via the Actix Webframework and I only run into the most trivial of borrow checker issues. I guess my projects are not complex or interesting enough :).
What I've found good about rust is its modern toolchain and documentation.
For instance, I haven't yet had to debug weirdly muting memory, deal with the immense nuanced possibilities of doing the same thing (there's a reason for CPP core guidelines), stuff like the rule of five, writing cross platform cmake files that include various libraries, ...
Not proof for a general statement, but another example from a Rust adaptor
Imo I write programs faster than other languages because the compiler helps me out. Your mileage may vary.
Incorrect. This handler-dispatcher problem illustrated in the article is utterly trivial in C++ and many programming languages. No need to struggle and stretch your brain as if you are in the Math olympiad.
I am learning Rust myself and I think Rust fans are spreading misinformation and propaganda by saying Rust makes things easier than other languages. No, Rust makes things VERY difficult - and you need to study and learn the Rust way of doing things - which is significantly different from other programming languages.
We need a BIG design patterns for Rust book - which takes lots of common design problems and shows the idiomatic Rust way of doing it.
But just stating that C++ code for a given design problem is harder than Rust is utterly wrong and demonstrably false. Rust tooling is definitely simpler. Rust coding is definitely not.
I don't think anyone is trying to claim that rust is some trivial language. It's not. But it makes systems level concerns explicit, flying blind is much harder in my opinion. The complexity is technically the same, but the language has training wheels that other languages don't.
This is absolutely not true. Something hard in Rust but almost trivial in C++ (via fold expressions): mapping or folding a function or operator over a tuple of heterogeneous types. E.g. to sum over a tuple of arbitrary numeric types:
auto mySum = std::apply([](auto... x){ return (x + ...); }, myTuple);Fwiw rust doesn't let you dynamically index tuples, because rusts' take on tuples is different.
Should your tuple actually be a struct with a "sum" method on it? Imo probably. Or should you have your numeric types be the same type for better memory layout, maybe...
Just because another language let's you do something doesn't mean you should do it. Kind of like in js, does it make sense to divide a number by a string(not sure if you can do this but guessing it's valid js), probably not, is it a feature or a bug, you decide. Should this be part of say the kotlin standard, probably not.
That is not true of C++. There are many valuable things that are much easier to express in C++ than Rust, owing to C++ being a significantly more expressive language. Rust also doesn't make it easy to write software where ownership is intrinsically ambiguous and must be algorithmically resolved at runtime without wrapping the object, a common case e.g. for high-performance storage.
I would agree that everything is harder in C, though.
It's a matter of style and character how you cultivate new skills, and Rust wants you to cultivate them in a certain, some people are just not made for. YMMV.
But if you deviate from those, into higher level programs, a language with garbage collection will always be much more productive than Rust. So we have a set of people doing the things that Rust does best, and celebrating that they have a great language, and a set of people trying to fit the square language into a round hole, and complaining that it doesn't fit at all.
Added to that, there is a wide space of problems where high-level languages do not have appropriate support, but you can always hack your way in with a low level one. Those are the worst, because there is just no good option, there's just one that you can beat into working.
I was recently tasked with developing a fragment of a mobile app written in Objective-C++ (an awesome PL powerhorse, BTW) that mixed multi-threaded (multiple dynamic UI layers, multiple levels of background processing) and async (multiple over-the-network downloads, multiple database updates, etc.) To make a long story short: even though I've been coding C++ for 20 years, I am still a bit depressed about how badly I got my own @$$ handed to me on my first try. It took a ton of bug hunting and code refactoring to get everything right. Being forced by Rust to throw together a couple of somewhat inelegant reference counted pointers is... trivial compared to the benefits Rust offers (safety).
I was also recently tasked with investigating serverless. To make a long story short: due to the fact that serverless is billed by the amount of memory used and the CPU time required to execute a lambda, so far in my analysis Rust is coming out on top over interpreted and garbage-collected languages to such extent that it actually makes the difference not just between a "good" and "very good" solution, but between an "impossible" and "feasible" one, cost-wise. Being forced by Rust to read a couple of books on how the new concepts of ownership and lifetimes work is... trivial compared to the benefits Rust offers (efficiency).
If you define "feasibility of a commercial success" as "finding a pain point and being the only solution for it on the market", then it becomes more and more likely that, with time, Rust will win over many other PLs.
It seems that serverless is cost prohibitive in many scenarios, and requires lots of optimization to be cost effective. So isn't it more of a fault of serverless than a strength of rust?
If infrastructure is a priority, (e.g., "we don't really want to hire anyone just to setup/monitor/secure that Linux box with all those REST API server/database processes, thus we prefer serverless), serverless is important and Rust performance is its strength.
If infrastructure is not a priority, (e.g., "I was gonna roll out my own VPS anyway, I love this stuff"), serverless is all about its faults and Rust is irrelevant (it is logical to choose an easier PL, e.g., the excellent Golang).
It’s having a product vs. not having one and the latter guarantees a lack of commercial success.
Async & Tokio is pure hell if you have any kind of shared mutable state. And also it's tough to fight 20-30 years of programming experience with modeling things in an object-oriented fashion; I find tying state & method into structs and using 'self' on methods in OO fashion just makes things far worse when async is involved.
The thing that really bugs me about Rust and I think it's the core of what really confuses many people is that it upends our normal intuitions about scoping. In other languages we're used to thinking of the scope of things in terms of lexical scope at the block/function/module/class level, and inside that scope, anything is game. You can almost visually see it. Usually.
Rust can work this way, but mostly doesn't by default. It's like you were programming in C++ but instead of normal copies and references, almost every single variable use was a std::move. Profoundly unintuitive. I just used this variable, why can't I use it again?!
I think many of the things in the language -- lifetimes and borrows especially -- should have been modeled with a more explicit syntax instead of hiding inside existing standard language features (type parameterizations and assignment)
You just need to get used to it, really. As you've noted, it's about our habits. The ownership model makes perfect sense after some experience in Rust, unlike that stuff with asynchronous programming.
A lot of borrow checking related stuff in rust forces you (until you internalize how it works) into a check/compile-fix loop that is horribly inefficient and tedious. if performing moves had special syntax it’d be palpably obvious to even new rust programmers that a move is happening and that assignment does not work as it does in about 99% of other programming languages (in most cases).
also important things that then invert or alter this behavior should also have special syntax. for example, I’m still not sure how any client code is supposed to know that some library type implements Copy (which complicates things further by inverting rusts inversion of typical semantics). seems like the only way is “try it and see” or read the docs. I guess the idea is that you shouldn’t necessarily need to know this, but I do think rust would have benefited from making borrow and memory handling more transparent and obvious with special syntax around it (beyond lifetimes, at point of use, not just in signatures). As it stands it seems to want to give users the control that comes with manual memory management with the sort of “hiding away” of details that GCs traditionally afford
This was my experience of rust as well. I've since forgotten exactly what I was trying to do (it was a year ago now), but a seemingly innocuous check of the variable I was using meant I couldn't use it immediately afterwards. I have no issues with move semantics in C++, but making that the default behavior in rust made it a pain in the ass to use, especially when it doesn't seem to be consistent. I ended up rewriting a week's worth of rust in C++, including the unit tests, over two days.
It feels "bolted on".
Variable assignments and usages that borrow should be clear with a different syntax.
Lifetimes should not be lumped in the same box with other type parameters but instead broken out visually into some other sort of annotation on the type.
BTW I also think "std::move" in C++ is a hack. They should have introduced a new syntactical element for that as well instead of masquerading it underneath a pseudo-function.
Most of the pain here come from the unholy trifactor: combining async, lifetimes and dynamic dispatch with trait object closures; which is indeed very awkward in practice.
Async support is incredibly half-baked. It was released as an MVP, but that MVP has not improved notably in almost three years.
There are lots of ideas, some progress on the fundamental language features required (GAT, existential types, ...) and the random RFC here and there, but progress is painfully slow.
This isn't because no one cares, but because Rusts async implementation (which is very cool due to the low overhead) and its interactions with the other language features require complicated extensions to the type system. It does seem to me like there might be a lack of resources/coordination/vision ever since the Mozilla layoffs, but that's a different topic.
If you can avoid async I would recommend doing so. The problem is that the entire ecosystem has completely shifted to async. There are almost no active / popular libraries related to network IO that haven't switched over.
To be clear: using async is fine if you know what you are doing, and it can provide incredible performance. But if you do, keep it simple: avoid lifetimes and most importantly: don't attempt advanced trait shenanigans - if you do need traits, just returned BoxFutures without lifetimes, throw in lots of clone(), share as little data as possible, use Arc<tokio::sync::Mutex<_>>, and call it a day.
Yes. I've been complaining about async contamination for some time. I'm writing heavily threaded code, with threads running at different priorities, and libraries which want async get in the way.
If you look at the poster's example, the "Arc" version is very close to the Go version. And if it didn't use "async", it would be even closer.
Go's green threads simplify things. It's real concurrency; you can block. But there are times when you have to lock.
As I've said before, if you're writing webcrap, use Go. The libraries for web-related stuff are stable and well-exercised, since Google uses them internally.
I don't like Go, I hated the year I had to work in it @ Google. But frankly, it's better suited for 'server' type stuff, unless you're talking about a very specific type of server that has super intense latency guarantees. And now that Go has generics, I'd probably hate it less.
Go is the new Java. Rust is the new C++. Let's just stick with that.
To be fair, I usually prefer to use go as well, lately though rust is more appealing.
There's different kinds of fast. For most kinds of fast that people doing 'web' type things need, a garbage collector and a VM are not going to be the bottleneck. Efficient management of workloads across blocking I/O is going to be where the hard work is.
Now, I've written ad servers and video streamers, and other low latency high throughput things, and yes, I'd probably reach for C++ or Rust there. But some of the jobs I've seen lately posting for Rust, I do question. Even if I'm tempted to apply, because I'd like to get $$ to work in Rust.
That said, people misuse technology all the dang time(I've done it). And in general I agree, Go is usually enough and has the best cloud ecosystem. I've had trouble with large go code bases exploding over time and requiring a lot of bodies to maintain compared with rust/scala.
Sometimes it's hard to speculate when one tool is clearly better than another. Some companies just want to use new stuff to sound cooler...
Super high-end stuff can outclass Go by that much, like if you're seriously using DPDK or something, and in those cases I strongly recommend Rust over Go. There some other noches like that. But in general, it's not a factor of magnitude.
However, by far the bigger problem is people thinking their little web service serving out 5 requests a second requires that level of optimization when in fact even a Go implementation will use <1% of the CPU and no other resources to speak of. Dynamic scripting languages, IMHO, have skewed a lot of people's performance views. Go is already more power than most problems need, in terms of raw performance, and it's only a sliver, a niche where that's the difference between a Go vs. Rust choice.
It's a delicate understanding, but an important one for a professional.
For a lot of web stuff you need throughput more than you need latency "fast".
In order to do throughput well you need concurrency.
Anecdata are all I can provide, but the equivalent (Google) batch job in C++ vs. Java is often an order of magnitude better for a variety of reasons. It might only be 0.5-3x faster, but it also uses less cores and less memory to do the same amount of work.
Having written C++ and Java services, the median C++ service is performs better than the median Java service. Some of that is less pointer chasing, some of it is better libraries. Some services will be slow no matter what you write them in. There's too many variables to quantify, and "performs better" is a real load bearing term. Sometimes it's less CPU, memory. Sometimes it's latency.
I still primarily work on Java services, and they perform pretty well. That said, the operational dynamism of Java services is a real thorn in my side. Beyond the GC, there's a lot going on in the runtime including JIT that just makes JVM services less predictable. Even classloading is a source of unpredictability, unless you eagerly load classes.
FWIW, I think Go does a pretty good job being predictable too. AOT compilation helps there, you're not worrying about new tasks needing to JIT. Go mostly worries about GC (often irrelevant), warmup of state (like TCP connections), and avoiding footguns that leak Goroutines. :) At least Go's footguns rarely blow your whole leg off.
https://web-frameworks-benchmark.netlify.app/result?f=axum,g...
I've done some benchmarking with Actic, Axum and Go (std lib). Actix was top, Axum about 85 % of Actix and Go about 75 % of Actix.
>"Rust is the new C++. Let's just stick with that."
I write "web domain stuff" in C++ and it is incredibly easy (well for me at least). In C++ I could always use my own styles / paradigms / patterns etc. etc. Not forced to any particular way. And modern C++ is incredibly safe if one wishes.
So if it is bad idea to write "web stuff" in Rust it means it is anything but new C++
I run my own company, prefer to count money and Have no shortage of work - so could not care less about what "the rest of the industry thinks".
Go is on the same page performance wise with Java and C#. Sure, if you don't need the best performance, you can move to another language. But, then, why did you start using C++ in the first place?
That's also one of its major disadvantages, unless you literally rewrite the entire thing when major maintainer changes happen, because otherwise you get a mix of different C++ styles in your codebase which leads to nobody being able to maintain it.
If you application is actually using the OO parts of the language that way, even without modern C++ its fairly easy to maintain as the different styles/etc area also encapsulated in their classes. Then hopefully the top level is using some kind of message passing interface/whatever to avoid trying to glue everything together into a god class.
AKA, there are a few fairly easy to understand rules that allow people to do their own thing without creating a maintainability nightmare even with very large C++ codebases. If an experienced engineer/architect/etc with a track record of successful C++ projects is in charge during the initial application design/etc you should have a fairly maintainable application.
I'm not sure this is really a C++ thing though, rather being a general engineering thing. If the main architecture is well thought out and understandable, a lot of sins can be burred in places where they can't create application wide chaos.
There are a lot of things that can make C++ applications suck, but you don't tend to hear about the success stories on your favorite board, those systems silently do their job. So many of the things people rail about with C and C++ simply aren't problems when appropriate engineer culture is maintained. AKA, having solid unit tests for most of the base classes being agragated, means that running them under various address sanitizer/etc tools will find the errors that aren't picked up by static analysis tools/etc.
Yes, sometimes things sneak by, but i'm not sure there are any languages java/rust/etc that solve that problem completely.
I'd be really interested in meeting someone who has enough mental flexibility to learn C++ but not enough to accommodate different code styles in a single codebase.
If you are not embarrassed by your old code, you have not learned anything since.
I use other libs of course but those are domain specific and are solving non web problems
Can we cut it out with the hyperbole? Using "It Isn't Really Safe Unless It Is Written In My Favourite Niche Language" as an argument is .. well ... ridiculous. You can extend that argument to any practical programming language.
So ... Rust ... "Isn't Really Safe" because you can get into a point where memory is corrupted, where your application deadlocks, or threads starve, etc.
Haskell ... "Isn't Really Safe" because it is not possible to formally verify the logic.
Etc, ad infinitum ...
Maybe go for "Not As Safe As". After all, I don't even like C++ (see my comment history), but it's certainly possible (and not very hard) to get about 90% of Rust safety in C++.
Safety is not a binary, it's on a spectrum. Saying that a language is either safe or unsafe implies that the "safe" language is actually safe while the "unsafe" language is completely deadly. That's certainly not true.
i dare you to find memory corruption in safe rust that isn't already on an issue tracker.
> Saying that a language is either safe or unsafe implies that the "safe" language is actually safe while the "unsafe" language is completely deadly.
tell that to the people who got owned by https://googleprojectzero.blogspot.com/2021/12/a-deep-dive-i... , most likely human rights activists targeted by less-than-savory regimes. memory corruption bugs are so frequent that it is possible for nso group to sell to pretty much any regime, and not forced to be classified as top secret information.
can't you just mess around with /proc/self/mem :)
https://github.com/rust-lang/rust/issues/32670
(To be clear, I’m not saying that there’s no issues that aren’t in the tracker yet. But there are a bunch of them that are. More will absolutely be found as time goes on, that’s just how these things are.)
Safety isn't an all or nothing thing, and in most cases, rigorous safety comes with tradeoffs. In some applications those tradeoffs are worth it, and in others they're not. A small project probably doesn't need to be provably correct, because it's easy enough to analyze it, and you aren't gaining much if the more rigorous language is unwieldy (they often are); similarly, a large enterprise project is harder to analyze, and has larger costs if there are CVEs, so a safer language is probably a good fit. Note that this doesn't necessarily mean rust, though -- garbage collected languages sit here, too. Rust sits in the niche of "big enough and / or critical enough to justify rigorous safety", and "high performance really matters".
Maybe I'm the worst C++ programmer in history and maybe I didn't spent too much time in trying to write web apps in C++ - it was just to test if it's a viable approach - but that was my particular experience.
Aside from doing more boiler plate code, the language being more ceremonious, I find that I miss the tools like frameworks and libraries which are very easy to integrate with each other and which I take it for granted in C#. Probably the story is the same with any other language used for web: Java, JS, Ruby, Python and even the (in)famous PHP.
I think the situation can be much better if someone would make some nicely designed frameworks and libraries, but I guess no one is interested to as C++ is perceived as a "not for web" language. People who are into C++ are generally systems programmers who are not into Web, and people who are into Web are taught they only have to use "web languages".
That is really a shame because for some situations there would be a huge benefit of having performant web apps - scaling out is not always a solution to a performance problem.
Before .NET we had ATLServer, then C++/CLI.
They also published some frameworks for writing web APIs in C++.
Now, I would also advise C# and then if needed, to call into C++ via P/Invoke (or C++/CLI, C++/WinRT if on Windows), than exposing C++ directly into the wire.
I rewrote web apps written in PHP and Python. Those were rather decent size and in my case app specific code was about the same size in C++ as in the other 2. The performance was of orders of magnitude better.
My applications do not contain millions line of code and maybe because of this and the way the code is organized I do not really suffer long compiling times. Usually it is just few seconds. Good enough for me.
So all in all, I really don't think that the (long-running) productivity and maintainability of managed languages can be approached by these low-level langs. And that is fine, (thank God) not everything is a dumb CRUD web app, there are very real niches where that low-level detail is a necessity.
This is not the function of the language but the ability of the developer to properly architect their code.
And tools like Visual C++ / CLion have very advanced refactoring features.
But this all said, there's definitely a point to be made here - Go works fine for so many use-cases, people should use it if they like it or it fits the story better. The idea of "one true language" has never worked out.
wasm-pack build --target web
And it builds a little wasm file and some js shim, and that's all it takes to get a Rust website up and running.Why can't I just compile to wasm32 with Cargo? What's missing?
Mostly because of performance. Looking at latest Techempower benchmark, stuff written in Rust is 50% faster than stuff written in C# or Java. [0]
If that difference in performance and the tradeoffs to obtain it are worth, that's for everybody to decide.
Have a look here:
https://web-frameworks-benchmark.netlify.app/result?f=axum,g...
Possibly a combination of type safety + speed in one bucket makes the difference
I hope not, it might have some generics now, but still has a lot to catch up with.
I surely don't miss coding in Java 5 (2004).
> Rust is the new C++
Rust might become the new C++, and while being safe by default is great, there are plenty of C++ use cases where Rust has zero presence in 2022.
The good reason is the target demographics as Rob Pikes puts it, people that don't care about learning what programming language can be like.
I don't get why are you so dismissive about web programming? The Go libraries are good because it's easy to write good libraries in Go. And it's easy to write good libraries in Go, because of design decisions. Same for C#, F#, Java, Python and more.
Once an application has been written in Go, updating dependencies or using a more recent compiler version is a breeze. Nothing breaks.
Rust code on the other hand comes with a high maintenance cost. The ecosystem is still very unstable. Core dependencies constantly have breaking API changes, or get abandoned/deprecated/superseded. Keeping everything up to date is not trivial and very time consuming. I abandoned projects due to this, and others use dependencies with known vulnerabilities, that may not even compile any more at some point. But dealing with changes in Rust dependencies instead of the actual application logic is not fun.
So, for a project that has to be maintained long-term, I would choose Go anytime. Productivity is so much higher, especially when including maintenance cost.
I'm only a hobby coder but this has hit me a few times. I suspect this will level out in time, though.
* "image": 0.24.2 (Read and write common image formats, 7,257,076 downloads)
* "glam": 0.20.5 (Basic 2,3,4D vector and matrix operations, 1,155,849 downloads)
* "cached": 0.34.0 (Cache management, 1,367,883 downloads).
Rust needs a push to get everything with more than a million downloads up to version 1.x. Then the semantic versioning rules are supposed to require no breaking changes for existing code without changing the major version number.
The ecosystem, or the ecosystem where network IO is a thing? Surely that's just a corner of the Rust library ecosystem. I have almost never used network IO (databases, http,...) in 20 years of programming, and zero times in Rust.
There is a big (and might I say extremely comfortable) world of programming everything is on one machine, and things are CPU instead of IO bound.
It seems like everyone doing async Rust goes through a long journey before arriving at this conclusion. Once you know the pitfalls you can navigate around them, but I hit a lot of dead ends along the way.
The "special quality" about Rust that hurts here is that it takes a lot of time going into a dead end before you realize it's a dead end.
Does Rust give you rope to hang yourself when doing it without async or does it continue to be very specific about forcing you to guarantee that you’re not going to run into races and whatnot?
Once upon a time, the whole promise of Rust was “fearless concurrency”.
In terms of shared-memory threading concurrency, Send and Sync, and the distinction between &T and &Mutex<T> and &mut T, were a revelation when I first learned them. It was a principled approach to shared-memory threading, with Send/Sync banning nearly all of the confusing and buggy entangled-state codebases I've seen and continue to see in C++ (much to my frustration and exasperation), and &Mutex<T> providing a cleaner alternative design (there's an excellent article on its design at http://cliffle.com/blog/rust-mutexes/).
My favorite simple concurrent data structure is https://docs.rs/triple_buffer/latest/triple_buffer/struct.Tr.... It beautifully demonstrates how you can achieve principled shared mutability, by defining two "handle" types (living on different threads), each carrying thread-local state (not TLS) and a pointer to shared memory, and only allowing each handle to access shared memory in a particular way. This statically prevents one thread from calling a method intended to run on another thread, or accessing fields local to another thread (since the methods and fields now live on the other handle). It also demonstrates the complexity of reasoning about lock-free algorithms (https://github.com/HadrienG2/triple-buffer/issues/14).
I find that writing C++ code the Rust way eliminates data races practically as effectively as writing Rust code upfront, but C++ makes the Rust way of thread-safe code extra work (no Mutex<T> unless you make one yourself, and you have to simulate &(T: Sync) yourself using T const* coupled with mutable atomic/mutex fields), whereas the happy path of threaded C++ (raw non-Arc pointers to shared mutable memory) leads to pervasive data races caused by missing or incorrect mutex locking or atomic synchronization.
Thread1: takes lock A, ..., tries to take lock B
Thread2: takes lock B, ..., tries to take lock A
Looks like you should be able to pass Mutex<A> and Mutex<B> to both threads otherwise what's the point of mutex if there's no way to share data protected by it, so it doesn't look like it prevents you from hitting this scenario.
I wonder if it is similar to the halting problem. Can deadlocks even be prevented in theory?
The mechanism is costly but elegant. If a lock you are trying to acquire is owned by another thread, you inspect the locks you own to determine if that thread is waiting on one of your locks. When a deadlock is detected, there are several strategies to automatically resolve it e.g. rolling back one of the threads to a point where forward progress can be safely serialized.
No one wants to use these mechanics for ordinary code, due to their cost. For the fashionable thread-per-core software architectures, deadlocks aren't something you commonly have to worry about.
In the trivial example you gave, one strategy just insists we take locks in alphabetical order. Thread 2 can't take lock A, because it already has lock B and that's not the correct order.
The common method for deadlock-free code is roughly that when a thread is required to wait on a lock owned by a second thread, it checks to see if the second thread is waiting on a lock already owned by the first thread. This requires that the lock graph essentially be a high-performance and concurrent global structure.
Locks like this can be expensive, particularly under high concurrency or contention, so they aren't used for most software. If you can fit your software in a simpler model e.g. where locks are singular or only acquired as a DAG, then much higher performance options are available that don't require deadlock detection.
This should be more ergonomic as this should get rid of everything needing to have send/sync traits. I also suspect it may be more performant as I am not sure how good the async runtimes are about keeping scopes pinned to a particular core so its not constantly jumping around and busting the l1 caches (which would be extremely detrimental to compute latency and bandwidth)... Happy to be schooled on any of this.
For an efficient core context switch the scheduler must accurately predict that the source (current) core won't be free for the duration of the full core context switch time and that the sink core will be free by the time the meta context gets there and will have been free by the time the rest gets there. Otherwise, the scheduler ends up thrashing the cpu (it is actually a bit worse as future task might need same context so you have to be aware of the future). So, for the scheduler to know this it would need to be:
- Global: The only scheduler on the system or basically rafting with all the other schedulers on the system
- Prescient: The scheduler(s) would need to be able to predict all tasks, thier context, and work time per task perfectly. Which could really could only happen when everything is static and hence deterministic.
For example, I think most tasks people are throwing at async are web requests. Most actually take the core an order of magnitude shorter time to compute then the time it takes passing the context from one core to another and they are all unpredictable to the scheduler. In this scenario I could see the scheduler taking up the majority of computational time on the system. So turn on multi-threading + async on a quad core and you will get worse bandwidth and latency(always) for all your pains.
EDIT: Although this single data point would tell me I am wrong (see description):
I wonder how much technical debt is slowing things down. It seems like we keep hitting a breaking point where we'll finally be forced onto polonius and chalk but people keep finding ways to extend the existing implementation to make things work, kicking the can down the road.
That said, if you have a physical server, Java is quite good, especially with the upcoming Project Loom improvements. The langspec and vmspec have gotten fat the last 20 years, but at least those documents exist. Plus there is OpenJDK which is a great enabler and calmer of nerves; there's a reason so many alt langs target the JVM, and they are good. Groovy, Clojure, Kotlin, Scala are all first-rate languages, IMHO. And with projects like Quarkus you can more easily build native executables that bundle the JRE and make distribution very Rust-like.
Another environment I like specifically for async operation, but mostly by reputation, is Erlang and it's BEAM VM. Erlang itself is such an interesting language, being dynamic, functional, immutable, without traditional control structures (!) but relying heavily on recursion and pattern matching, and of course the "actor model" and extremely lightweight "processes" was invented here (and promptly ported to elsewhere, as with Akka). It was also created by one of the nicest human beings I've ever experienced, Joe Armstrong, may he rest in peace.
IANALL so YMMV.
I'm not sure what you mean here, but if you refer to OracleJDK here then there is basically only OpenJDK for quite some time now -- OracleJDK is just an (optionally) paid support version of the same codebase. Also, most other vendors are pretty much just tiny patched OpenJDKs also, with some niche exceptions.
Swift's design is made to be good for IO workloads on smaller clients, though it hasn't got as many tools for the other end.
The nice thing about this is... You don't have to care. Haskell is a good enough programming language to just put the platform-specific non-blocking hell APIs in a library and let you write code that looks linear and blocking. If you want more control you can get down to the low level non-blocking APIs, but that's usually not going to be worth the trouble.
Haskell IO is basically as async as Javascript, as in every operation is fully asynchronous, you need to call some foreign function if you want otherwise. Except that you have parallelism and can have concurrency too added if you want.
For example, in your typical web application a Goroutine is spawned for every incoming connection. This basically gives you a dedicated runtime for each simultaneous connection. In Node or Python, this would be the equivalent of starting a whole new process for every request. But in Go, there's very little cost to doing it this way.
The advantage is that each connection basically never blocks waiting for another user.
In Node and Python, we talk about handling tens to hundreds of requests per second per server.
In Go, we talk about handling thousands to tens of thousands per second. It's just orders of magnitude more throughput.
See: https://www.fastify.io/benchmarks/
Node is more susceptible to poor code and out of band IO slowing it down, but the V8 runtime itself is cpp and simple services are close to just writing decorators over a cpp webserver. It's obscenely fast and good enough for most purposes.
Not only can write programs that are "async", but you also get easy retries (and other tricks), safe refactorability (because of its Pure FP nature), reliable and painless resource management and some other goodies like STM (Software transactional memory).
It's really that good.
Even in regular Rust, trying to get too clever with lifetimes can cause serious pain. The usual culprit is complex code that tries to never allocate memory. "Oh, well this closure borrows this parameter from the parent function, and then stores a reference to it in this stack-based structure inside the closure, and then we pass everything by reference to this higher order function..." Just say no.
If you add async to the mix, then you need to keep your ownership simple. If you have a long-running async function, then pass parameters by value! If you have a polymorphic async function, then return your result in a Box. (This also breaks up the generated state machine used to implement async functions, and can reduce binary size.)
So much of this pain is caused by premature optimization.
In C++, if you get to clever, you eventually make a mistake and segfault. In Rust, if you get too clever, it eventually becomes impossible to satisfy the borrow checker. Most of the time, the solution is to be less clever.
Async Rust was a fascinating experiment, but in practice, I think it turned out to be a tool that's best used conservatively. I've written some very stable and high-performance production code using async Rust. But I keep it simple when I can.
I've seen a lot of new people also prematurely over generalizing code. I know it sounds terrible to say, but if you probably aren't going to reuse it, you really don't have to prepare your code base just in case you might later. Decide that later and move along...
This reminds me: "you are holding it wrong"
You're trying to draw a comparison with a consumer product with a design flaw.
Not at all. I just do not like to dance around bonfire with tam-tam and being told that this is the one and only way
I've taken to making heavy use of the smallvec and smartstring crates for this. Most lists and strings are small in practice. Using smallvec / smartstring lets you keep most clone() calls allocation-free. This in turn lets you use owned objects, which are easier to reason about - for you and the borrow checker. And you keep a lot of the performance of just passing around references.
I tried to use async rust a couple of years ago, and fell on my face in the process. Most of my rust at the moment is designed to compile to wasm - and then I'm leaning on nodejs for networking and IO. Writing async networked code is oh so much easier to reason about in javascript. When GAT, TAIT and some other language features to fix async land I'll muster up the courage to make another attempt. But rust's progress at fixing these problems feels painfully slow.
https://crates.io/crates/smallvec / https://crates.io/crates/smartstring
Agreed. Copy, Clone, Box, Arc, RwLock, etc. are your friends. Don't be afraid to use them.
You don't need as much performance as you think you do. Passing things around on the heap is fine for most applications. The Rust compiler is often smarter than you think.
And, when the Rust compiler is dumb or your code is getting stuck, you have code that you can understand and find the hotspot of. Now you can be clever.
Part of the problem is that Rust itself has such lofty aspirations to do it all (particularly "abstraction without overhead"), and is largely successful at that. So if I compromise on efficiency, it feels like my code is unworthy of the language it's written in.
Also, it's easy to feel that the slightest inefficiency puts us on a slippery slope to the bloat and slowness of something like Electron. I'd like to fight back against that bloat, the upgrade treadmill, and the rapid obsolescence that serves hardware makers but hurts poor people. On the one hand, there was good software in the 80s and 90s that made liberal use of heap-allocated, reference-counted objects with dynamic dispatch. On the other hand, there were slow, bloated desktop applications before the rise of Java, C#, Electron, etc. So I don't know where the right balance is at.
How it'd be designed and implemented is probably a theme for a separate blog post, not a HN comment.
And then the O in OCaml is all about OOP.
I want to write `toString (1,true)` not `ToStringPair(ToStringInt)(ToStringBool).toString (1,true)`.
Traits, maybe? Switch to an OO model more closely aligned to what the majority of developers understand. Dump all of the line-noise-type syntax.
My Own Toy Language (ComingRealSoonNow)^tm that I started designing had exactly one goal - prevent the majority of memory errors, not prevent ALL memory errors. All I wanted was to indicate when/where an object will be destroyed/mutated. That's enough for me.
Also the basis for one programming language that used to be widespread in the enterprise for quick and dirty solutions, VB and its ecosystem of OCX libraries.
It is more widespread that people think, because when many argue about OOP, they miss the full spectrum of how OOP is approached.
So? Giving up Rust-Traits doesn't mean that I will give up on interfaces as a whole.
I don't like the way C++ does interfaces, for example, but that doesn't mean that my ideal language won't have interfaces.
F# is probably what you're looking for if you want functional-lite programming with a strong type system, but don't want to deal with the pains caused by static resource management.
[0] https://docs.microsoft.com/en-us/dotnet/fsharp/get-started/i...
https://github.com/ionide/Ionide-vim
If you want to use .NET, F# is a fine choice. But otherwise, and especially in the context of an alternative to Rust, I recommend OCaml.
F# to the rescue. :)
Right but you dont have todo this everywhere, just some sporadic use where it makes life a lot easier. It doesn't have to be all or nothing.
You can have the best of both worlds: In many cases, I simply put an object into an Arc, clone that a bunch of times, and pass/store it to wherever it's needed. Then, within loops, I pass a reference to local function calls (instead of cloning the Arc for every call). For any given v: Arc<T>, doing &*v is zero-cost – unlike in many other languages, where you'd have to pass the Arc itself, which would involve atomic increments/decrements without escape analysis.
Meanwhile it has one of the fastest concurrent+GC runtimes, obliterating Ocaml in this regard, and competing handily with JVM or CLR langs.
Finally, it has standard abstractions for concurrency and parallelism that are basically flawless.
Clearly, the hate comes from the association of Haskell with category theory, which is frankly useless/borderline harmful to the working Haskell programmer.
CT was critical to core library devs being able to deliver the main workaday abstractions that make Haskell such an unreasonably productive app languages. But since 2012 you can simply use these main abstractions and enjoy a level of safety and maintainability that rust, c++, ocaml, kotlin, java can't touch.
It's not perfect, but there's been huge improvements of late to tooling (cabal) so there's never been a better time. I really wish the tide could turn, because if you're willing to take on rust for low-level, you're missing out if you don't try haskell for everything else server-side.
Just make the allocations.
Even if you write it that way it's still significantly more performant and lightweight than the alternatives without much loss in productivity.
The important part is perspective. Nobody cares if you clone your command line arguments ten times during startup. As Dijkstra said, optimize the 2% of your program that matter, keep the rest simple
All that is actually needed is for it to be more work to get it wrong than to get it right.
Rust is normally used only when high performance is of uttermost importance, so it will always attract people who want to optmise everything.
> Most of the time, the solution is to be less clever
I've done it myself and saw performance degrade, as was expected. That's fine, but by that point you might as well just use another language that has none of the hard problems and end up having very similar performance, which is what I did.
Which is not the way to do things. Profile, then optimize.
I'm writing a metaverse client that's heavily multithreaded and can keep a GPU, a dozen CPUs, and a network connection busy. Only some parts have to go fast. The critical parts are:
* The render loop, which is in its own higher-priority thread.
* Blocking the render loop with locks set during GPU content updating, which is supposed to be done in parallel with rendering.
* JPEG 2000 decoding, which eats up too much time and for which 10x faster decoders are available.
* Strategies for deciding which content to load first.
* Strategies for deciding what doesn't have to be drawn.
Those really matter. The rest is either minor, infrequent, or not on the critical path.
I use Tracy to let me watch and zoom in on where the time goes in each rendered frame. Unless Tracy says performance is a problem, it doesn't need to be optimized.
The Rust game dev ecosystem is far enough along for simple games, but not there yet when you need all the performance of which the hardware is capable.
I have trouble with concept of WGPU. GPUs are complex by themselves to bolt on top any abstraction that is coming from Web. But its just me, its not important since I am not a 3d programmer myself. I am more Engine / CPU optimization guy.
My interest in Rust and this topic is that i would like to see fine grained task parallel systems written in Rust. Instead of systems with separate thread for render that became a bottleneck years ago. I wish you good luck and hope to see a success story about Rust.
WGPU's API is basically Vulkan. It exists mostly to deal with Apple's Metal. Metal has roughly the same feature set as Vulkan, but Apple just had to Think Different and be incompatible. I'm not supporting the Wasm or Android targets. Android and browsers have a different threading model, and I don't want to deal with that at this stage. Linux/Windows/Mac is enough for now.
Thought for the near future - will VR and AR headgear have threads or something more like the processes with some shared shared memory model from Javascript land?
(That video isn't me, it's the Rend3 dev, who also works on WGPU.)
I dont work with VR/AR myself so I dont know.
I'm using Rust since ~2 months. At the beginning I was trying to write my usual C/C++ code and I got completely entangled with lifetimes relationships and the code ended up becoming a mess.
Nowadays as soon as the compiler starts mentioning lifetimes I see that as a warning => I then take a step back and change approach/design => no problems.
I use a little bit of async (I create dedicated variables that are moved to the async functions) and classic multithreading (I use "mpsc" to exchange data between the main & subthreads) and so far I never had a single segfault nor any kind of weird behaviour, which is incredible if compared to some other languages at least for my programming skills :)
And yet it seems like so many things have tied themselves to this async/tokio mast. It's not a good look for Rust.
To extend this with a slight caveat: for many of those network IO libraries, there's still often a sync alternative. e.g, ureq in place of reqwest works for many use-cases and doesn't bring in an entire tokio runtime for a blocking request. You can find sync DB libraries.
Some other crates have started to catch on and offer them as a feature-enabled adapter (e.g, Sentry does this and can use ureq in the background).
I feel like a real problem, though, is that there's a division of eyeballs across these boundaries. I would chip in to funding work on a "reqwest-non-tokio-adapter" or something that utilizes all the same reqwest types, but avoids Tokio.
(And I like Tokio! I'm basing a new project on it as we speak. I just cringe every time I need to use reqwest::blocking because of something the sync alternatives haven't gotten around to implementing.)
A proper algebraic effect system could resolve the problem. You can take a look at Koka to see how elegantly it abstracts common control flow patterns using the concept of an algebraic effect.
It could also take as long as the async MVP itself to get off the ground, and you can't know if it wouldn't hit its own snags when deployed at scale. (From long ago, I remember the "non-parametric dropck" RFC as an illustration of a neat concept colliding with reality.)
Languages with mainstream aspirations evolve under greater pressure. I don't know this, but I strongly suspect that async was necessary for Rust to gain acceptance at, say, Amazon. So it couldn't practically have been designed with a wide-open timeframe. The result is what we have; one can uncharitably call it "half-baked" and tut-tut about the sharp edges, but it's workable if you know what to avoid. (But it's so tempting to imagine what could have been, eh?)
Dancing around with lifetimes can be premature optimization. Yes you can write very efficient code that way but if you find yourself spending tons of time fighting the borrow checker you might be overdoing it.
I tend to use Arc<> a lot in async code. It makes things relatively straightforward and easy to reason about. Mixing lifetimes with async is probably the most confusing thing you can possibly do.
... Until third-party libraries push you to use specific features.
(1) Async in the core language and do async I/O.
(2) A fat runtime like Go that implements lightweight concurrency.
(3) Everyone hand-rolls their own implementations of select, kqueue, epoll, etc. loops as well as optimizations like io_uring for every single application.
Rust chose (1) because (2) is off the table as it's a systems language and (3) is more painful and bug-prone than (1).
As for how to implement async: I am having trouble thinking of a significantly better model than Rust and the fact that the Rust community hasn't dramatically improved async shows that a bunch of people who are way smarter than me about languages and type systems are also having a tough time here. Doing async natively in a systems language with virtually no inherent overhead is some seriously difficult stuff.
I personally think the biggest oops with Rust async is the absence of a runtime in the standard library, forcing everyone to pick a third party library and causing fragmentation around which one to use. Tokio seems like the clear winner but having played with both Tokio and Smol I really think the latter is better designed. Tokio does not implement structured concurrency and makes dealing with lifetimes harder. If we used Smol there would be less of a temptation to just throw Arc<> everywhere and less memory leak bugs around forgotten-about tasks.
(For those curious about the latter: Smol JoinHandles abort when dropped while Tokio just forgets about tasks and leaves them running when handles are dropped.)
I have come to the same opinion about Scala. Stick to the basics (i.e. the Scala Book[0] and doesn't even have to be all of it) and it is a joy to use.
[0]: https://docs.scala-lang.org/scala3/book/introduction.html
wouldn't Go afficianados say Go is a counterexample right under every Rust programmer's nose?
(i definitely could have worded that better up above, to your point!)
I have been coding since the late 70s -- the latter half of the last century. After all I have seen, async feels wrong.
In pseudocode:
1. var logResults = Background writeLogServiceStarted() // Sent to different machine
2. var authoResults = Background performAuthorisation() // Perform by 3rd party
3. var userSettings = Background getUserSettings(request.currentUser) // Stored in DB
4. var results = Background executeQuery(request.query, authoResults) // Different DB
5. var response = Background generateResponse(results, userSettings)
6. wait (logResults, response)
7. transmitResponse (response)
The current async/await solution doesn't really make this as clear as the above though: The code is littered with some form of unwrapping/wrapping at every step hiding the actual intention, the call stack is marked as async making it hard to figure out where and when a sync function can make a call, etc.As the primary mover of the MVP (who stopped working on Rust shortly after it was launched), I'm really sad to see this. I certainly didn't imagine that 3 years later, none of the next steps past the MVP would have even made it into nightly. I don't want to speculate as to why this is.
I also recommend avoiding async if you don't need. Unfortunately for people who don't need it but do want to do a bit of networking, a huge part of the energy behind Rust is in cloud data plane applications, which do need it.
At the risk of rehashing something that has already been discussed to death, did you stop working on Rust because of the difficulty of launching that MVP? I imagine that all of the arguments involved, particularly about things that are prone to bike-shedding like the await syntax, could be exhausting.
Any idea where we should send money to get things moving again? Lack of money is always the biggest problem in open source, right?
FWIW, I only started seriously using Rust in the past year. I quietly watched and waited while the async/await MVP was being developed. I didn't participate in any discussions. On the one hand, that means I didn't exacerbate any exhausting arguments. On the other hand, I wasn't actively supportive either.
Coming from a C++ background I appreciate Rust's novel approach of borrow checking, though I'm not sure if it's more "elegant" than C++'s approach with unique pointers, move constructors etc.. What I love is the modern ecosystem and build tooling around the language (including documentation and package management), for me that's Rust's main advantage over C++.
Coroutines are nice as they allow trivially writing an app which scales to millions of concurrent operations. Rust is nice as it lets us write code for every occasion, async is awful as it poisons the entire ecosystem. The same result could have been achieved with a few tactically inserted yield statements for I/O and locking which are pretty much the only times that async or coroutines help.
Your application has n threads to begin with, if you use async, these will be utilized to schedule tasks. You're basically trusting your language to properly pause and restart the procedures.
If you spawn new threads, these will be managed by your kernel. If that kernel is Linux, that it already has a pretty good algorithm to pause and restart them, so you're not really wasting a lot of resources.
So I believe there are effectively two differences: threads can scale up to the limit of your os/kernel and hardware. Async can scale up to the level of the threads your language spawns to handle these tasks. Threads trust the kernel/OS to prioritize workloads, async trusts the language
The interesting thing about async (especially async-await) is that it is a fluent way to write a program that waits for events (e.g. I/O), without resorting to inefficient designs like thread-per-client in order to achieve concurrency.
Software (even in C) has made use of this concept for a long time through poll() and friends; async-await is just a higher level abstraction over the same concept.
I couldn't disagree stronger on that one. Abstraction is generally good, but this becomes a purely philosophical discussion as soon as you ignore the actual implementation when talking about the differences of how something actuals behaves and should be used. And philosophical discussions like that have their place but are imo pointless in the context of what should be used, when. They're generally better placed in a context of deciding wherever you want to implement an abstraction
I do fundamentally agree with the rest of your comment however. that's what I meant with trusting your language to prioritize workloads vs your OS.
FYI, Rust (the language) has no concept of prioritising workloads; there is no async runtime in the language.
Even with any of the async runtimes, workloads (i.e. tasks doing work) are prioritised by the OS. It's just threads executing code. Async runtimes are primarily concerned with the gaps between work, like waiting for I/O or other events.
Thread context switches are pretty expensive. If you use an async runtime such as tokio, all your tasks will be spawned on a fixed number of threads and you won't have any context switches on a task switch.
Thread context switches are pretty expensive.
Opinions are pretty divided on this one these days. There was a time when this was very much true but there is some evidence that it is no longer, practically speaking, an issue except in extreme cases. You do pay a cost but it's not immediately obvious that the cost is material. I reach for a threadpool first and async when it becomes clear that I need the extra performance. 99% of the time the threadpool is more than good enough.It doesn't have to be one or the other. Go uses coroutines and multiplexes them onto multiple OS-level threads (usually as many as there are cores).
As always it is a game of trade offs, but it is something that bothers me.
No, because you wouldn't slap `Arc` on every single variable. The example from the blog post is a dispatcher and `Arc` is then needed for dispatched functions. The same might be true for queues and other stuff where you need to move stuff between threads, but other than that it would be mostly `Arc` free.
As an example: when you write a web service in one of the frameworks like Actix, you typically use `Arc` for stuff that you need to share between all of the handlers like DB connections, queues, counters etc. All of the other stuff would be relatively straightforward Rust without much consideration for lifetimes.
The Go version is similar to the final Rust version; except the Go version is forcing you to use Arc everyone[1]. Seriously, just use Arc (or Arc<Mutex<>>), in 70% of cases you are wrestling with the borrow checker trying to do something dangerous, in the other 30% the borrow checker is wrong and isn't smart enough to understand what you are trying to do. In both cases, 95% of the time, you aren't creating enough objects per second to justify getting rid of that atomic add.
I'm not even sure this is limited to async code either. I've seen plenty of code by C++ "zero cost" gods which abuse lifetimes in order to avoid a single clone or Arc. At least the compiler made sure you won't segfault, but I'm not sure the complexity is worth it over just an Arc.
[1] I know this this is a simplification.
It also blurs the advantage of Rust as a lifetime validation tool if you use reference counting anyways. But it seems that it's the only viable approach for async at the moment.
let id2 = id.clone();
let watch_id2 = watch_id.clone();
let id3 = id.clone();
let watch_id3 = watch_id.clone();
let ready2 = Arc::downgrade(&ready);
let video_stream = async move {
// use id2, watch_id2, id3, watch_id3, ready2
}
My realization is that in most cases an Arc is the right tool for the job, especially in concurrent applications. I see it as the borrow checker doing it's job, if you have two threads that need to share data and one of them may go away at any moment, the only way you can ensure the memory stays around is with "runtime borrow checking", e.g. a garbage collector.In C++, the way this is solved is usually "just trust me, I won't use this memory here"
If Rust forces you to use Arc where in C/C++ you would normally just be freewheeling your lifetimes, by complaining about your lifetimes, a lot of people are going to see that as "Rust is really annoying and gets in my way, it's making me choose between a tangle of lifetime bounds and explicit Arcs". The truth is that it is teaching you that your normal style would have led to mistakes and you should never have been coding that way. So people go on complaining about being re-taught until their normal style works in Rust, or until they give up. (I don't think that's what you're doing, this is mainly just to say Arcs are not a waste of the language's power, rather an effect of the language's power to know when your freewheeling would be mere lifetime guesstimation and quite possible buggy in C++.)
The author's point is strong though.
> the process of designing APIs is affected by numerous arbitrary language limitations like those we have seen so far.
There's a long way to go in being able to perfectly express all the ways you might want to use lifetimes. Sometimes you come across a situation when a few of these limitations converge on one spot, and you get the author's problem. But equally, the author is trying to write something that has probably never had its lifetime rules described before. Yes, languages with GCs can do it easily. That was also true of lock-free queues & running destructors. GCs have it easy in a ton of different areas around lifetimes. The core complaint here is that Rust does not have a GC and we have had it really easy being able to rely on one & not having to write down exactly the lifetime bounds that could make it work statically. The author is complaining that there are hundreds of compiler issues preventing some of this being expressed... well, yeah, nobody has ever, EVER, tried to write this stuff down before in a machine-readable way, we are slowly covering the untrodden ground, and you might be in an especially untrodden area. I think that's a pretty good excuse for the compiler having issues and some things being not quite expressible, but it's also an important caveat to "fearless concurrency" and I think the post illustrated that well.
The overhead of atomic reference counts everywhere is higher than the overhead of a good garbage collector. This thinking is biased against Go for no reason.
I don't think it's biased against Go. The an `Arc` (or garbage collector) is the correct way to model lifetimes for complex programs whose lifetimes are managed at runtime rather than compile time. In the context of both languages, rarely is overhead of an atomic add or GC going to be the limiting factor of your programs, just like Java programs are efficient. While static memory management is nice, it's not the sole reason I continue to use Rust. I've been using Go since 1.0, and there are two times where I have ever thought "My program is slow because of GC", and in one of those times, it was solved by simply upgrading Go.
Rust might yet get there.
He is a talented kid from Kazakhstan. So nice to see this here after all that Borat nonsense badmouthing Kazakhstan just for cheap gigs.
Some of what the author is complaining about matches my conversations with people who aspire to be Rust library authors — that you're often trying to hijack the type system because you <do> actually know better.
The narrative that "Rust isn't hard" is getting tiresome, and I say this as someone who writes a lot of Rust. Let's be honest that Rust can be harder than many other programming languages in many ways, but those of us who use it believe the upsides and tradeoffs are worth the minor to moderate increase in difficulty.
Pretending Rust is easy just sets beginners up for disappointment when they get into Rust and realize it wasn't what they were sold. Or worse, they start doubting themselves when they encounter the hard parts because Rust fans were busy insisting it's all easy.
If someone says programming in another language is easier, it's because they are not attempting (or are failing) to do one or more of those things. Those things are not always important so that's fine, but a lot of programmers (myself included) have this dream of being able to solve a problem once rather than over and over again, and that's why Rust appeals to us, and why we argue against Rust being hard - because it's actually easier for our particular usecase, but clearly not everyone has that same usecase.
Correct.
> Rust is the easiest language to do that in.
Wrong.
Rust provides very substantially less support to library designers than C++ does. Anyone creating ambitious libraries finds Rust a big step down.
Rust might get more of what C++ offers library designers as it matures, but only at the cost of whatever simplicity it can still claim.
Before I used Rust my main language was C++ where I specialized in writing libraries, and this is a ridiculous statement.
It's only recently that the C++ standard library has gained enough functionality to do even some basic things in a portable way, so you're relying on other libraries to provide that, yet there is no standardized way to declare dependencies on those other libraries. There is no module system to ease structuring library code. There is no hygiene - a ton of stuff you include will just pollute the global namespace. There is no standard way to version your code. There is no standard way to update a library. Everyone uses incompatible string types - seriously, if you think Rust has too many string types, wait until you find out that every freaking C++ library represents strings differently, sometimes using the same types though! There is no standard place to publish libraries. Even basic language types like `int` differ massively from platform to platform, or even between different compilers on the same platform. Each compiler's preprocessor behaves slightly differently. The programmer must manually forward declare their functions, types, etc, and the rules are different for inline/templated code. All code is unsafe, and yet the rules for what constitutes UB are informal at best. (Whereas in Rust, the rules for UB are also not fully defined yet, but this is only relevant for the minority of your code which is not safe). All experienced C++ programmers think in terms of lifetimes, and yet cannot express this through the type system, so this must be documented informally. There is no standardized coding style or format.
I'm going to stop now just because I'm bored but this list could go on for a very long time...
In fact today C++ lifetimes are expressed in the type system, and this is an example of what C++ enables a library to provide.
If, working as a library designer, Rust was not a big step down, you were neglecting to provide users of your libraries much of the value you could have offered. Your remarks suggest that libraries you delivered were closer to C than C++.
That's not true, the standard only specifies that some things are UB, it is not exhaustive, nor is it unambiguous. Furthermore, no current C++ compiler is compliant even with those parts of the standard which everyone agrees on (eg. see proposals to introduce a "bytes" type to LLVM to resolve known miscompilations). There is work to improve this, such as defining an explicit memory model, but Rust is ahead of C++ on this.
> In fact today C++ lifetimes are expressed in the type system, and this is an example of what C++ enables a library to provide.
Do you have an example of this, or are you talking about using smart pointers? Smart pointers are about ownership, not lifetimes.
> Your remarks suggest that libraries you delivered were closer to C than C++.
Most of my remarks were about consuming other libraries from my library, which is not something I have control over. Sure, I can use smart pointers and other modern C++ features in the API I expose... That doesn't change any of the points I mentioned.
I feel you, but, my experience differs in the end I guess. To be fair turbofishes we're something I discovered by intuition, so maybe I'm a lost soul.
The author compares Rust code with Go code. Those two languages serve entirely different purposes, with entirely different mechanisms behind it.
Go does what the author does with ease because the runtime fixes all the complicated parts for you. You tell Go what you want and it'll try to solve all the memory management/threading/memory safety issues for you, usually succeeding.
Rust doesn't do that. Rust expects you not just to tell it what you want, but also how you want it to happen.
I think comparisons between Rust and Go/C#/Java are what will really trip up a beginner. Rust has a lot of nice features found in higher level languages, but it's decidedly not a higher level language. Rust operates in the space of C and C++, where a small mistake can cause memory corruption no debugger will ever be able to unravel, but where a well placed byte of padding can accelerate a program by as much as 30 percent.
I think the difficulty in Rust lies in that it will enforce correctness. Competing languages are less strict about that, especially when it comes to threading. You tell them a piece of memory is safe to use across thread boundaries and they'll believe you, and most of the time you can rely on race conditions not screwing you over perfectly well. A C program can be short, fast, and clear, as long as you leave out the error checking and resource management in case of failures; with Rust you often don't get that luxury. Writing correct code is a slow, tedious, painful experience, and in Rust you'll have to live with that pain (unless you throw around unsafe{} everywhere).
I believe that teaching programming should follow a bottom-up approach, but many others disagree. If you've dabbled in assembly, done some multi threaded C(++) and experienced the challenges in low-level code, Rust should be enjoyable enough to learn after cursing at the borrow checker a bunch of times. If you're a top-down learner, though, you'll run face-first into low-level problems and their complexities for seemingly no good reason.
Enforcing correctness at compile time is not the only way to insure correctness.
Some do enjoy solving language puzzles (so choose Rust) and some prefer thinking before coding and prefer solving design puzzles. I personally prefer that latter, as the 'hard' problems are intellectually interesting, solving them is satisfying, and over the years the design lessons build upon each other. At which point you don't need a Mommy Dearest Compiler to ensure correctness.
Crucially, it's hard to prove whether or not you've actually solved whatever design issue you wanted to overcome. Such a proof usually would entail some sort of analysis of the program as written (because it may actually differ from your design). To perform this analysis, you may want to annotate the lifetimes of the various objects as they are declared, so that you can track (for example) that some memory is not accessed after it is freed, or any other number of issues.
This lifetime analysis as you would imagine can be very tedious and complicated, so you would perhaps want to automate the process. And that's essentially why Rust's borrow checker exists. It's almost inevitable that it should exist imo. Seems completely obvious after the fact.
> All type systems will have meaningful and true propositions which are apparent to the programmer but not yet to the language team... Some of what the author is complaining about matches my conversations with people who aspire to be Rust library authors — that you're often trying to hijack the type system because you <do> actually know better.
Rust catches some problems (like data races and use-after-free). Safe Rust translated into correct C++ is still correct. Rust also fails to catch some problems (like preventing out-of-bounds indexing at compile time); admittedly idiomatic C++ fails to catch bounds errors at runtime. And when encountering problems that Safe Rust cannot solve (like the generic lifetime quagmires in the original post), C++ often makes it possible (and easier than esoteric programming languages like Unsafe Rust) to solve the problem correctly in the current situation; though admittedly, ensuring you haven't missed any UB cases, and validating that your assumptions don't break later on, is difficult (Unsafe Rust is better at marking unsafe code for future readers).
> some prefer thinking before coding and prefer solving design puzzles
Yeah, you just need to guarantee that person working on it, considered all edge cases, had uninterrupted time to think, thought about how the edge cases interact, didn't make a single mistake, wasn't sleepy, under influence of substances, and perfectly wrote it into the program without a single semantic error (off by 1).
Easy.
That's why I code in Malbolge Lisp CodeGen that outputs Brainfuck.
If maximum performance, whence thread-per-core architectures originate, is not the objective, then GC languages start to become more attractive and C++ may not be the right tool for the job. And in those cases, Rust may not be either.
I'm no professional Rust dev but I wouldn't have written the code like this; I know Rust isn't particularly suited for this style of callback mechanism and I know not to try and force this paradigm into Rust the same way.
For example, I think the author would have had a much raiser time if instead of passing async futures, they'd use channels or some other message passing mechanism in combination with a bunch of blocking threads to communicate events. Such a mechanism would also translate into Go quite easily (less so for other languages, though).
This example was deliberately picked to show a complex problem with writing Rust. I don't think this represents a challenge you'd face very commonly if you were programming Rust all day, at all. It's not bad criticism, but it appears to imply a much wider problem than there really is, in my opinion.
I don't really know what kind of programs require such an elaborate callback system commonly enough where it even makes sense to use Rust. C#, Java, and Go are fast, easy to write, and each have libraries to do almost anything you want. That 10-30% speed boost you can achieve with well-written Rust is probably not really worth the effort, especially with upcoming AOT compilation features in C#.
Rust isn't a solution to all problems, and neither is any other programming language.
I agree. But if you use Rust for web programming, it is fair to compare it with C# or Java. On the other hand, C and C++ feels easier to read and write, a bit more productive than Rust.
So the question is vs C# and Java: is the performance worth the pain and loss of productivity?
And vs C and C++: is the guarantees made by Rust worth the pain and loss of productivity?
I'd argue that in some cases the answer can be yes, while in others it ca be no. So, there's no universal good or bad choice, it really depends on project, team, budget and many more.
The only reason I can think of is the WASM space, which Rust lends itself very well to, to reuse the same entities and data structures in the front end. Then again, you'll end up writing a terribly bloated web UI and other languages have similar bindings.
I think for new projects where C++ makes sense, Rust probably makes more sense. There are some edge cases (if you expect to be operating on trees in memory, for example, or if you're interfacing with libraries written in other languages) but I think Rust is generally better for such system tools. That assumes that you have in house Rust devs, of course; if you're a C++ shop, you'll have to teach everyone a new language before the switch makes sense.
The C(++) crowd is difficult to teach other languages because they, more than any dev group I've encountered, seem to have a larger amount of vocal people who think their code is perfect, they won't ever produce bugs, and all those compiler errors warning about failing edge cases are unnecessary because they know best.
You could be describing me. I recently convinced my boss to let me write a server in Rust for the safety, speed, etc. After being two weeks overdue, I threw it all out and wrote a working version in modern C++17 in an afternoon. Of course part of the issue was language familiarity, but I think also what I was trying to do was objectively harder in Rust in many respects, and the ecosystem of crates was less mature than battle-tested set of libraries I was using in C++.
At the end of the day I want to ship code and move on to the next project. Rust wasn't helping me there.
Programming should be hard only if the problem you are trying to solve is hard. Creating an boring CRUD app shouldn't be hard. Predicting with a good degree of success the market trends for stocks and options should be hard.
This is a very good thing, but if you are a programmer that is used to "copy paste, and then it works". You will have a very, very, very bad time with it. Rust forces you to think about memory. In an age where dynamic typing is so prevalent this seems like a fading art.
I've written before about some of my woes in taking a service to prod with mostly Python code. It was easy to write and get running, but then it had all sorts of errors that were basically impossible to detect until runtime. Kind of a nightmare to maintain. Transitioning it to Rust was a lot of work, but I've literally only had one error that I had to fix, which was caused by my faulty assumption about incoming data that I simply `.unwrap()`ped.
I initially was interested in Rust for the promises of higher-performance, lower memory footprint compute, and that is a great aspect of it. But the correctness guarantees that I've experienced make all the hard work I went through absolutely worth it.
Sure, it would be great if the learning curve was less steep. But that doesn't mean the hard work isn't worth it.
"Is the pain worth it" is the question. My 42 years of professional development tells me the answer is no. (I lean towards programming pleasure and not pain). YMMV.
Imagine if you had to pay for the performance loss or production bugs directly though...
With dynamic languages you can pretend you're done in an hour and then endure a lot of production bugs. That's not being more productive than Rust. That's playing pretend that a complexity doesn't exist.
For a lot of cases, my rust code is very identical to my Python code when it comes to writing CLI tools or flask like web apps.
Oh wait, that’s not what you meant?
I'd go with 1/3 less performance by using Java or C#. While having better productivity. If we are talking a web service, of course.
Is it really as bad, or does the post only highlight the misery you get when you're working on the fringes of what the language is capable of doing?
The hardest thing about rust is, it tells you straight up when you don't understand something you are trying to do. For people who are otherwise productive and think they know certain things, this can be an ego slap. In return it makes you a better programmer because you learned something, it's just a bitter pill sometimes.
In my opinion this person should have gone to rust discord/discourse, and asked for help. A lot of this would have been explained. Some of what they dealt with are rough edges that require practice to overcome though. But yea, lots of people are happily shipping async code in rust, right now. It's very possible.
Some people also get into C++ templates and build complicated things with them for fun (eg the whole subfield of template metaprogramming came from inventing a clever abuse of the accidental power of templates as a poor man's macro system)
rustc strives to provide you as much relevant context as possible. When hitting very verbose output, it is a hint that what you're trying to do is difficult and will require considered design that the compiler can't help you with. The compiler is trying to help you clarify your code so that it can understand it.
Looking at the diagnostics shown in the blogpost, I see only one that is indeed terrible[1], but those in particularly are getting less and less verbose as we tackle them one by one with more targeted diagnostics. I also see the lifetime errors (the closure one when you specify the argument type and the on "`Execute` impl is not general enough"), which I wish gave more context.
As support for HRTBs lands, related errors will stop happening as much and the code will indeed work (or the compiler will be able to tell you what alternative syntax you should be using).
And, in a general plea for people writing Rust, when you encounter a subpar diagnostic, file a ticket[2].
[1]: https://hirrolot.github.io/media/rust-is-hard-or-the-misery-... Be aware that this is not exactly what rustc outputs, as some text decoration have been stripped, but that is a lot of text.
[2]: https://github.com/rust-lang/rust/issues?q=is%3Aissue+is%3Ao...
They can be quite verbose, but that's largely from showing me what the problem is exactly. e.g. Rustc will show you where you borrowed X and then where you tried to modify it after forgetting it was borrowed, not just go "You can't do that, it's already borrowed" and expect you to figure out where and how or fly into a rage because you're sure you didn't borrow it.
The error[E0308] mismatched type message with a type whose name repeats itself several times is a bad sign, yeah, you probably should not make a type like that. But most of the rest look like helpful Rust errors to me.
Any software engineer that doesn't have a love/hate relationship with C++ templates is lying. On one hand they are extremely opaque and not user friendly, unnecessarily so. On the other hand, mastery of that dark art allows metaprogramming that you could only dream of in other systems languages -- the modern C++ template facility is extremely powerful. The number of C++ software engineers that achieve this level of metaprogramming mastery is quite small. In fairness, the C++ language has been intentionally evolving to make metaprogramming much easier than it used to be, and it has made massive strides in that direction.
Rust lacks the expressiveness of C++ template metaprogramming facilities in significant ways. However, it is plausible that it will gain them eventually. The question is if it is possible to support advanced metaprogramming without the train wreck that is C++. I think it is eminently possible to do better, the question is how long it will take other systems languages to have metaprogramming expressiveness similar to current incarnations of C++.
Can you give an example for this?
With C++17, if constexpr, static_assert and type traits provide some tools to make life better, C++20 concepts improve on that front.
Rust macros curently are harder to debug than what for example Visual C++ offers for template debugging.
We are back to primitive expand macro, like in the early Lisp days.
Rust is a systems language. To be a systems PL, it is very important not to hide underlying computer memory management from a programmer. For this reason, Rust pushes programmers to expose many details that would be otherwise hidden in more high-level languages. Examples: pointers, references and associated stuff, memory allocators, different string types, different Fn traits, std::pin, et cetera.
Rust is a static language. This is better explained in my previous essay “Why Static Languages Suffer From Complexity”. To restate, languages with static type systems (or equivalent functionality) tend to duplicate their features on their static and dynamic levels, thereby introducing statics-dynamics biformity. Transforming a static abstraction into its dynamic counterpart is called upcasting; the inverse process is called downcasting. Inside push_handler, we have used upcasting to turn a static handler into the dynamic Handler type to be pushed to the final vector.
C and C++ are both systems languages and static typed languages (I don't know of any systems language which is dynamic typed) and don't feel to me so hard.
C# has "pointers, references and associated stuff, memory allocators, different string types" as features but it is at the same time a higher level language and you make use of those features only when you need to deal with low level stuff, no pain implied.
So, to me, these reasons don't quite explain why "Rust is hard".
Maybe in C++ it's easier to get a program to compile, but it's dramatically harder to make sure you've checked all the boxes you're supposed to check to make sure your code is safe and complies with best practices.
For instance, consider the rules of 5 [1]. There are so many rules like this in C++, so much complicated stuff you're supposed to know to write anything at all, and much of this is just _accidental_ complexity: it's just there for historical reasons or language design reasons that in retrospect don't make much sense.
[1]: https://stackoverflow.com/questions/65455464/is-the-rule-of-...
Your complaints about C++ have aged badly. C++20 is not the same language as C++11, which was not the same language as C++03.
Rust's complexity is rapidly approaching C++'s. In 5 years, if Rust hasn't fizzled, it will get there. Only being used to it will make it seem any simpler.
This is not a good thing. In fact, it's a shockingly bad thing. C++ is basically 40 years old, and still every few years the C++ community feels like, "OK, now we know how to get C++ right; yeah the old C++ was a mess, but now we just need to add these 12 new features and it'll all be good."
Unfortunately many of the problems with C++ are due to mistakes in its early design. It was wrong from the beginning.
I've been programming Rust for 6 years. C++ for over 20. I'll take Rust every day of the week and twice on Sunday.
Evolution is necessary to stay relevant; the only alternative is stagnation. If your language or your code of five years ago does not embarrass you today, it only means you haven't learned anything new.
But C++, even in its pre-template 1980s form, was already full of wrong decisions (granted, many of those decisions were done for compatibility with C).
Including headers is the wrong way to share code. C-style macros are a bad idea. The whole copy constructor idea instead of move semantics, and then an explicit clone function when you need that, was the wrong idea. The complex mess of various constructor types. The friend mechanism to control access is complicated and unnecessary - why not just a simple module system? The complicated inheritance system - virtual, public, protected, private inheritance - what is this goop? The implicit conversions between integer types has caused countless problems. The language is extraordinarily hard to parse.
All of these things (and plenty more) I feel perfectly comfortable saying are just outright bad.
In Rust, all the basic language stuff... the module system, the traits, trait objects, the mechanism for defining new types as enums or structs, even the borrow checker... it's all fine, and it works well together. It's definitely not perfect, I have my gripes. But when I write C++ I can never forget for one second that it's a conglomeration of baffling design decisions and one attempt after another to patch over prior mistakes. I never feel anything like that with Rust.
> If your language or your code of five years ago does not embarrass you today
I mean I get where you're coming from with this, but I would say if an entire community that's been around for 40 years can look back every 5 years and always say "the way we were doing things 5 years ago was completely wrong," that's a serious problem.
But how C++ was coded to older Standards was not wrong at the time. That code was chosen judiciously according to features of the language as it existed then, and our understanding of it and our world.
Changes in languages are not random. Every last change in each new C++ Standard was in response to recognition that code would be better using the proposed change. Failing to use the new features as intended, after, would amount to choosing not to write code better.
Rust started at a snapshot of how problems of coding were understood at one time. Our understanding today and the world we program for have changed markedly from that time. Both will continue changing. What is good Rust will also change as the world, our understanding of it, and the language all change.
Failure to adapt is just failure.
I've written fairly extensive amounts of code in C, C++, Rust, Python, Java, OCaml, Haskell, Clojure, Common Lisp, Scheme, and probably some others that aren't coming to mind right now. All of these languages except Rust have been around for decades (and I guess Clojure is about a decade and a half). My opinion of these languages vary. They all have their warts. Some of these languages I don't particularly like. Some of them are complicated, some are fairly simple.
But C++ is the only one where I feel like the basic features of the language are actively working against me. It's the only one where I feel like such a huge portion of language design decisions were utterly baffling and wrong.
IME some members of the C++ community can be fairly insular. They just can't see how things are done outside their world. You expect me to believe that I'll eventually feel the same way about Rust as I do about C++, but I know that's unlikely to be true because C++ is the only language that I feel this way about.
I honestly don't know of a big tech company that is happy with the idea of just continuing to use C++ indefinitely - they are _all_ looking for alternatives, and Rust is the most obvious option.
Every game company ever. And Google.
ChromeOS as a product? Maybe. (note: not Chrome)
ChromeOS platform internals? I totally could. You don't just rewrite everything from C++ to Rust, because that wouldn't make much sense, but more and more new service/products are being spun up that use Rust (we have docs and stuff https://chromium.googlesource.com/chromiumos/docs/+/master/r...).
I don't think that's true. It seems to me that a lot of people believe this simply because a lot of other people believe it. You see it everywhere. "Oracle rewriting MySQL in Rust". "Microsoft rewrites Skype in Rust". "Linus Torvalds rewriting Linux in Rust". I have yet to see proof of any of it.
It looks like you're actively avoiding finding that evidence.
Citing clickbait titles doesn't sound like you have actually looked.
C++: 27 172
The bigger a company is, the more various languages people there dabble in, and the less it means that somebody there dabbles in your favorite. You would better add up revenue (NB: not market cap) of companies specializing in using your favorite language; but that number would be disappointingly small for any language as far from maturity as Rust still is.
Doubly so when that entrenched state of affairs is actively being attacked every day for years now.
But sure, let me not stop you.
Have people never experienced "Lots of pain" debugging?
All those hoops modern languages make you do are just the same things you'd have to do yourself.
Instead of having to use some language construct to do whatever it is, you do it the simple and obvious way... but all reasoning to prove safety is on you.
Instead of "Learn these 50 features", which is hard but not impossible, you have to "Lol IDK git gud and don't do bugs", which is not only hard, but it's all on you to even figure out if you did it right.
My best advice for loving errr learning rust is put it aside for a few weeks after being annoyed with it. Survey some other language like idk Haskell, then try it again.
> I have the unsubstantiated theory that experienced developers have a harder time than less experienced developers when learning Rust. You need to forget a lot of constructs that work well enough in the languages you already know because they introduce things that go against the single owner enforcement that Rust has, whereas somebody with less experience will simultaneously accept restrictions as "just the way it is" and not seek out more performant constructs that can be much harder to understand or implement.
> Rust has a curse (it has many, but this one is critical): inefficient code is generally visible. Experienced developers hate to notice that their code is inefficient. They will recoil at seeing Arc<RefCell<T>>, but won't bat an eye at using Python. I know because I have the same instinct! This makes it much harder to learn Rust for experienced developers because they start with the "simple Rust code that will work but is slightly inefficient" and in an effort to improve it they land squarely in parts of the language they haven't yet developed a mental model for.
TL;DR: Keep Calm and Call .clone()
Since it was mostly about building something for users and not just my own learning I had to go with go.
Not sure if I will ever learn rust but it would have to be for a hobby project or solve a problem for me that is fundamental to the desired outcome that go, python etc could not.
Handler-Dispatcher is not some complicated design pattern. It is very basic. Its implementation in any language should be short and elegant. This is NOT something you should be losing sleep over.
Rust, unfortunately, is even MORE complicated than C++ in language complexity today. Good tooling does not eliminate the burden of extraordinary complexity that Rust wants to enforce on its adopters.
Certain things are short and elegant in Rust. Certain things are short and elegant in Python. Each optimises for different things.
I also don’t believe it places “extraordinary complexity” on the shoulders of the programmer; this complexity is often a sign you’re doing something wrong, or working against the grain of what Rust is optimised for.
The async story hasn’t finished, and is definitely the area that needs most improvement in terms of developer experience. There are a number of gated features and RFCs that propose solutions to these problems (e.g. GATs), but they haven’t been moved into the core language yet.
That doesn’t mean Rust is inherently bad or difficult, only that we’re working with an early version. Rust is young! We didn’t have futures until 2019, iirc.
Lots of core Rust concepts (lifetimes, ownership, references) need to be rethought for how they fit into asynchronous programming, and how to make everything compose well together.
The benefits proposed by proponents of both languages are there but after the mean of the bell curve.
Also a beginner is asked to work with external interfaces and libraries because the language and its standard library is defined to be minimal in certain ways. The answers to which are supposed to come later. So a lot to take it on the first step itself.
Both languages are still working on some answers and kept options open. So "why" of that is also complicated.
I like the strategy that Nim employs. Due to multiple ways of memory management strategy you can use (different types of garbage collectors, reference counting, manual memory management or no memory management), you can have different combinations of performance, safety and ease of use - so it can fit nicely what you are trying to accomplish.
> It compiles fine now but the final API is defected: ideally, we do not want a user to wrap each handler with Box::pin.
Why not, whats the problem (apart from some sort of philosophical idea of "beautiful api")?
https://go.dev/play/p/sEptCgMM0Bh
In rust, you could not write that code without using either unsafe, or getting borrow checker errors.
Consider the humble for-each loop:
for (let x : xs) {
xs.remove(0);
}
If you're not careful, modifying a collection while you're traversing it can cause you to visit elements multiple times, or skip some elements entirely. There are effectively two tasks occurring at the same time -- interleaved, not parallel, but still concurrent: the ordered traversal and the action on each element. Both tasks can view the list, but one of them also modifies it behind the other's back.This problem simply can't happen in Rust (without going out of your way, at least), because you can't independently mutate a list you're already iterating over.
everytime i sit down to give rust a try i get catapulted down some esoteric rabbit hole about conceptual abstraction over computer memory that rust invented for my safety that isn't fully fleshed out conceptually or in implementation
all my rust programs are provably safe though, because i never finish them and no one ever uses them
As someone who does it all the time, I noticed that code becomes less readable and hard to comprehend that way. Important types stand much less out in function signatures once you have Arc<Mutex<InterestingType>>. Rust is already extremely verbose and adding additional layers to types doesn't help. Not even to mention that now every time you want to access the value you need to do dance with calling .lock().
But Rust is a really bad general-purpose language. Because Rust isn’t a general-purpose language. It’s designed for high-performance, low-memory, safe computing. The only reason it’s even remotely considered “general-purpose” is because of how great it is. Unless you actually need to write something that’s high-performance, low-memory, or safe (or there’s a really important package in Rust that you can’t find anywhere else), stick to something like Swift or Kotlin or Java or TypeScript. Or even JavaScript or Python or Lua if you’re only writing small scripts.
-
Rust abstracts almost nothing about the OS. In order to understand Rust, you need to understand at a low-level how computers actually work and how languages compile. You need to understand the what, why, and how of threads, the stack / heap, I/O, endianness, etc.; and language features dynamic dispatch, references, async runtime, memory management etc.. These language features are all implemented in Rust, but they’re explicit, you have to think about them and choose your implementation.
People say that Rust is hard because of the borrow checker but there’s much more than that: dyn Traits (you have to understand when a trait can and cannot be dyn), Unsized types (and why there are so many type parameters, because everything must be sized), closure traits, multi-threaded communication, what async actually does, etc. Heck, we almost got you to manually choose allocation schemes (they’re default type parameters, which is weird because Rust doesn’t have many implicit defaults).
You often face decisions like
- should this trait be a static / type parameter, or dynamic / Box<dyn Trait>?
- Is this value borrowed or owned or Cow?
- Should this be a unique reference / Box or a shared reference / Rc or a shared-across-threads Arc or not a reference at all?
- How do we control this value and communicate across threads, do we make the whole thing thread-safe or message passing or provide a shared Mutex/AtomicBool?
- How do we manage async, do we set up an async runtime ourselves (and if so how do we configure this runtime), or how do we add integration with existing async runtimes?
- Should this be a Cell / RefCell because we don’t want to or can’t pass a single mutable reference around? And when / how do we dereference the RefCell as to ensure it doesn’t get accessed anywhere else until the dereference is dropped?
- Can we do this safely, aka encode it in a way the compiler understands? And if not, how do we a) do it correctly unsafe and b) make the unsafe code as short as possible?
-
And this explicitness is part of what makes Rust so amazing, because other languages make these decisions for you. But every decision has costs, and either they a) incur higher overhead, or b) lose safety guarantees.
e.g. in JavaScript everything is on one thread, so no data-races but no true parallelism; everything is garbage collected, so no use-after-free or double-free but you get GC spikes; everything is a dynamic boxed reference and types are checked at runtime, so no passing complex types and type parameters everywhere, but calls are much slower and there is no true type-safety.
Or in C++, there are multiple threads with shared memory but you must explicitly avoid data-races; there is no GC overhead but you must explicitly manage allocs/frees; you can choose whether to pass/store things as references or values and there’s a type system, but values can be assigned / casted to the wrong types leading to memory corruption.
Rust is the only mainstream language where you can do unsafe-but-efficient stuff, but it’s actually safe because it’s checked at compile-time.
-
But this explicitness also makes Rust a bad general-purpose language. e.g.
If you want, you can pretty much make everything in Rust like this. #[tokio::main] for the implicit async runtime, everything Gc<RefCell<T>> for no borrowing concerns, dyn Any with upcasting / down casting, etc. This is mainly at the cost of a ton of extra syntax. And of course you get higher overhead and lose compile-time guarantees, which are now checked at runtime.
Or better yet, what you should actually do, is write your code which doesn’t need high performance, low memory, or security guarantees in another language. And possibly interface it to Rust code with FFI.
Because Rust isn’t a general purpose language. It tries to be general-purpose, and is to an extent, but it’s ultimately built for niche performance and reliability guarantees, and you just can’t have those without user overhead.
Most programming projects are single thread, not async, and if you aren't juicing for performance, is pretty easy to write. Even in rust.
I think what makes writing rust for general purpose projects hard, is knowing you can do something better that doesn't have to be better.
But even when you're doing everything single-threaded with Rc and RefCell and Box<dyn>, you can still run into pitfalls. More significant, when you do this your code becomes incredibly verbose. You have to constantly specify over and over again you're doing this the high-overhead way and not the weird optimal way, it gets annoying.
Honestly I think Rust should have an "easy mode", where you everything is implicitly wrapped in ARC / dyn / mutex, and when you write code it's almost like you're working in a truly general-purpose "regular" language. Except it interfaces perfectly with regular Rust and compiles to Rust, so you can still use your favorite cargo packages and even jump into real Rust when you need to.
The issue with that is Rust is already stretching itself kind of thin. So this "easy mode" has to be implemented so it's very simple and you can completely ignore it if you want. Maybe it will just be adding better FFI support to Rust, or maybe it will be another ambitious language like Rust but has better support for implicit easiness.
I guess what I mean by this is just as C++ got move semantics and auto keywords, it's very possible for a language to choose to evolve in ways that can make code easier to maintain and more expressive. TypeScript or Kotlin are examples of this. Also .NET core and Java since Gradle.
Some things rise in popularity despite their complexity. Objective-C and Swift come to mind... as well as writing truly portable Unix code or Kubernetes.
Personally, I'm waiting for the inevitable "Rust: The Good Parts" that I can feed into my linter and help myself and others on my team understand which parts are minefields of complexity and which parts are incredibly useful once understood.
In JS, there were multiple JS APIs and common practices with global state and more that just fell off the face of the earth. Most IDEs now say what is modern and what isn't, backed with new syntax and conventions that avoid traps in understanding like rebinding this for magic syntax, or using lots of stringly-typed functions.
The only problem I can see is that improvements don't come for free, or quickly. In C++'s case, some of the improvements took multiple decades before they were implemented and half a decade to become widespread and recommended...
Meh. If it had been designed for low-memory computing, it wouldn't have had an implicit static global allocator, or a standard library that panics on allocation failures. Currently you have to choose between having no upper bound on your program's memory usage, or using an allocator with an upper bound and accepting that you'll crash on OOM, or giving up on almost the entire third-party crates ecosystem and write with no_std.
The upcoming effort to stabilize std::alloc::Allocator and the A in `Vec<T, A>` etc doesn't help, because every piece of third-party code that currently uses `Vec<T>` is implicitly using `Vec<T, GlobalAlloc>`, and using other allocators in your own code will do nothing to change that. Heck, libstd's own std::io::Read::read_to_end requires a `Vec<u8, GlobalAlloc>`
Maybe someone in the future will invent some bastard child of Zig's comptime, Zig's explicit allocators and everything else from Rust, and that will be the language to rule them all.
Rust is hard. It's not hard because you have to understand all those things. It's hard because you have to understand how Rust understands all those things. Different kettle of fish.
I'm perpetually leery of "you'd have an easier time with Rust if you had a better grip on the CS of systems programming". No, that's not it at all.
(It's fine that Rust is hard. I recognize the achievements, and see where it's a near-perfect fit; I'm looking forward to Rust kernel code. But then, there's a reason almost nothing I write is in-kernel, even when I need "performance".)
Rust has opinions. They are all at least arguably reasonable opinions. But, often enough, you have sound reasons to do things differently. Then, Rust will fight you.
Rust is a pretty good language, and is getting better, but it just takes a very long time for a language to mature. There are no short cuts.
When Rust is mature it will be very far from simple. People will talk about which language subset they are working in. It goes with the territory. Tools that adapt to the real world get as complicated as the world they serve.
The difference is all the stuff Rust adds on top of fundamentals to make it easier to write correct code with respect to memory usage (in other words, it’s a sophisticated system to make sure you always pair one malloc with one free and don’t make any of the other mistakes C permits partly as a result of its simplicity)
Modern systems software architectures are thread-per-core, involve a lot of DMA, etc for performance reasons. That is language agnostic, though most is written in C++ because it is amenable to it. This obviates a lot of the safety features Rust obsesses over. It often feels like the Rust community hasn't acknowledged that this is how systems software actually works these days, or the deficiencies of the language in supporting these software architectures.
If you are using async rust, and want to borrow data, use an Arc<T>.
In fact, if you ever use tokio::spawn, you will have to use Arc<T> anyway because it requires 'static lifetime.
Like `if err != nil`, you know :))
It was dormant for a long time, but it seems like it might be getting some love towards stabilization (or outright removal, I guess) at some point in the medium future.
[1]: https://doc.rust-lang.org/beta/unstable-book/language-featur...
> Finally, imagine that Rust’s issues dissapear, it is high-level, and has uniform feature set. That would presumably be close to the theoretical ideal of a high-level, general-purpose programming language for the masses. Funnily enough, designing such a language might turn out to be a less intimidating task than original Rust, since we can hide all low-level details under an impenetrable shell of a language runtime.
Holy smokes yes. Rust introduces some extremely powerful features that other languages seriously need. Matching on an enum (although Rust's also needs a more traditional enum,) is awesome. Rust's error handling, IMO, is a step in the right direction away from exceptions. I even like the fact that I can enforce single ownership of an object; or have methods that take ownership of an object. And, differentiating between mutable versus immutable without needing different types is infinitely valuable.
Furthermore: My favorite feature of Rust is that it's 100% compiled with no need for a framework or runtime.
Not to take away from your overall point, which I agree with.
By your logic every language, except for assembly, has a runtime.
It's true that you usually don't have to ask anyone to install anything else to run a Rust program, because the default experience of using Rust + Cargo bundles the runtime for you. But the same can be done with the languages you used as examples as well. Many programs do in-fact include .net, a JVM or a Python runtime so the users don't have to care about it.
Biggest example is probably Blender, which ships with Python (and doesn't require a installer). Many IDEs made with Java also ship their own JVM runtimes.
What Rust will likely need is a set of libs similar to Guava, and Guice to make coding actually productive. We already see that starting to emerge with the Tokio ecosystem, however it would be helpful to have stronger support outside of the Async community.
I'm extremely wary of issues of productivity in Rust - most code does not need to be that perfect and the tradeoff is not rational in so many cases.
Rust is the 'C++' of it's era of languages - bizarre, unwieldy. It's an experiment.
We are waiting for the 'James Gosling of Rust' to come along, and blow a ton of unnecessary complexity away with the 'Java' iteration of Rust.
Then it will be mainstream and uncool :)
I have absolutely no idea about what is going on in that article.
Is Rust just completely different from everything else, or have I been completely shielded from "actual programming"?
2. if you don't know if you can afford it, you can.
Probably not for everybody. I haven't noticed any significant productivity boost when coming from C++ to Java years back, and then I also can't see any significant productivity decrease when switching from Java to Rust. I feel the unavailability of GC is widely offset by other features that make Rust more productive than Java. Surely, I got stuck once or twice while learning (Rust is harder than Java) but once it clicked, I know which things to avoid and generally it goes very smoothly. Now there are many things I love about Rust and I constantly miss in Java.
(The number of times I wished I had RAII in Java) > (The number of times I wished I had tracing GC in Rust).
if you were a competent C++ programmer, yeah, i can absolutely see that; nowadays, most people start with a GC-ed language and get tripped up all the time if things they take for granted aren't there. unsurprisingly.
2012: we cannot trust programmers to write correct C
2022: we cannot trust programmers to write compilable async RustLast year I ported a mid size internal project to it as part of a resolution to learn a new language and the experience left me feeling like it was just me who "didnt get it". Building things took a lot longer, not just in the amount of code, but also the compile time. Ultimately compile time was the dealbreaker. On more than one ocassion I'd have to go back to the git history to remember what it was I had planned to do next.
Also there aren't that many special symbols, it's not scala after all.
Spend two weeks with it and it makes sense why things are the way they are for the most part.
> And I consider myself a polyglot.
Then you should realize that Rust borrows some of its syntax from other languages:
- Function's parameter and return types are borrowed from Python's type annotations.
- Turbofish syntax is from C++.
- Closure syntax follows Ruby's.
I tell new programmers to start with either Python or JavaScript. The pay is the same or better, and it's much easier.
I've been programing for almost a decade, I still can't understand lower level languages like Rust and C++.
Why, if you can design a new language, not make something with the simplicity of Python ?
If your going to compile the binary anyway, have the complier figure out the types and optimize.
This is why JavaScript is eating the world. We don't need legions of hardcore software engineers. We need people who use a bit of Python, JavaScript, etc, to make their jobs easier.
The future of programing is easier higher level languages. Rust has some applications, but it's too hard for most
In Rust the design space is very large. For instance, if your new type is going to refer to some data, should it own the data, should the data be on the heap (in a box), should it carry a safe reference (if so, mutable or not?), should the data be reference counted, or possibly atomic reference counted, or maybe for some reason you need to keep an unsafe raw pointer to the data?
In Python this is simple because the decision is already made for you: your new type keeps (essentially) a reference counted ref to the data it needs. That’s it. That’s all you can do.
That’s great if that decision is always adequate for your application. If you need extra flexibility you’ve got a problem.
Rust does have type inference. I normally only use types explicitly when I'm collecting from an iterator, or when I want simpler error messages.
> We need people who use a bit of Python, JavaScript, etc, to make their jobs easier.
I am not a programmer, but I use a bit of Rust to make my job easier.
> Rust has some applications, but it's too hard for most
Debugging scripts is usually harder than debugging Rust, in my experience. Being explicit about things has a lot of benefits in the long run.
Some observations:
- The compiler is always right.
- Do what clippy and rust-analyzer says. Don't ask questions.
- If you're fighting the compiler, clippy, AND rust-analyzer, then you're almost definitely wrong.
Virgins try to use &str everywhere. Chads just use String.