Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Tell HN: Rust Is Complex
107 points by slashdev on Nov 13, 2022 | hide | past | favorite | 93 comments
I'll preface this by saying I like Rust, and I've found myself coding more in Rust the last two years than anything else. But Rustaceans kind of like to laugh at Go, because it's not as expressive or elegant a language by comparison. That's mostly true, think of how nicely Option types, enums, and iterators work in Rust compared to Go. However, Go is simple and deeply pragmatic. There is an underrated value in that. Some parts of Rust are starting to remind me of the horror I ran from with C++. Look at this:

Update: It is possible to abuse existing CoerceUnsized implementations on stable. See #85099 (although I created that issue before reading any of this issue and its IRLO thread, so don’t expect any syntactic similarity to the unsoundness examples of this issue).

The type Pin<&LocalType> implements Deref<Target = LocalType> but it doesn’t implement DerefMut. The types Pin and & are #[fundamental] so that an impl DerefMut for Pin<&LocalType>> is possible. You can use LocalType == SomeLocalStruct or LocalType == dyn LocalTrait and you can coerce Pin<Pin<&SomeLocalStruct>> into Pin<Pin<&dyn LocalTrait>>. (Indeed, two layers of Pin!!) This allows creating a pair of “smart pointers that implement CoerceUnsized but have strange behavior” on stable (Pin<&SomeLocalStruct> and Pin<&dyn LocalTrait> become the smart pointers with “strange behavior” and they already implement CoerceUnsized).

More concretely: Since Pin<&dyn LocalTrait>: Deref<dyn LocalTrait>, a “strange behavior” DerefMut implementation of Pin<&dyn LocalTrait> can be used to dereference an underlying Pin<&SomeLocalStruct> into, effectively, a target type (wrapped in the trait object) that’s different from SomeLocalStruct. The struct SomeLocalStruct might always be Unpin while the different type behind the &mut dyn LocalTrait returned by DerefMut can be !Unpin. Having SomeLocalStruct: Unpin allows for easy creation of the Pin<Pin<&SomeLocalStruct>> which coerces into Pin<Pin<&dyn LocalTrait>> even though Pin<&dyn LocalTrait>::Target: !Unpin (and even the actual Target type inside of the trait object being returned by the DerefMut can be !Unpin).

Methods on LocalTrait can be used both to make the DerefMut implementation possible and to convert the Pin<&mut dyn LocalTrait> (from a Pin::as_mut call on &mut Pin<Pin<&dyn LocalTrait>>) back into a pinned mutable referene to the concrete “type behind the &mut dyn LocalTrait returned by DerefMut”.

From: https://github.com/rust-lang/rust/issues/68015#issuecomment-835786438

Where's that elegance now? I still maintain that I'd rather use Go when the problem domain allows for it (e.g. can use a garbage collector, don't need fast interoperability with C, don't need maximum performance.)



Complexity is relative.

Go is a language for high-level application development. It has a runtime, garbage collection, and a sophisticated M:N threading scheduler that are all designed to help the programmer express themselves in terms of the problem domain. It competes with Java, C#, and (in some cases) Python or Ruby.

Rust is a language for low-level systems programming. It needs far more complexity than Go because the programmer might need to make extremely specific guarantees about performance or memory layout. It competes with C++ and C.

Is Rust a more complex language than C++? It's not obvious to me that this is true when you add in the third-party tooling (linters, static analysis) required to provide Rust's correctness properties in a C++ codebase.

Is Rust a more complex language than Go or Java? Sure. Obviously. Anyone who says it's not is lying or foolish. But that's not the comparison being made.


The issue with Go is it's a "worse is better" situation: it's easy to go from 0 to a limited amount of productivity, but then it peaks because of the impractical and unreasonable design limitations due to absurd governance decisions. It's too minimal and too limited in key ways out of design purity.

Rust's syntax and verbosity sometimes leaves much to be desired or the convoluted syntax and structures to get interior mutability, but it doesn't matter because of its immense power in the spirits of say ML, Haskell, or C#. Rust can do a lot of what they do and a lot more, and safer. Pony and Idris are barely used but have features Rust doesn't have.

I think too many people poo-poo Rust because they either don't want to understand it or can't grasp its concepts like lifetimes (liveness) or move. It's easy to dismiss what one doesn't understand and stake a claim with an irrational, tribal ego opinion rather than consider the alternatives in the programming toolbox for a given purpose.


>they either don't want to understand it or can't grasp its concepts like lifetimes (liveness) or move.

