Do you really think this or are you just projecting elitism?
Software engineering != Computer science
Formal theory of computation (finite state automata etc.) was also a higher level elective that depended on your concentration.
I used Haskell for a few years and the main problem with it is that to do anything you need to use Monads, Functors and a lot of complex types. It's a requirement to even write the most basic program. "strange" functional stuff proliferates haskell like `foldl` and `foldr` and a extremely discouraging Lazy computation model that makes it almost impossible to know what the compiler will do unless you have a near perfect comprehension of all the language.
Rust to me is as simple as writing annotated Python or Typescript, with the only addition that a beginner needs to know is that there is a new option that those two languages don't have which is pass by reference or pass by value. The lifetime stuff was really easy to get, "pass a value into a function, can't use it anymore."
The Rust compiler gives extraordinarily useful error messages. I don't know if haskell improved but you can end up with some very strange stuff to figure out.
The only real hiccup I had when learning Rust was that going from `sync` to `async` code had a bit of a learning curve. It was simple for basic stuff, but the problem were that Future's have their own Lifetime that depends on every piece of data used in the computation between `await` calls. This means all your data needs to be Sync + Send if you want to use Async.
I think anyone who's done anything beyond beginner JavaScript is going to understand the difference between pass by value and pass by reference, even if they don't call them that?
function foo1( val : string ) { val = "hi" }
function foo2( val : [string] ) { val[0] = "hi" }
const bar : [string] = ["yo"];
foo1( bar[0] );
console.log( bar );
foo2( bar );
console.log( bar ); > I think anyone who's done anything beyond beginner JavaScript is going to understand the difference between pass by value and pass by reference, even if they don't call them that?
Pass-by-reference is a concept that is unfamiliar to many (most?) modern JS/TS developers, as the common idiom is that you "return" a new object or value at the end of your function.If you want to update a value in an existing object, you pass it by value and return a clone of it with the updated values
Performance doesn't matter, you can do the most pants-on-head-stupid thing you can imagine, or have your frontend page/component be re-rendering 20 times on every state change, and it makes no perceptible difference. (This has been my experience)
It wasn't until I learned C that I realized you might even program in this fashion (somewhat to my horror) where passing variables to functions could mutate them
If you pass an array to a function in JavaScript, then any changes you make to it will mutate the version the caller has.
Separately, to express code which creates multiple non-const references into a single object (trivial in any other imperative or OOP language), you need compromises like verbose Cell, overhead RefCell, ugly and footgun-filled UnsafeCell and raw pointers, unsafe confusing (and currently unsound, see https://github.com/rust-lang/rust/issues/63818) Pin, or experimental ghost cells (https://plv.mpi-sws.org/rustbelt/ghostcell/paper.pdf, not sure the current implementations).
One of the benefits of Rust is that it gives you an option to switch to imperative style if thats what you choose which isn't the case for Haskell or purely functional languages.
Tacit programming was never a problem as the IDE does a pretty phenomenal job of telling you the types anyway.
non-const references in a single object can simply be solved by making the whole object mutable, which is essentially what Python et al. do for you anyways.
I never had to use any unsafe code or pointers. Not sure if that way of thinking is carried over from C and C++ engineers. The only time I think I use Cell or the like is if I have a global static variable that I need to fill in at runtime. But that is more of a code-smell for most or at best a more advanced programming design that beginners don't need to worry about.
The best way to solve the last problem is to understand your data model from the start and make what objects need to be mutable mutable.
More and more, I find this is just a general problem and not a rust specific one. Any language with enough syntax flexibility will have a group of programmers that try to treat the language like lisp or haskell. (I recently ran into exactly this with a typescript lib I've been working with).
C++ is no stranger to this problem, it's famously the reason Java doesn't have operator overloads.
What I'm also finding is that complex style of programming is annoying to find in the company code base, but is rarely one that catches on in the ecosystem as a whole.
I know for myself, I overdid it at times as I was learning the trade offs.
Where I've currently settled is simple map/filter operations to tweak things into a more readable state are better than the more procedural approach. Also keep to simpler combinators (e.g. no `map_or` / `map_or_else`). If things become too complicated, be procedural. If there are side effects, be procedural.
Another way I have for framing it is the code layout (files, functions, blank lines, newlines, indentation) should be used to highlight the business logic. Early returns, `?`, post-fix combinators, etc should be used to reduce boilerplate that detracts from the business logic.
Yep! That's why Iterator::for_each https://doc.rust-lang.org/stable/std/iter/trait.Iterator.htm... is more often than not a code smell [0]; it exists mostly for the sake of writing side effects in functional style.
So something like
something.map(|x| ..).for_each(|x| do_something(x));
Should probably be written let iter = something.map(|x| ..);
for x in iter {
do_something(x);
}
It's still okay to use Iterator::map and other iterator combinators, but using for instead of for_each here is probably easier to read.[0] One exception is in the documentation "In some cases for_each may also be faster than a loop, because it will use internal iteration on adapters like Chain."
Can you give an example of that? Do you mean something like, using .map() and .map_err() on Result?
I can't imagine myself not using those methods; they make programming much more enjoyable, and should be considered elementary as of 2023.
(Note: about .map_err() specifically, error conversion is kind of important in Rust; it's usually performed automatically by the question mark operator but sometimes you need to do it explicitly, and doing so without .map_err() usually obscures the intent of the program for no gain in simplicity.)
So multiple mutable references to the same data? I'm personally happy that's harder to do in Rust, as it enables anti-patterns like God Objects [1] and spooky action at a distance [2].
I don't like the "you're doing it wrong" response when people point out flaws, and that's not what I'm trying to say here. You can write perfectly fine code with remote mutation. This is just one example where the things that Rust chooses to make harder are also the things I have a harder time understanding.
I'll also agree that remote mutation is still necessary for some important use cases (e.g. testing, memoization). Then you have the `Cell` / `RefCell` escape hatches. Rust is pragmatic about such things.
> you need compromises like verbose Cell, overhead RefCell, ugly and footgun-filled UnsafeCell ...
The actual need for these constructs tends to be rare in my experience.
We have tens of thousands of lines of Rust in our codebase and ~five instances of these wrappers. They're really not hard to avoid. And actually, looking through the results here, I've found a few cases we actually _shouldn't_ be using `RefCell`.
> unsafe confusing (and currently unsound ...) Pin
This is a fair point. `Pin` always felt to me like a compromise to get async out the door. The best thing I can say is it's proof that the Rust team is willing to not let the perfect stand in the way of the useful.
But I'll concede it still is difficult to understand and complicates the async picture. This is definitely an example where Rust's underlying philosophy makes things that should be easy harder.
1. https://en.wikipedia.org/wiki/God_object 2. https://en.wikipedia.org/wiki/Action_at_a_distance_(computer...
Val is probably the next language that fleeing C++ developers should get behind.
There's even science behind it: https://www.jot.fm/issues/issue_2022_02/article2.pdf
Not every language evolution is a cautionary tale. Java, C#, PHP, JS have grown a lot and are fine.
Rust’s learning curve is not from accidental overgrowth of complexity (e.g. it has two+ string types not because of legacy, but because it needs different kinds of ownership). Rust has editions that make its evolution less constrained by backward compatibility, “how are we going to teach this?” is a mandatory question for every language change, and Rust has exceptionally helpful compiler errors.
Rust has some tooling for managing supply chain risks - vetting dependencies, vulnerability scanning, vendoring, etc.
C++ doesn’t have any special security model or other solution that makes dependencies safe, only pain and annoyance that makes people avoid using dependencies. So it’s unclear what Rust could do better here (switch to autotools? ;)
Rust projects tend to split themselves into multiple crates, so “lots of dependencies” in Cargo isn’t as heavy and reckless as same number of C++-sized deps.
Rust has already broken out of being a niche language.
Naming things is hard. Rust has some less common features and needs to refer to them by some name. Sometimes it borrows terms from other languages (like enum instead of a sum type), but sometimes there isn’t a common word for everything.
Kind of, Java 20 and C# 11 have lots of interesting material for job interviews and pub quizzes that I bet random joe/jane developers will fail at.
I am mindly aware of them, because alongside C++, they have been my main tools for the past 30 years (rounding to the oldes one), and I surely would fail as well.
Copying and pasting code is equally do-able in both languages.
I find it very weird that anyone would think that any programming language is even remotely fine. Everything is a mess, there are terrible compromises to be made everywhere between expressivity, readability, build-time performance, run-time performance, safety, language community, etc etc. A language will be "fine" the day there's no compromise to be made anymore and it outclasses everything else before it on every metric relevant to shipping good software fast to end user while enjoying the process.
Fine is a synonym for "satisfactory" or "acceptable."
"Fine" in programming specifically, usually translates to "I can use it to solve my problem without the flaws in it inhibiting me from doing that" or "I can forget that I'm using the tool and just focus on solving the problem."
Many things in the world are "fine" by this definition. That doesn't necessarily mean they're all good, let alone perfect — but they're fine.
This is one example but there are other threads of work where what they’re adding brings more consistency across the language, making it easier to learn.
Learning curve. Yes there is one. I dare to say lower than C++ though. Getting up snd running with Rust to do build clis, small web servers, python extensions, wasm for the web is not any more difficult than with C++. I think there is a lot to say for incremental learning with Rust. You can forget all about lifetimes (almost). You do need to deal with the borrow checker my in my experience developers get it. I think the Rust community has done a great deal of fantastic work on this "step learning curve". It will never be pythons, but if you are comparing to C++ then then... Hey, I think it might be easier!
https://en.wikipedia.org/wiki/Stranger_in_a_Strange_Land#Gro...
For Safe Rust this feels pretty OK to me. You're much closer to the Java world where not understanding properly leads only to unnecessary performance overhead, than the C++ world where it's maybe going to catch fire for seemingly no reason if you misunderstood.
A Rust beginner is just as likely to make sub-optimal choices as a C++ beginner, but it's far less likely that to their astonishment their program explodes messily. Suppose I have a Bunch of Clowns, I want the six funniest Clowns, in both languages the beginner is likely to try to sort all the Clowns by how funny they are. But actually in both languages we can and (optimally) should tell the algorithm we only care about six, it needn't sort the 7th and subsequent Clowns at all.
But in Rust if the funniness of a Clown is a floating point type, the programmer is obliged to explain what they actually mean to happen here†. Floating point types can be NaN and we can't "just" sort NaNs as if somehow not-a-number is comparable when the whole point is that it isn't! In C++ the standard actually does say, deep in the ISO Document, that you can't sort NaNs, but you won't be warned about that it just silently makes your C++ program Ill-Formed.
† "It's OK, Clown funniness is never NaN" is a reasonable thing to believe but Rust will make you write that assumption into your program code.
1. Plenty of tools to keep track of your dependencies. It is also entirely possible to go it your own way and keep dependencies very low. You just have to implement more things yourself. Dependencies don't just insert themselves into your code on their own.
2. I don't even understand what you mean. Rust has a ton of very good resources to learn from coming from all knowledge levels. You can also use Rust at an entirely C level and only use as many advanced abstractions as you are comfortable with.
C++ in 1993 was also quite easy to learn.
The good:
* Cargo! It’s amazing, thank you.
* Portability. The build model is the same between different platforms; all the time I waste managing multiplatform builds with C++ instead goes to coding.
* Libraries. C++’s stdlib has gotten a lot better, but it still can’t parse json or make web requests or create zip files.
* Community. The vibe from docs and blogs and videos is good, cheerful and optimistic.
The bad:
* The borrow checker. Memory safety is great but there are plenty of memory-safe designs that the borrow checker complains about. With modern C++ and sound design techniques, memory safety is not something I worry about much in my C++ projects, and the static analyzer proves me out on that. In particular, a typical application design involves some variation of the Observer pattern (implementations can vary wildly), where views read/“observe” models owned by controllers. Why should I beat my head against the wall with a systems language? There should be an escape valve that doesn’t sacrifice memory safety.
* Documentation about the borrow checker and lifetimes. The Rust book tries to treat this in an approachable manner, but I just wind up more confused; I don’t think it can be treated casually and as such I’d like to have a very thorough and detailed description of what -exactly- is going on with these language elements. If Rust’s going to be a systems language, it’s going to need to get comfortable describing what’s happening precisely.
* dyn. I love templates, static polymorphism is great. But sometimes you need runtime polymorphism. Rust treats dyn like a second class citizen.
* Arc<Box<Foo>> ugliness. Heap-allocated reference counting is a useful thing sometimes. I know Rust doesn’t prefer it, but Rust’s FTFY attitude here is annoying.
* Where are the functors??!?!? Lambdas are great and all, but I can’t create unbound trait/struct functors and then invoke them on strict references later? This is a huge limitation in runtime flexibility for application development. Boost::function and boost::bind worked with VC6 twenty years ago! When I realized Rust didn’t have functors, I became much less interested in the language.
* No function overloading. This is just silly and onerous. Rust could create sensible restrictions to avoid ambiguities and C++’s ADL/type coercion complexity but there’s nothing ambiguous about having two functions with different arity sharing the same name. The hardest problem in CS is naming things, and Rust’s lack of function overloading makes it harder still.
In general, Rust seems a whole lot less expressive than C++, and the claims that these restrictions are sacrificed for memory safety are bogus. Nothing above involves pointers and reinterpret casting and void*.
I've certainly seen truly awful C++ codebases, full of undefined behavior and stack and heap corruption. It's possible to write very bad unsafe C++ code; it's also possible to write safe C++ code without much difficulty.
As far as design patterns: fair point that design patterns and idioms may be different in Rust but... what are they??? Fundamentally, in an application, some parts are going to read some data while another part owns it. How does that work in Rust? I haven't seen good answers yet. With procedural information processing utilities those concerns don't come into play as much, but I'd love to read about the architecture of well-functioning Rust GUI app.
I think people are increasingly frustrated by this refrain, because there's empirical evidence that suggests otherwise. Now, it's entirely possible that you are starting greenfield projects with only super senior engineers in domains where a few safety critical bugs just don't matter, and in that case the learning curve required for memory safety just isn't worth it. That's a business decision you can make, and I wouldn't judge for a second. There are always trade-offs in engineering. However, "I don't write bugs" just isn't credible in 2023. I'm sorry if this seems uncharitable, and I appreciate that you're making a more qualified claim, but safety is not a "boogeyman."
> I'd love to read about the architecture of well-functioning Rust GUI app.
GUI is certainly a still developing area in Rust and has a long way to go. It's easily the weakest part of the ecosystem. That being said, it's still totally possible to build GUIs in Rust. The most common architectural pattern would be the Elm architecture, which avoids many of the issues of ownership that more traditional OO patterns introduce. I've also had a lot of success recently using immediate mode libraries, although obviously this won't work for every application.
... but rust cannot do this three things either from just the stdlib? I don't see anything related to requests, json, or zip files in here: https://doc.rust-lang.org/std/#modules
otherwise if you can install libraries, json, zip and web requests are just one vcpkg / conan dependency away in C++ too - and if you want to limit the amount of dependencies you can just use boost which in 2022 does support json and web requests, and can compress / decompress gzip (not zip though :/)
It's the compsci version of Hawking's quote 'for every equation I include in this book, I'll lose half the readers' (or words to that effect). For every extra line of setup, you lose some of your potential audience.
Just setting up C++ tooling requires non-beginner level skills.
I've taught C++ beginners to use a C++ compiler, linker and make with no problems, and earned quite a bit of money while doing so.
But why would any beginner do not do the same thing that Rust offer: use one written by someone else? It does not have to be special or even open. Beginner needs one that they can learn from other people around them. Most of corporate ones are not even documented. People onboard by watching over a shoulder how it works xD.
Don't you still have Iterator invalidation to keep track of and I thought I saw posts regretting using `std::string_view` because it is easy to reference deleted memory.
Anecdote: I maintain a template language library for Rust. I saw the potential for it to speed up if I used more borrowed data (like `&str`, Rust's version of `std::string_view`). I gave it a try and once it compiled, it just worked without crashes. I reflected back on if this was in C++ and the conclusion I came to was that maintainable code is a much higher priority than performance and that any future change in a similar C++ code base would require global analysis to make sure it was safe, making the performance gains not worth the lack of maintainability. In Rust, its been trivial.
> Where are the functors??!?!? Lambdas are great and all, but I can’t create unbound trait/struct functors and then invoke them on strict references later? This is a huge limitation in runtime flexibility for application development. Boost::function and boost::bind worked with VC6 twenty years ago! When I realized Rust didn’t have functors, I became much less interested in the language.
The fact that you can't `impl Fn` bothered me for a long time. It was one of the many examples of where Rust felt unfinished.
Recently, I've seen an inverted pattern for this, define a trait and `impl MyTrait for Fn`. I've found I much prefer this pattern over the cases where I would have used a functor. Granted, there are more ad-hoc cases where functors would be better.
> No function overloading. This is just silly and onerous. Rust could create sensible restrictions to avoid ambiguities and C++’s ADL/type coercion complexity but there’s nothing ambiguous about having two functions with different arity sharing the same name. The hardest problem in CS is naming things, and Rust’s lack of function overloading makes it harder still.
I thought I'd miss this but it hasn't been as bad as I expected.
I don't run into iterator invalidation that much in practice. I'm not often erasing elements from collections. There are all sorts of things like std::string_view where you could reference bad memory if you don't manage lifetimes, but I... just... come up with designs where lifetimes are managed properly? It's probably habit and very intuitive for me, hardly conscious of it.
Also, this is where the static analyzer is fantastic. If you have good unit test coverage, it will let you know you've screwed up and pinpoint where.
I don't mind the borrow checker as a default, but it would be nice for an escape hatch that wasn't an unsafe{} block. Perhaps one with runtime checks? Predictable conditions are virtually free with branch prediction.
> Recently, I've seen an inverted pattern for this, define a trait and `impl MyTrait for Fn`. I've found I much prefer this pattern over the cases where I would have used a functor. Granted, there are more ad-hoc cases where functors would be better.
Can you give an example or provide a citation to this? I'd love to read more.
It would look something like this:
trait MyOperation {
fn run(&mut self, a: T, b: U, c: V) -> R;
}
impl<F> MyOperation for F where F: FnMut(T, U, V) -> R {
fn run(&mut self, a: T, b: U, c: V) -> R { self(a, b, c) }
}
fn my_api<F: MyOperation>(f: F) { ... f.run(a, b, c) ... }
struct MyFunctor { ... }
impl MyOperation for MyFunctor { ... }
my_api(MyFunctor { ... });
That is, when defining `my_api` and you would like to accept both lambdas and functors, instead of using a `Fn` trait bound directly, use a more specific trait which has a blanket impl for lambdas.`nom`'s `Parser` trait is implemented for functions. You can mix and match parser functions and parser types, including the built-in types that are returned when you call the post-fix combinators on the `Parser` trait.
Not exactly sure what you mean about functors though. Maybe an example would help.
No function overloading was definitely a good decision. You can emulate variadic functions by implementing traits for tuples.
type ArcWrap<T> = Arc<Mutex<Whatever<T>>>
The escape valve here that doesn't sacrifice memory safety is reference counting and perhaps interior mutability. (And this point certainly does involve pointers, if not void*.)
> I can’t create unbound trait/struct functors and then invoke them on strict references later?
You can't implement the `Fn` traits for your own types, but this shouldn't have any impact on what you can express, should it? If you're going to pass in references at the call site you can do that just as well with lambdas (or if you really want to use your own type, with a hand-rolled trait to replace `Fn`).
>Arc<Box<Foo>> ugliness
Box is not needed, Arc<Foo> should already be heap allocated.
But I haven't seen outright hostility in the community.
BTW, we had a discussion about monads in Rust forum, and I pointed at this same self-referential definition.
But I feel you in that I'm an average-ish developer, and I write Rust with a constant fear that I'm "doing it the dumb/noob/wrong way". For example, maybe I just want to write a print method for an object, but I know the Right Way is to impl Display, so I do that so I won't be ridiculed.
I imagine the total time spent compiling Rust to be much, much lower.
As for problem 1 (and I say this as a professional rust developer and shill), it's extremely serious and people in the ecosystem do not seem to care enough about solving it. You can steal login credentials and SSH keys by typosquatting today, and all a user needs to do is open an editor. I get why the solutions pitched are not implemented, but it's troubling that major companies are investing into the ecosystem and consuming from it but not securing it.
I would like to mention my subjective concerns for Rust. I hope the Rust community can think me as a canary who is from the C++ world. I am desperately looking for an alternative to C++, and I am a mid-weight C++ programmer. And here is what Rust could face down the line (remember, in early days C++ too was a grockable language and hence its popularity):
0. Rust's C++ problem: Learning curve. From the get go if there is murmur that Rust has a learning curve then think what will happen when Rust reaches the maturity of C++.
1. Rust's NPM problem: supply-chain. If I decide to use npm, and use the npm toolchain to download a project then to my horror it downloads god knows what. Lots of dependency hierarchies, some of them have not even reached 1.0. This raises the concern for supply-chain attacks. Rust with the aim of "boosting developer productivity" just decided to follow the path of all those who are susceptible to supply-chain attacks.
2. Rust's Haskell problem. Haskell is a very beautiful language. Even then it has not reached the level of adoption of other popular languages. It has nothing to do with the language, but some in the community who have a tendency to project a coolness level by saying something like "A monad is just a monoid in the category of endofunctors". What that ultimately does is alienate lot of software developers. Early Rust evangelist tried very hard to project Rust as having a very welcoming community, but if acronyms and terms from category theory and lambda calculus, that are alien to most developers, are thrown around without taking into consideration the larger development community then I fear that Rust will go the Haskell or OCaml way i.e it will become a niche language with small community.
Just 2cents from an average everyday software developer.