These worldwide anti-competitive lawsuit and penalty news are getting far too frequent that it makes me wonder if any of it is working at all. Meanwhile, there are some talks about some big impactful interventions. But again, they have remained just that - talks. Am I paranoid to think that these are just diversions with no real consequences? If not, why are we not seeing any improvement on the ground?
I think it already has. It just isn't very visible to us. Rust becomes easy (especially the borrow checker) once you know how the hardware works. This is unfortunately true for even non-system programming in Rust. However, those who deal with hardware - OS devs, web engine devs, DB engine devs, etc - can see the problem and appreciate what Rust offers. That's why we see big companies like Microsoft, Google, Amazon and even Apple adopt Rust quickly. It's just not visible to us on the surface.
That 90% is moot if you're trying to replace C++. Rust is a better fit than Go. Granted that Go iteration times are better. But Rust is by no means just a low level language. The abstractions in Rust are fantastic. They feel very ergonomic and high level. I think the real problem with Rust is that many people can't come to terms with the borrow checker. It's whole another skill set to know what the BC is trying to achieve, how to solve BC errors and the alternatives available when you can't. Once you know that though, Rust feels very productive - even extremely helpful in resolving problems in advance.
Believe it or not it’s possible, even desirable, to use an appropriate solution to the problem at hand, rather than treating everything as a nail just because you’re find if your hammer.
That argument is selectively and rather condescendingly applied to Rust far too often. In this particular context (both the parent comment and the article), that criticism should apply more to Go than to Rust. Besides, I don't understand the argument of asking everyone to learn a dozen different languages for an 'appropriate solution at hand' when the differences between them don't justify such effort. I don't dislike Go. But it's disingenuous to argue that Rust shouldn't be used where Go can be.
There are more than one way to achieve safety in Rust. Two more, infact - runtime safety and fully manual (unsafe with some additional work). Runtime safety is a very fast way to overcome the resistance offered by the borrow checker. While it does slowdown the code a little bit, that should be pretty fine for prototyping. A more careful use of runtime checks will still give you a program more performant than in most other languages.
Runtime safety still comes with a good amount of complication source-wise, having to spam RefCell/Arc everywhere, and whatever derefs/.get()s they necessitate, and probably a different approach to passing references. Refactoring either that or unsafe away after being done with prototyping isn't trivial, if even possible without entirely rethinking the approach.
Isn't that a bit dramatic? How much extra code or care do you need to implement runtime safety checks? This isn't the first time I hear these sort of arguments either. It gives the impression that a crowd is making up one exaggeration after another to drive the perception that Rust is somehow not suitable for general purpose programming.
It is a bit dramatic, though less so for some approaches to programming. And of course prototyping is very different from general purpose programming.
With Rust, if you really want the borrow checker to actually never bother you, you really do just have to spam the dynamically-checked wrappers everywhere; when in the groove, having to stop and wrap some existing field (or its holder) and all its usages just to be able to mutate it because you previously didn't think you'd need to is just completely awful if you don't find that "fun".
Whereas in C, if you want to mutate a thing, you can always do so whenever with no concerns (whereas using unsafe to mutate a field of a &Thing in Rust is just UB, though maybe you can get away with using unoptimized builds for a bit) and deal with previous code that may have been written with the assumption it wouldn't change Later™ (but, importantly, still allowing properly testing the modified thing, and perhaps the entire project). Want to change the lifetime of a thing? Just move where you free it, no need to ever change lifetime args across a dozen functions. And in Java the most anything may take is removing a "final" on a field.
While the argument is generally correct, Fortran and COBOL aren't in the same league. Fortran is still being upgraded and has some use cases that are hard to achieve in another language. I've seen the Rust subreddit recommending use of old Fortran code with Rust, rather than rewriting it in Rust. Unlike most other languages like C++ and Rust, Fortran kept its scope mostly limited to numerical processing - making it still one of the best solutions to code highly parallel numerical algorithms in.
Fortran isn't surviving just because nobody wants to rewrite Fortran code. Unless you're talking about Fortran77 code.
Oh agreed - Fortran is much more alive than COBOL - but in both cases I think the Fortran had outstayed its welcome. I never saw one of the codebases but the one I did see was Fortran 77 I believe, and there were a lot of gotos and labels.
That's odd! Finding libraries (crates) has been the easy part for me. You usually get the answer directly from crates.io. Even in cases where there are multiple alternatives, it's easy to choose one based on the statistics available on crates.io. And in the rare case where you still can't decide, a web search reveals the frontrunner with detailed articles on why.
Oh, I'm not saying it's difficult. I'm saying that at one point it becomes too much. And if I don't actively work with Rust I end up forgetting and having to relearn in the future.
How is it different from the situation in other languages? Especially the ones like C and C++ that don't have a canonical source registry either? (Not a rhetorical question)
Normally yes but f.ex. my favorite Elixir has a community that very strongly prefers and orbits around singular solutions of thorny problems. You will not find many ORMs / DataMappers in that ecosystem, it's one that has been super hard worked on and nearly everyone accepts it and loves it and contributes to it when the need arises.
Meanwhile in many other languages, Golang and Rust included, there are many ways to do the same thing. That introduces difficulty to keep well up to date.
But I can easily agree this point gets very weakened after you have worked with the language for a certain amount of time and on.
None of the points you make is universally true. FOSS means that there is less of a chance of the rug being pulled from under you in the form of subscription services nobody asked for. This has been a repeated annoyance with one certain company which did that recently for an ECAD tool and an MCAD tool. And, there are enough motivated people who do a ton of work to make FOSS software reliable enough. In fact, MCAD is an exception in that area. We have world class FOSS operating systems, 3D animations systems and development tools in an endless list. Even Blender, which was not very popular a decade ago, suddenly gained recognition. FOSS ECAD is on the way and it will happen some day for FOSS MCAD too. Finally the license cost. The cost of proprietary software - especially engineering and scientific software is exorbitant in countries with better purchase power parity. Much less would happen in those sectors in such places if it weren't for the large repository of FOSS software available to them.
That's their excuse. But I don't want to pay a rent for something that doesn't offer anything new over something I already paid for. I'm okay with paying for news, electricity, water, waste collection or even proprietary software as long as it's one-time. But I don't want to pay a rent to keep using heated seats, security cameras or a software without significant updates. That's just pure rent-seeking. That's legalized extortion in my books. Hence my preference for FOSS software.
It is an extension of match ergonomics, called deref patterns. There's some experimental code to make it work for `String` to `&str` (`if let "hi" = String::from("hi") {}`), but it is not anywhere close to finished. The final version will likely have something like a `trait DerefPure: Deref {}` signaling trait. There is disagreement on whether giving users the possibility to execute non-idempotent, non-cheap behavior on `Deref` and allow them to `impl DerefPure` for those types would be a big enough foot-gun to restrict this only to std or not.
The more complex the pattern, the bigger the conciseness win and levels of nesting you can remove. This comes up more often for me with `Box`, in the context of AST nodes, but because I use nightly I can use `box` patterns, which is the same feature but only for `Box` and that will never be stabilized.
If anyone's just starting out with Rust and struggling with the borrow checker, here are some tips. I have been using Rust since 2013 and have tried these ideas myself and while training others. (C/C++ devs may be familiar with some of these and may skip them.)
1. Give priority to the fundamental semantics of code execution - things like C memory layout [1], function calls, stack, stack frames, frame invalidation, heap, static data, multi-threading/programming, locks, synchronization, etc. Also learn associated problems like double-free, use-after-free, invalidated references, data races, etc. These are easiest to understand if you learn an assembly language. However, if you don't have the time or patience for that, at least focus hard on the hardware and memory layout topics in Rust books. If you prefer instead to learn those by making mistakes without the borrow checker intervening, start with C. (Aside: This knowledge is needed for C and C++ as well, especially C. You can't write large code without it.)
2. DON'T try to memorize the borrow checker. Borrow checker is based on a few simple principles (which you should know), but they can manifest in very complex and surprising ways (same as memory safety bugs). It's not practical to learn all such cases. Instead, check the borrow checker error message and see if you can correlate it to any memory safety problems I mentioned above. While the borrow checker can seem very complicated and arbitrary, it's designed solely to prevent those memory safety bugs. Pretty soon, you'll be comfortable with correlating BC errors to such bugs, without having to worry about how the BC found them. Knowing the real problem will also make it easy to satisfy the BC and avoid fighting with it.
3. Understand the memory layout of data structure that you use. Ref: [2]. Borrow checker errors often require this knowledge to make sense. The same becomes crucial during debugging if you make such mistakes in C/C++. The BC wont even allow you to compile it if you make similar mistakes in Rust. You need it just to get the program to run.
4. Borrow checker wont solve every problem for you. It doesn't have the intelligence to reason it all. There are a few notable cases:
- Data structures with cycles (like closed graphs, dequeues, etc) and algorithms that deal with them. (BC prefers data structures in a tree hierarchy)
- Function calls across an FFI boundary (since the BC can't check that code)
- Valid cases according to borrow checker rules, but rejected anyway due to complicated lifetime analysis. These may eventually get resolved in a later Rust version. But such cases exist.
5. Most of the BC errors can be solved by simple code refactors. But in cases like above, you need to identify them (as a limitation of the BC) and look for an alternative solution. BC is a compile-time safety checker. The alternatives are:
- Runtime safety checks using concepts like Rc, Arc, Cell, RefCell, etc. They will pass the BC checks at compile time. But if memory safety is violated at runtime, it will simply panic and crash. It may also have a slight performance hit due to runtime checks. But this is most often very negligible, given the fact that most other languages are based entirely on such checks (GC, ARC, etc). You don't need to be too shy in resorting to these to get around BC complaints. Many Rust programmers do.
- Manual safety checks using unsafe. If the performance is absolutely important for you, you can use unsafe functions and blocks. Unsafe keyword activates some potentially memory-unsafe language features (like raw pointer de-referencing) that the BC doesn't vet (Note: BC is not deactivated). This is often what you need when you're trying to implement an unavailable data structure or algorithm. This is also the only choice for FFI calls. Rust will not check them at any time. But this usually isn't a problem. Unsafe blocks are often used only for very fundamental ideas (eg: self-referential structs) and consist of no more than 5% of a codebase. If a memory safety bug does occur, it will be in one of those blocks and will be easier to locate and correct. Moreover people convert them to libraries and publish them on crates.io. This improves the chance of finding any hidden bugs. If you need unsafe code, there's probably a library for it already.
To get a better intro, check out 'Learn Rust With Entirely Too Many Linked Lists' [3]
That applies only if you're struggling with Rust. It's as good as any other general purpose programming language once you're out of the fight-the-borrow-checker stage. Structuring or refactoring large applications in Rust is nowhere as tedious as many project it. There are many zero-cost abstractions and other features that even makes it very pleasant.
My first preference for making simple utilities is as a shell script. The immediately next one is actually Rust.
Manually managing memory complicates the program, and makes it harder to change. Every interface written is contaminated with the implementation details of how it manages memory. This has a large cost in terms of development time. As much as you might want to imagine Rust has made every other programming language obsolete, that just isn't true.
Your assertion doesn't match my experience. Besides if that were true, nobody would be using C or C++ for writing general purpose (non-system) applications. C and C++ require the same system knowledge that Rust developers use to satisfy the borrow checker. Even worse, Rust borrow checker will remind you of those rules. C and C++ will just allow you to proceed and crash. C and C++ memory management is even more manually involved than Rust's. Yet people do write normal applications in C and C++.
The only explanation I can think of for the dislike towards Rust's compile time checks is that some people don't entirely understand these rules when they use C and C++. It's possible to resolve simple memory safety issues in C/C++ without in-depth knowledge of hardware semantics. But a complicate bug will easily stump you at runtime (personal experience).
You're diverting from the context of this thread. I'm asking why a developer who is comfortable with Rust borrow checker choose any other language? How is a GC language preferable in any way to Rust for such a person?
In a broader sense, I keep seeing some people asking everyone else to avoid Rust based on an exaggerated account of the struggle with the borrow checker. There is actually a way to get comfortable with the BC. Perhaps beginners should be introduced to those ideas rather than such negative takes.
> I'm asking why a developer who is comfortable with Rust borrow checker choose any other language?
Because there are other languages which don't require you to hold to a strict set of rules to maintain memory safety, as they automate memory reclamation at runtime.
> There is actually a way to get comfortable with the BC. Perhaps beginners should be introduced to those ideas rather than such negative takes.
Regardless of how comfortable you are with it, it imposes a necessary overhead, either in terms of architectural constraints or the runtime cost of reference counting which is greater than that of garbage collection.
> I keep seeing some people asking everyone else to avoid Rust based on an exaggerated account of the struggle with the borrow checker.
The borrow checker is irrelevant here. As you have mentioned, a person coding is C will have to keep to the same constraints which Rust statically enforces. There is clear reason to use a language such as Java over C. Not having to manually manage memory simplifies code and makes refactoring easier. This is true regardless of if your memory restrictions are checked or unchecked. Imagine the simplest example: a data structure containing a string. In Java, you write a class and put a string in it. The data for the string is shared and a simple copy of the pointer will suffice in the constructor. In languages with manual memory management, you have three options: the struct can own the string (or reference count it), in which case it will have to be cloned and performance will be worse than garbage collection; the struct can borrow the string, in which case you'll have to track the string's lifetime throughout the program which could cause issues later; the structure can be generic over owned and borrowed string types, in which case code will be more complicated and generic parameters will proliferate. It should be immediately apparent that the latter requires more architectural consideration than the former, and hence is a greater burden to programmers. Several of the options in the Rust case will also lead to potentially complicated refactoring later, which adds even further cost to development.
TL;DR managing memory manually takes more effort. That's why it's called MANUAL memory management, because it's not automated.
reply