I think it's funny/sad when I hear C++ programmers complain about this. If you don't understand lifetimes in Rust, you don't understand lifetimes in C++ either. Rust however will prevent you making a mistake. That makes it less complex for you. If you can't get your program to compile in Rust, you might get it to compile in C++, but you shouldn't.


Far from an expert in Go or Rust, but I think the use-case for Go is you need to do something very by-the-numbers and you want it to be easy to maintain and performant.

Once you start reaching for more off-the-shelf solutions, you hit diminishing returns with it as a language.


I too program in rust, however I don't like programming in rust. It feels like C++ on steroids. I consider all the meta-programming and traits mostly read only. There's too many concepts you have to understand and wrappers on top of wrappers. And even when you know what you want to do, it's sometimes a huge fight to get rust to understand what you are doing. Even with unsafe this process can be really annoying and frustrating. Compile times are slow and binaries big.

For example, recent struggle with rust I had. Was storing something pre-computed into a global variable (in C terms think of pointer to binary data). And after it's stored this variable is never mutated anymore, just read, so it's completely safe. Unfortunately it felt almost impossible to get rust to understand this, and it always wanted to copy the damn thing, or preaching me how it's unsafe.

That said, rust is still very much good tool to have in pocket when you have to program high-performance and secure software. But I would not ever program a huge project with it, perhaps use it only in critical parts.


Precomputing state in a global variable works great with once_cell::sync::Lazy (which, like the rest of once_cell, is on track for stabilization). I use it all the time, and I agree that the ability to do that sort of caching is important.


I don't want to come off dogmatically defending Rust, I code little Rust in comparison to JS, and I've done C and C++ for some embedded systems. C is a very different monster to most other languages, I find people defending C to be just as proud and defensive as Rust programmers.

To address some of your complaints: yes, there are a lot of concepts to understand. I like wrappers, I found it crazy that in C, you first declare a mutex_t variable, and then you specifically have to call chMtxObjectInit(*mutex_t mutex) to initialise it [1]. If you forget? UB, kernel panic sometime in the future. I think Mutex::new() is far cleaner, and it's namespaced without arbitrary function prefixes. Binaries are tiny in comparison to JS/Python with deps, they will be larger than C. Compile times aren't that slow and you can't make extra language features happen out of thin air.

In C, I've found that it's commonplace to do a lot of clever and mysterious pointer and memory tricks to squeeze out performance and low resource utilisation. In embedded, there's usually a strong inclination to using "global" static variables, even declaring them inside function bodies because it "limits the visibility/scope of the variable". Not declaring a static variable inside a function is what knocked a few points off my Bachelor's robotics project.

I personally don't like this. It puts a lot of pressure on the programmer to understand the order of execution, and keep a complex mental model of how the program works. Large memory allocation, such as a cache, can be hidden in just about any function, not just at the top of a file where global variables are usually defined.

It sounds like what you're trying to accomplish is inherently unsafe, hence the "preaching", as in it requires the programmer's guarantee that 1) the data is fully initialised before it's accessed and 2) once the data is initialised, it's read-only and can therefore safely be accessed from other threads. C doesn't care, it will let you do a direct pointer access to a static variable with no overhead. Where's the cost? The programmer's mental model. I haven't tried, but I imagine that Rust's unsafe block will allow you to access static variables, just like in C with no overhead, effectively giving your OK to the compiler that you can vouch for the memory safety.

Rust solutions: lazy_static crate (safe, runtime cost in checking if initialised on every access), RwLock<Option<T>> (safe, runtime cost to lock and unwrap the option), unsafe (no overhead, memory model cost and potentially harder debugging), extra &T function parameter (code complexity cost, "prop-drilling", cleaner imo). On modern hardware, the runtime cost is absolutely negligeable.

Why would you not want to use Rust for a large project? This seems a bit contradictory to me. The safety guarantees in my opinion really pay off when the codebase is large, and it's difficult to construct that memory model, especially with a team working on different parts. Instead, you overload that work to the compiler to check the soundness of your memory access in the entire codebase.

If you like C, by all means keep on using it, I enjoyed my forray into C, it's simple and satisfying, but would much prefer Rust, after spending a lot of time tracking down memory corruption. Rust's original design purpose was to reduce memory bugs in large-scale projects, not to replace C/C++ for the fun of it. We usually have a natural inclination to what we know well and have used for a long time. Feel free to correct me if something is wrong.

1: http://www.chibios.org/dokuwiki/doku.php?id=chibios:document...


The point was none of those rust crates worked, or required you to use mutex in the end which the solution would not actually need (not zero-cost abstraction). I would've fine using unsafe, but even with unsafe it felt like I was fighting the compiler. I would just write this particular function with C, or use the lower level C FFI functions instead.

