I think the cognitive overhead of Rust's memory model outweighs its benefits only if all of the following are true for your application:
1. Your application can afford the cost of GC and a runtime.
2. Your application does not use concurrency, or you are willing to tolerate data race errors, or you can eliminate all data races with a runtime race detector (due to extensive test coverage or similar).
3. The help that compiler-enforced immutability affords to your ability to reason about the code isn't important enough to you to justify the cost.
I think that there are a lot of apps that meet (1), (2), and (3), don't get me wrong. But I also think there are a good deal of apps that meet (1) but not (2) or (3), and they can benefit from Rust as well.
For example, these days I write a lot of data processing stuff that is closer to scripting than anything else, and is usually "one-offs" that get scrapped afterwards. But the code needs to be super fast, talk to databases, parallelize across multiple cores and nodes, which rules out many languages. On the other hand, safety is the absolute least concern. I've been using Ruby when it's not performance-sensitive, and Go when it is, and while Go is significantly less pleasant, it gives me decent performance, concurrency and access to libraries. Nim would probably also be ideal, if not for the lack of libraries. But Rust just seems like a terrible choice for this stuff because of the syntactical overhead of its abstractions, in particular memory.
That limits its usefulness. A single language for all my work would reduce context switching and allow code reuse.
In general, my impression is that Rust is good at the complex, heavy stuff, but it doesn't scale down.
In that case, you may be interested to know Rust permits unsafe code:
"Safe by default" saves a lot of different kinds of bugs.
In general, I think if you're looking for "one language to rule them all", that can scale up to the highest performance and down to one-off apps you would write in Rails, you will be disappointed. Even if you don't care about safety. That's because, no matter what people say, static and dynamic typing is a tradeoff. Rust is more statically typed than Go is. Go is more statically typed than Ruby is. Which you should use depends on your circumstances.
A lot of language designers and enthusiasts, from all communities, believe there's a "sweet spot" of language design where your language can scale up or down at will. I don't believe it. That experiment was tried with Lisp and Java and failed. Multiple languages are here to stay.
At the end of the day, all I can say is: If you're happy with Go, use Go. Rust was never designed with all use cases in mind, and I don't see that fact as a failure at all. I see it as just acknowledging reality: trying to make a language that scales up and down indefinitely to all use cases is chasing the impossible, and language enthusiasts who believe their language is the one language that can do that are all mistaken.
I actually do believe there exists a sweet spot. I just think that during design, too little thought it given to the scalability of languages. And I don't think it's that difficult.
For example, type inference is something of a game-changer, capable of making a statically typed language feel nearly like a dynamic one. Yet it's only recently being applied in mainstream languages, despite the theory not being particularly new.
I also think too little thought it given to the cognitive overhead of syntax. Wirth famously designed his grammars to fit on a single screen. Go (which, like its earlier incarnations Alef, Limbo and Newsqueak, is very much influenced by Wirth) gets this syntactical simplicity right. I thought early Rust syntax showed a lot of promise, but I'm unhappy about the current jungle::of::punctuation.
> If you're happy with Go, use Go.
To be clear, I'm not happy with Go.
Fixing these issues in Nim would make it much more like Rust. In particular, it would have all the cognitive overhead you're criticizing Rust for.
If you want to show that it's possible to achieve all of the use cases of Rust without the cognitive overhead, just citing Nim won't cut it. You need to (a) explain how to do memory safety without garbage collection (reference counting being, of course, a form of GC) without introducing new concepts like Rust does, (b) explain why everyone who thinks they can't afford GC is wrong, or (c) explain why memory safety isn't important even for security-critical software. I don't think (a) is possible, and I don't think (b) or (c) are correct.
This is not a claim about safety.
Every time I have benchmarked something, Nim has turned out faster than Rust - or anything else, for that matter. Yet, Nim does have a GC. But since one can allocate on the stack, and it is per thread, it is not a bottleneck.
About safety: you can tune it with compile time flags, or even annotate per function what checks to leave.
So, please, stop spreading FUD about Nim.
- Just being able to allocate on the stack is not enough. You need to be able to freely make pointers to those stack objects. The ways to do that are to either (a) accept the lack of safety and admit dangling pointers; (b) use an imprecise escape analysis; (c) lifetimes and borrowing. Every choice has costs.
- Safety with runtime checks is not equivalent to what Rust does. Rust provides the safety without runtime checks. That yields better performance than safety with checks.
I'm not intending to spread FUD about Nim specifically. The more interesting question is whether it's possible to have a safe language with the same performance as C/C++ without any cognitive overhead. I strongly believe, after working in this space for over five years, it is not.
That's not to say Nim is a bad language. There are lots of things I like about Nim. It's just that it won't escape the basic tradeoff between cognitive overhead and safety/performance.
From everything I have read (I've followed the development quite closely since it was first announced), Rust's safety comes at a significant cost to the developer, in ways that prevent it from scaling down.
You guys are doing a terrific job, don't get me wrong. But from my perspective this looks like a costly design mistake. Looseness is the key to being able to scale down, and strictness is its antithesis. Rust just doesn't offer a forgiving mode that approaches the kind of ergonomy you get from a less strict, garbage-collected language.
I think Nim made a better, more flexible choice, in offering a kind of graduated safety — you can restrict functions (for immutability, memory safety, exception handling, etc.), but the default is wide open. In my experience, this correlates to the top-down structure of programs: You want the foundation (runtime, stdlib etc.) to be as hardened as possible, because you can afford to spend lots of time on it, since it's the most used code that needs to be the most stable over a longer period of time. Towards the top layers everything should be able to decide whether it wants to be sloppy (fast to develop, but unsafe) or strict (slow but guaranteed to be correct).
Because it's impossible (without sacrificing Rust's basic goals). And Nim doesn't prove otherwise, because Nim doesn't share those goals.
You are framing safe vs. performant as a fundamental tradeoff. It is not, and that is the entire point of Rust. Nim does not allow you to remove that tradeoff. Rust does. That's why Rust is suitable for different projects than Nim is.
In other words, Nim's safety features are not equivalent to what Rust has. They have a performance cost in Nim and don't in Rust. So in order to make your "sweet spot" of a language that scales up and down a reality, then you're going to have to either convince me that I should accept the performance overhead of Nim's features or that I should give up safety.
I don't believe that it is possible to combine a loose, GC'd mode with a memory-safe, non-GC'd mode in the way you want. The best you can do is what Rust already does, with Rc and RefCell. What people observe with Rc and RefCell is that you do indeed get back your freedom to make aliases and mutate at will, but you still have to know how the borrow checker works. So Rc and RefCell get little use in practice, because once you know how the lifetime system works you might as well just use the ownership system instead of paying the cost of Rc.
Even if it acquires ownership and borrowing, Nim will not be able to escape this dilemma, barring some fundamental research advance.
People forget that the learning curve for correct memory handling in C is steep.