I stand that rust is more fun to write when you work higher level, treat it higher level language, where you'll have less control over the memory model of program. It all starts breaking down and you need to become a "rust low-level expert" when you want to work closer to the memory model (copy-free, shared memory, perhaps even custom synchronization / locking models ...). It does make sense, but in my opinion figuring out how to map your own model into rust concepts is not trivial, it requires lots of rust specific knowledge, which will take a long time to learn IMO.

When unsafe was marketed to me, I thought it was a tool I could use to escape the clutches when I'm sure what I'm doing and don't want rust to fight me, but sadly it doesn't work that way in practice, but the real way is to actually write C and call it from rust.


Just for fun, I tried the static variable approach for myself. I have to agree with you, it's really hard. I gave up after half an hour. Rust doesn't seem to like casting references to pointers, which I understand, as I don't think there's a guarantee that they are just pointers. A &T[], for example, is a fat pointer (two words, also encodes the count). I think the correct approach here is either accept runtime overhead, or pass a context to each function as a parameter.

I also agree with your other statement. I think Rust tries to abstract a lot of behind into its own type system, such as Box<T> for pointers, whilst keeping it relatively fast. C is definitely the right tool for the job if you want direct memory access, I also think this is a relatively small proportion of people, working on OS, embedded systems or mission critical systems such as flight control/medical equipment.


For your issue there are two ways to achieve it:

1) Use lazy_static crate 2) Use a arc wrapped rwlock


Neither worked, it always complained about Copy trait, and no I don't need a mutex for this. I eventually got it to work with few bits of unsafe, but then I gave up when one of the apis I was using captures mutable self, and after that I couldn't force rust to move the damn thing. I decided to not cache anything at all in the end. But it just shows how some trivial things can be nigh impossible in rust, or less efficient. (Also I had to wrap the thing I was caching into yet another struct just to give it a dummy Send and Sync trait)


It's a RWLock. Not a mutex. As long as no one is writing to it, readers are not blocked.


I know, but the RWLock itself also did not work. Complaining about safety and the only solution seemed to wrap the value itself into mutex inside rwlock. If I still had to go back to this, I would just write the function in C and go from there. Rust is productive when you can work with the highest level of abstraction and when it can copy values freely. When you want to work with copy-free or shared memory, is when rust becomes special kind of hell, and you have to dwelve deep into the depths of rust to get it to do what you want. Somehow the meta-programming is even more unreadable than C++.

I also get slight PTSD whenever I have to write `new`, not sure if the method will allocate or not behind the scenes. This is one of the things zig gets right I think:

- No hidden control flow.

- No hidden memory allocations.


I feel your pain as I started to learn Rust recently (again) and found the same problem, coming from C.

In my case, after fighting for literally days, the crate OnceCell did the job (kind of), but I felt awkward retorting to an external crate to deal with a simple static-global-initialize-once-never-touch-again piece of data.


Initializing global, static state isn't simple at all -- C++ is a prime example for how a bad design for static initializers can go horribly wrong.

At the very least, you need some sort of locking mechanism to deal with the potential issue where multiple threads try to initialize the variable concurrently. And you need some poisoning mechanism to prevent panics and reentrancy during initialization. Rust makes you realize "oh, yeah, I need to think about these too".

In any case, OnceCell is a very reasonable way to do this and is on track for stabilization.


I agree, but I have to say that not all the scenarios are the worst case scenario, and sometimes initialization is just "simple", like it was in my case and in other thousand cases I've worked in my life (I do embedded C mostly single-threaded).

This application was also single-thread (a command line utility to parse a text file). I now understand that Rust cannot guarantee the safety of initializing global variables so it makes me take the long road, but it could be great it could infer better about each particular case and enforce only when required.

If I can shoot myself in the foot with C then Rust would be kind-of clamping down on me and nailing my feet to the ground. At the end of the day, it hurts more or less the same.

That was my experience as a rookie with Rust in this particular case.


Yeah, Rust is built from the ground up for programming at scale. One of the consequences is that it makes you think about thread safety from the start. (Imagine if you're on a large team and not everyone is aligned on whether some part of the code is thread-safe or not.) Some things are harder, but the benefit is that you can often make a program multithreaded, and many times faster on real workloads, with just a few minutes of work (an experience that's unmatched in all of programming).

One of the ways Rust is successful at scale is that the intraprocedural analysis is sophisticated, but the interprocedural analysis is deliberately quite basic. It's not quite in keeping with that to do things like selectively enforce bounds on global variables.

For use cases like the one you mentioned, the general strategy Rust wants you to use is to pass around a context with your data inside it (and the data could be in OnceCells if it's lazily fetched). This is also a more testable design. Global state is meant to be used sparingly.


May I suggest you take some time to understand the concurrency and memory management parts of Rust a little more to figure out where your program is falling.

I agree it's not an easy language to pickup, but the use case you mentioned is a pretty common one and there are several ways to solve it. For example, any kind of config reading from a file needs it. Similarly, connection pools for DB etc are shared this way so its not an uncommon use case in day to day usage of Rust.


They need atomic operations which can still stall most of your massivly parallel machine.


Yes - Rust can be complicated. However it doesn't have to be. There's nobody forcing you to use Pin. Even async code doesn't do, if you just stick to using library functions, don't write any Future implementation manually and just stick to async-trait.

There's even nobody forcing you to use async code. In fact it might be the wrong choice for > 99% of use-cases, since it adds complexity and limits on which functions can be called in certain places, adds limitations like not being able to perform recursive function calls, and can even lead to higher latencies (no preemption) and more memory usage (for `Future` state allocations) than the boring synchronous versions.

I've worked together with multiple teams building > 30 applications/services in Rust and had seen even > 100 engineers in those teams picking up Rust successfully. So for me there's more than enough datapoints that it's complexity is manageable after an initial rampup period of 1-3 month.

To be fair: What I definitely haven't found is engineers outside the Rust core group that really understands how async and Pin work - usually people just put async in function definitions and .await where the compiler asks and assume that will do it's job - which is mostly good enough. So yes, I definitely second that those details are hard. And even I - as someone who contributed to async/await standardization and was active in the async working group am giving up mentally in the new discussions around Pin/poll_fn safety. But as I said earlier - knowing Pin is not necessary to write programs successfully in Rust.


Yes - C++ can be complicated. However it doesn't have to be. There's nobody forcing you to use templates......


This is true, though. You can write most programs with simple use of types as template rags for containers, and defining only simple templates, where you just abstract over the given type itself in a “find and replace” way (no type traits, sfinae, template overload / recursion stuff).


C++ can be unsafe. However it doesn't have to be. There's nobody forcing you to use pointers...


> There's nobody forcing you to use Pin ...

... in your greenfield solo project with no legacy code or third party contributors.


> There's nobody forcing you to use Pin.

This kind of argument does not work in community and professional projects where you do get forced to use the features that your colleagues or predecessors have used in the codebase.


In that case you hopefully have colleagues that are able to help on those questions.

And if predecessors have moved on, teams should typically be empowered to make changes to the codebase which make the code understandable and maintainable for them (but sure - I am fully aware about that these changes are not easy to get prioritized in a business sense).


The problem with go is not it's simplicity, it's it's inconsistency. As a small-minded programmer my hobgoblin is tripping over having to remember that X works this way but not Y, which looks vaguely like x.

It feels like the designers of go tripped over themselves to make things simple for some definition of simple and then ran into some feature they wanted and then just bolted the feature on without thinking about whether simplicity/consistency tradeoffs were a thing.


Do you have more examples? I’ll admit that Go is full of weird quirks, but nothing feels bolted on. Generics were agonized over for so long because they had to fit it within the existing language


Here’s one: The fact that contexts are used extensively in the standard library but that there’s still no way to cancel a write to an io.Writer. The only way to “cancel” a write on a TCP socket is to set the socket’s “deadline” to some time in the past from another goroutine.


I agree that this is a PITA, but this really is more of a growth pain than an initial design flaw. Contexts have appeared way after the Go 1.0, when the entire ecosystem has already gotten used to io.Reader and io.Writer.

If you want a much more annoying example of inconsistency in the initial language design, look at the behaviour of nil and closed channels:

- sending data to a nil channel blocks forever;

- receiving data from a nil channel blocks forever;

- sending data to a closed channel causes a panic;

- receiving data from a closed channel is fine: you receive the zero value, forever.

I can understand the logic behind the last two (sending to a closed channel is an error in program logic, and closing a channel is a common idiom for broadcasting completion of a task to several goroutines), but the first two are just a massive footgun, and, in my opinion, should cause panics instead.


Agreed on that one. Contexts definitely feel bolted on and unnatural. The sql libs for example all have 'Do' and 'DoContext', where 'Do' is any one of the operations you'd typically use.


Rust is full of weird quirks that defenders explain by saying that "the compiler isn't smart enough yet." They often seem like consequences of a "compiler-defined" implementation. One random example -- this compiles and runs:

   #![allow(unused)]
   pub fn main() {
     let mut x = vec![1, 2, 3];
     let y = &mut x;
     let z: &mut _ = y;
     println!("{:?}", y)
   }
This does not:

   #![allow(unused)]
   pub fn main() {
     let mut x = vec![1, 2, 3];
     let y = &mut x;
     let z = y;
     println!("{:?}", y)
   }


In my experience, this quirks don't appear in real life (at least unless you do bunch of trait metaprogramming). One exception is async code, since it stretches the type system you are far more likely to enter some obfuscated lifetime or trait error.

That being said, I was quite surprised this doesn't compile... (I understand why, but I expected the compiler to have all required information to perform reborrowing).


JSON marshalling is a big one


In which way?


Cherry picking an instance of complexity is not a particularly fair way to judge a language. It would be like explaining how to encode the lambda calculus in C++ templates and then claiming C++ is too complex.

As far as I have read, Pin has never been a satisfactory thing but a necessary one. Even recently Rust has been trying to decide how to really handle unsafe mutable pointers in a way that COULD be safe. I'm surprised you didn't bother to use that example, as it is much more convincing in my opinion.

But that gets to the point, not all of Rust is simple (Indeed, most novices will exclaim it is way too complex for merely having a borrow checker) or elegant, but that does not prevent the average use case from being so (the exact situations that you claim to want to use Go in, why would you be using two nested Pins in any normal situation?). Moreover, Rust is actively evolving, and we can hope that these rough edges do get better with time.


Yes, it's not a fair comparison. More like a wake-up call. Rust can get pretty complex. Especially if you're using async and then you need pin.

Like c++, languages that strive for zero cost abstraction seem to put a large cost burden on the developer in understanding it.


Question: How much of Rust’s type system complexity is due to async? It seems Pin is mostly related to async and the official examples for GAT are for async.

Given the complexities of async and how other alternatives such a Java Loom’s virtual threads, in about 10 years I am not sure that we won’t see Rust going all in on async as a mistake.


> I am not sure that we won’t see Rust going all in on async as a mistake.

I'm about a month into Rust, but Rust didn't go all in to async; it only went halfway in, which is why some functions are async and others aren't.

Erlang is all in on async/green threads/tasks. From what I gather, so is Loom. There's no special way to write async code, you just write regular code and it works; for Loom you just spawn your threads a little differently. For Rust, you have to be somewhat careful about what you do or don't do in a task, and you have to write .await everywhere.


Erlang isn't async. The actor model is great because you only have to think in single threaded mode. And with immutability being a core language feature, data races and other concurrency issues are literally impossible.


It's got async messaging, so it's not not async either. It's also got the trick that every function call is a potential premption point, and immutability means you can't go very long between function calls.


The GAT complexity is due to async working without extra per-task allocations. If you allowed a heap allocation per each Future (like e.g. JS does for each Promise), it'd be pretty simple, but Rust goes to great lengths to avoid heap allocations.

Rust can merge the entire call tree of async execution (which can be as large as the whole program if async recursion isn't used) into one struct that represents entire state space needed from start to finish. This struct has a static type and a fixed size known at compile time. Describing that type in the type system, including all the generics and lifetimes, is pretty complex.

Pin is a bit of a technical debt. It has been invented to ship async/await without having to first add non-moveable/self-referential types to the language. It is more clever and clunky than a first-class language feature could be. OTOH async/await has been production-ready for a few years now, and non-moveable types are still not a thing, so the trade-off was worth it.

In practice you don't need to even know about Pin if you're just using async/await syntax. It's needed only for lower-level plumbing, like custom runtimes or clever low-level primitives that can't be expressed in normal async code.


Since we’re comparing Go and Rust, I think Go really nailed it with goroutines and channels. It’s much easier to get your head around than async.


I don't think this is right. To me at least JS's promises and C#'s async/await are as easy as Go's goroutines. The problem with Rust is the effort to make everything static and without heap allocations, which is understandable but enforce really hard-to-grasp design. On the other hand, you can get rid of most issues of async even in Rust by boxing.


Yes, I think async is where a lot of the complexity comes in.


I get the sentiment, but one thing that probably needs to be underlined here is that one only needs to face that complexity if you really want to, however. Otherwise you can just wrap everything in Arc, Cow and similar helpers and call it a day.

Even lazy Rust code can still be incredibly performant, testable and fault tolerant. The lower level you go, the more you need to know about Rust and how what it does maps to memory, because it is a systems programming language and if you wanna write the equivalent of a kernel in it you will need all power you can get. The point here however is: you rarely really need to go that deep if you are writing whatever you'd be writing in Go.


> Otherwise you can just wrap everything in Arc, Cow and similar helpers and call it a day.

… which is how we got slow, bloated legacy C++ codebases today, should we make the same mistake with Rust? If you are wrapping everything in ref-counted smart pointers it might probably be better to just use a garbage-collected language like Java or C# instead…


As I said: It depends what you are writing. If you write big libraries and major codebases that are meant to represent the foundation for something big, maybe you should not follow my advice. If however you are writing a small tool where good enough is indeed good enough (and still 10 times faster than equivalent python code), who cares.

Rust can go anywhere inbetween counting every bit and byte and doing no such thing at all. My point was that counting every bit will probably be not be the way 90% of the programs will be written if they are written in Rust (mostly due to the fact that programming in such a way is both much harder and takes longer — independent of the programming language).

So if someone who does not count bits programs a software in a field where it would be paramount to do precisely that, then this is not the fault of the programming language, but the fault of that person, their organisation or whatever.


The thing is that languages like C++ or Rust has the potential to become something even slower than Python if you don’t know what you’re doing. For example, (the example is C++ but you can say the same thing about Rust):

https://stackoverflow.com/questions/9378500/why-is-splitting...

Same with smart pointers: Languages like Java and C# have highly tuned garbage collectors that can be faster than reference counting in most situations. The rationale mostly deployed for using RC isn’t simply just raw performance (it can be slower because of the refcount read/write for every dereference), but predictable performance (you don’t need to worry about GC lag spikes, which is important for real-time applications. But even that advantage seems to be disputed.)


It's an giant exaggeration to blame "slow, bloated legacy C++ codebases today" on smart pointers.


I agree. I've played with Rust a bit and I program with Go for work. Go is much much more productive for me. Yes, it has rough edges, but it's good for getting stuff done. I feel like everything I've done in Rust would take half the time in most other languages I know


Yes, Rust has very real unsoundness issues wrt. Pin<> and, more generally, interactions between unsafe and safe code. It's still nowhere near as complex as C++, which is a rather popular language nonetheless. Rust also has an editions system, so any such warts can be fixed whereas C++ is totally unfixable (other than via a clean start, see Carbon and Herb Sutter's new surface syntax).


99.99% of Rust programmers don't have to care about this issue, it's just nailing down some spots where the safety invariants can deliberately be broken.

The issue isn't actually async here, it's #[fundamental]. The holes it pokes in the orphan rule system lead to many unintentional outcomes.


BTW I think the fact that in Go, your program can change out from underneath you based on whether an array gets realloced is so much more disturbing: https://utcc.utoronto.ca/~cks/space/blog/programming/GoSlice...

It is genuinely shocking to me that people just live with this. This is completely inexpressible with safe Rust, as it should be.


the way arrays work and that they can reallocate on append is clearly visible by the fact that it returns the pointer to the new array.

It is something you learn very early, because the syntax is indeed a bit peculiar, and kind of a footgun. However it implicits teaches you to not try to play with the memory backing the array (which is something go programmers are very happy not doing).


Personally, I would simply use a language where such footguns just don't exist.


That was my first conclusion with that language after my first attempt. However on the second one, much later, i came to realize this language is an absolute marvel of carefully picked tradeofs, always in favor of simplicity and maintainability. Which for industry project is a must. Go footguns are pretty easy to spot and look « stupid » ( in the sense that other languages solve them elegantly). In exchange, the really hard ones (such as overly complex concurrency patterns, or codebases made of layers upon layers or crappy abstractions) are extremely rare and need constant fighting against the language.


I'd say that in my professional experience, Rust is probably the best "at scale" language that exists today.


Nim is far less complex, but isn't as popular. The community is working quite hard to change that though.


I really like Nim. I was able to build some non-trivial things in it after just a few hours of googling and reading documentation (supplemented with a few questions on IRC). I do sometimes worry though about investing in a language that, due to its lack of popularity of "catching on", may never have the richness of tooling and packages/libraries needed to use it in the context of a larger project or larger team. But perhaps that concern is misplaced...maybe it's a social thing where you feel a bit out on your own when there isn't a large community coalescing around the language.


100% agreed, to the letter.

My recent observation is that programming languages are a bit like warfare. We are drawn to discuss syntax and features, or machine guns and fighter jets, but what seems to trump these on any practical timeframe is logistics.

When coding up a new project, I need a language which gets a lot of mileage, so the bugs are stamped out. It needs good tooling so I'm not stuck making it myself. It needs robust package support, so I can just type the project-specific code. Oh and the language can't be unworkable, and vaguely needs to fit technical spec.

It's a sad chicken-and-egg situation, but users are often not in a position to take the risk.


I've had the same concerns. I made a Nim web framework, but I don't think anyone else has written a web app with it except for myself.

My thinking now is to combine Nim with other languages and their frameworks. For example a NextJS/React front-end with some GraphQL servers in Nim. Alternatively Nim and Python mix quite well.


What's your web framework?


Last time I remember fighting with Pin was when working with async in Wasm (a single threaded environment). It seemed to me that the complexity justified the cost. Go, for example, is a no-go for Wasm. As a result, I don't think the two languages are comparable.


>Go, for example, is a no-go for Wasm.

No? Google suggests it is possible


I've done quite a bit of development in C++. Now learning Rust for fun. My impression is that there is too much unnecessary complexity in Rust language and its type system that it gets in the way when you try express your thoughts in code. While writing Rust code I can't shake the feeling that I can write functionally the same code in C++ (sans the memory safety guarantees of course) without ever struggling with problems like the one described in this post. In fact C++ now starts to look like a simple language to me. I guess everything is known in comparison.


> But Rustaceans kind of like to laugh at Go, because it's not as expressive or elegant a language by comparison.

Ironic because safe Rust can't express many valid programs, like those with cyclic data structures.

> Some parts of Rust are starting to remind me of the horror I ran from with C++.

Rust is most certainly designed for C++ programmers. C is all about minimalism and simplicity. I think there is a niche for a C like language with memory safety that retains the simplicity of C. Rust isn't it.


>C is all about minimalism and simplicity. I think there is a niche for a C like language with memory safety that retains the simplicity of C.

Isn't that the aim of Carbon, the Google-backed language whose specification was announced a few months ago? I might be wrong.


Is there a subset of Rust that fills that need, like how there's a subset of C++ that's still reasonable?


Use a `Pin` type that doesn't include a generic. It's how I avoid this. Works out quite well! (Including for IO pins; I've seen some `Pin` abstractions that use 3+ layers of nested traits!)

Figure out which parts you like, and use them.

edit: Assumed you were referring to embedded GPIO pins. It seems this is something else.


I recently heard a similar sentiment shared by the developer of T2 Linux, although the issue he focused on was the relative complexity of bootstrapping the Rust toolchain: https://youtu.be/T83CGZz2HTc


I think I need someone to explain Rust async to me.

I think I understand that await causes the half of function to be rewritten into a state machine that returns immediately to caller and the promise object implements the rest of the control flow.

I don't understand waiters, pollers.


Waiters and pollers are artifacts from the fact that Futures in Rust are lazily scheduled, unlike Javascript, for example, where Promises are eagerly scheduled and executed.

When you don't want to block your process and program in a reactive, non-blocking way, which is what futures and promises are an implementation of, what you are actually defining is something like this:

> "our process will do some synchronous work, then it will need to stop at point X to wait for asynchronous data"

> "after that data arrives, we can restart our process where it stopped, at point X, and do some more work until it hits point Y, where it will need to wait some more asynchronous data"

> "after that also completes, we can restart from Y, and process until the end of the function"

However, the problem that arises is _how do you store the variables that are being used in this function_? Particularly those values that would be saved in the stack, because by returning the function, the program needs to pop all those stack frames so that the caller can continue executing.

There are two ways of accomplishing this. The first way is to use stackful co-routines, aka threads. The OS does this, but platform threads have very high overhead, so many languages avoid it. You can also use the OS's reactive APIs or thread pools and implement your own scheduler with virtual threads on top, which is what Go or Erlang (and soon Java) do, but then you can't avoid that runtime overhead, which Rust and C++ don't want to have, or maybe you want a more lightweight model (which Javascript wanted).

In that case, the other approach you can do is explicitly represent the processes' variables and state as a structure in the heap, and create functions that operate on those structures. In Javascript you can easily do this (you can capture the values in a closure), and in Rust it is more complicated but doable, and in general you can do this in most languages.

The downside here is that, instead of being able to simply code your process in a straightforward way and have it block implicitly, you need to explicitly code the synchronous steps between each asynchronous wait, which results in either callback hell, confusing functional styles such as continuation passing, or future combinators and callbacks.

What async lets you do, then, is have the compiler write those state machines for you. If you mark your function as async, the compiler will create the closure for you, the state transitions for you, deal with waiting for nested futures for you, and so on, and you can just write the code in the 'dumb' synchronous style. This is common to all the async implementations, from Rust to JS to C#.

However, at some point, you need to actually drive these state machines. You need some kind of scheduling that is able to receive external events, get the state machine that is waiting for it, and execute the next state transition. Driving these state machines is done using those waiters and pollers. Rust exposes these APIs, because it does not bundle the executor, and so it exposes those methods so that async runtimes like tokio can be built against a standard API. Javascript, on the other hand, still needs to perform those tasks but, because it has a runtime of its own, implements an execution service implicitly, and so the external interface of Promise does not need to expose that concern.


Thank you for your effort and time for explaining this to me.

I still need to learn more. I think I'm not sure how someone would write their own runtime with pollers and waiters. Those are the only APIs provided by Rust.


That's a great explanation, thank you!


a minor point, but perhaps coroutines, stackful or not, should be distinguished from threads.


I feel it's because it's a very new kind of programming model. But the lofty aim with which Rust started, the complexity is essential and not accidental. But no doubt it's complex.


Rust have nice ideas but the execution is very poor

- noisy syntax

- concerning and very bad compiler performance

I can definitely see Swift replacing Rust in the short term, Apple seems to be committed


Rust and Swift are not really competing in the same space. And nobody trusts Apple for cross-platform support.


> Rust and Swift are not really competing in the same space.

Rust is trying competing yes, Swift is not trying to compete, they use it, they do what they have to do [1]

What happened to Mozilla's Rust plan with Firefox? so far, no bueno

> And nobody trusts Apple for cross-platform support.

Swift supports a wide range of targets and architectures [2]

They officially provide release build for all major OSs [3]

[1] - https://github.com/apple/swift/blob/main/docs/OwnershipManif...

[2] - https://github.com/apple/swift/blob/aff7c14d92fd241aff85ba4c...

[3] - https://www.swift.org/download/


Yes, Rust is complex. It was designed to be faster and safer.


there is no horror in c++. it is a better investment of your time IMHO to learn patterns and best practices and learn how things work at a low level instead of learning a language like rust which attempts to hide difficult work from you and make your life easier. in the end you will pay for that when you finally understand there are no shortcuts and you really need to understand how the machine, memory management and operating systems work. rust cannot fill that void.


Rust is a systems language, like C++. I don't think your comparison is fair. Also I've done a lot of C++ programming over the years, and I won't willingly go back to that. It is a horror in my opinion.


The selling point of Rust is that it hides complexity from you and gives you "safe" memory management. That comes at a price.


Rust doesn’t hide complexity. It forces you to acknowledge it and handle it.

Rust largely won’t let you write the wrong thing, but you have to fully understand the nuts and bolts to know what it will let you do.


No, the selling point of Rust is that it exposes the full complexity of systems while providing safe memory management. Being able to dereference a null pointer without a clear override isn't complexity, it's just bad language design.


Rust doesn’t handhold you for anything low-level. It’s just that Rust hides all that complexity beneath Unsafe Rust, which is an eldritch language that no one quite knows all the rules yet (including things related to undefined behavior…) I hope the MiniRust project (https://github.com/RalfJung/minirust) succeeds in writing a formal spec of it someday.


ok, so you just confirmed rust does some stuff behind the scenes for you. and that comes at a cost.

what is more important is to understand how to architecturally build software and achieve performance and maintainability and so on. i rather spend my limited hours becoming better in a language like c or c++ which is not going away anytime soon than learning syntax, a new way of thinking and a package management system i don’t need.


The performance ceiling of C++ is substantially lower than that of Rust. For example, it is roughly impossible for humans to use `restrict` with C or C++, while it's a completely normal part of writing Rust (and LLVM optimizations have finally been turned on: https://github.com/rust-lang/rust/issues/54878)

Also see https://github.com/rust-lang/rust/pull/103070 and https://reviews.llvm.org/D136659, which start introducing further Rust-specific optimizations.


Go doesn't have any .forEach, .map, .filter methods on its lists.

This alone to me is mind-blowing.


Go and rust are actually not that far away from each other on the logarithmic scale of complexity.

I recommend that you broaden your horizons by studying something really complex like Haskell and something really simple and stripped down like Lua.


I've used Lua, it's nice and simple like Go.

I tried Haskell, but I found it unreasonably complex to be practical. I only learn programming languages if I have a practical reason to use them, so I never picked it up again. Perhaps you have a point, everything is relative.


Try writing a programming language with Haskell. Learning Haskell changed the way I view things and most other languages feel cumbersome in comparison.

There’s something almost magical about writing software in a language like Haskell, when you have defined good and elegant abstractions, it’s as if you peer into the matrix and touch the divine.. so to speak.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: