That being said I feel Rust has a lot of paper cuts that slow people down too. Perhaps the silver lining is that they are more like barricades than minefields.
That being said I feel Rust has a lot of paper cuts that slow people down too. Perhaps the silver lining is that they are more like barricades than minefields.
Beginners, count your blessings. You don't need to unlearn other languages before learning Rust.
I sincerely believe the only keywords common to Rust and C are a few control flow keywords (for, if, while; but not switch, goto, or do-while) and "bool"---the latter only because of C99 and stdbool.h
I think readability is a major part why java was hugely successful: C programmers could read java code and immediately understand what the code did and able to make minor modifications to the code.
This is very difficult with Rust code. Rust "learned" too much from Perl in this regard.
Here's Barry Revzin, a C++ programmer, explaining what he wants with a Rust example
https://brevzin.github.io/c++/2020/12/01/tag-invoke/#this-is...
Now like I said, I'm a Rust programmer, so of course I understand PartialEq's definition there, but, is it really hard to see what's going on as a C programmer? There's a lot of magic you don't have, you don't have traits, or references, or the Self type or the self keyword, or really any of the mechanics here, and yet it seems pretty obvious what's going on, doesn't it?
I literally don't remember any unpleasant surprises of this form when learning Rust. Actually mostly the opposite, especially for != I thought, from experience with other languages like Java, C++ or Python, OK obviously at some point the other shoe drops and they show me that I need a deep comparison feature. Nope, Rust's comparison operators are for comparing things, it isn't used to ask about some surface programmer implementation detail like the address of an object in memory unless you specifically ask for that. If Goose implements PartialEq, so that we can ask if one Goose is the same as another Goose, then HashSet<Goose> also implements PartialEq, so we can ask if this set of geese is the same as another, and HashMap<Goose,String> likewise, so we can ask if these two maps from geese to strings are mapping the same geese to the same strings.
I know ripgrep is written in Rust and works really well and "does one thing", so I'll go to its repo ( https://github.com/BurntSushi/ripgrep ) and read through a bit. First, I'm looking for a "src" folder, but that does not exist, so right off the bat I am a little uncomfortable. Nothing in the root folder looks like it would be the source. What are "complete" and "scripts" and "pkg" folders? Because those (in that order) would be what I check.
"complete" looks like command line completion. The "scripts" directory has one file, "copy-examples". "pkg" contains folders "brew" and "windows". Still lost...
I thought "crates" was like modules for Python, so I think that would only contain dependencies and stay out of there.
Finally, I relent and open "build.rs" which I suspect is something like "build.ninja" and I can figure out which node has source files. Stupid me. "build.rs" IS the source file.
Oh. No, actually, it's not all of ripgrep, in fact. It's the tip of the iceberg. The source is in crates/core/app.rs, and build does manpage compilation, registers shell completions, etc.
So, line one. I don't know what the return value App<'static, 'static> is. I understand it's an App object, and the tick (no idea what it's called; this is starting to feel like a fracture mechanics lecture) I know has to do with ownership. It's tough to see how there are two subtypes in App<> when the assignment of app has ten functions that assign attributes. I can certainly READ what is being assigned to the App object. I wouldn't be able to WRITE any of this so far. Unimportant, it's not the meat and potatoes of ripgrep.
Now I'm scrolling, looking for something cool to analyze and run across Vec<&'static str>. Okay, I thought tick was ownership, but I also thought ampersand was ownership-related. Maybe one is mutability? (This is like knowing how to play music really well but now learning a totally foreign instrument.)
Skimming, it looks like this whole file is the argc/argv parser. I'll look for something interesting in main.rs, search.rs, and subject.rs (the last because it's an unusual name).
struct Config {
strip_dot_prefix: bool,
}
Well. I can read that! Next line... impl Default for Config {
...
...not that. It's probably something like a class method.As I'm reading through, it feels like a LOT of kicking the can down the road and verbosity. There's a function binary_detection_explicit(self, detection) that just assigns detection to self.config.binary_explicit and returns self. binary_detection_implicit() does basically the same with a different assignment member. getters and setters, roadblocking readability since the dawn of computer science... (a few lines later, the function has_match() returns the value of has_match. ugh. This is why I prefer crudely written C to textbook C++.)
I see .unwrap() in a line. This tripped me up during the tutorial; still no idea what it does. Sure I can look it up, but there are a hundred other terms to learn: unwind, map, Result (OH! The two App return types are probably Ok and Err ... maybe?), convert, concat, const, continue, ...
subject.rs, btw, can probably be written in three lines of really dense Python or ten lines of really well-written Python with a few other lines of Doxygen. Instead, it's about 90 lines of Rust with another 50 lines of comments.
Finally, I find something noteworthy.
fn search(...) ...
fn iter(...) ...
for subject in subjects {
searched = true;
let search_result = match searcher.search(&subject) {
....
This is all extremely readable and just feels like a standard queue in any language. But then there are lines like: if let Some(ref mut stats) = stats {
*stats += search_result.stats().unwrap();
}
I give up. Are you assigning the variable stats to itself? by reference but mutable? If Rust doesn't have pointers, what is *stats? unwrap() is no longer the most confusing part of this.-----
Postface, I'm originally a mechanical engineer who eventually got roped into writing driver software, so C is my comfort zone and I don't need anything much more complicated than "set bits here, read registers, use a 40-year-old communication protocol, handle errors".
There's simply no easy analog between Rust and Python ... or Rust and C ... or Rust and SQL ... or Rust and PRACTICE ... or Rust and any other language I've learned. I understand a lot of the CWE Top 25 software errors are mitigated by Rust and it's not just a theoretically correct but unusable language, but there hasn't been a strong reason to learn it, and the learning curve is made steeper by preconceived notions of every keyword.
-----
I forgot to compare with grep.c
Extremely readable. I know right away which part is args, where memory allocation happens, where file handling occurs, and then the "hard part"---where the magic happens---is the last page on my monitor. I can just stare at that and mentally deconstruct until pieces fall into place... Look up regcomp()? check. Look up grep_tree()? check. Read the kernel.org discussion on why grep_tree() was written and how it relates to ensure_full_index(), which explains some of the other variables.
The whole thing sits nicely in a few pages with terse but clear comments (unlike this post) and looks familiar even though I've never seen the source before.
-----
Last edit. I tried just now to refresh on .unwrap(). https://doc.rust-lang.org/rust-by-example/error/option_unwra...
Only because of the comments did I realize that calling drink.unwrap() will panic (throw an error?) if that argument is empty. So, unwrap is just the "if (... == nullptr) { return ...; }" of Rust, it seems.
But it's somehow an alternative to .expect() and has its own alternatives, unwrap_or/unwrap_or_else/unwrap_or_default, and it also seems optional. And my time in the rabbit hole is over; my children need to go to bed.
This comment reminds me very strongly of myself, trying to read German. You know C, why shouldn't you be able to understand Rust? But they're not the same thing, and they communicate different things in different ways. There are concepts that simply do not map directly. Someone could very easily write the exact same post from the perspective of a Rust programmer starting with C. Where is the equivalent to Cargo.toml? What is this Makefile thing, and why doesn't it look like a configuration file? Etc. No amount of just forcing yourself to try is going to provide clarity unless you already understand the idioms and structure at play.
With that said, I have written embedded Rust and it is an amazingly freeing experience. Writing C right now feels like trying to balance a knife on my finger. With Rust I can just get work done. I strongly recommend you spend the time to learn it. Even if it doesn't convince you that C's days are numbered, it will make you a better programmer by training you to think about safety and correctness up front.
-----
I tried Embedded Rust about 4 to 6 years ago and found the experience underwhelming, as I was installing a TON of packages just to get Blinky and seeming to bypass a lot of standard Rust (no_std, no_main, use panic_halt as _, everything GPIO is unsafe, etc). The last straw was that imported Cargo packages had their absolute path somehow hardcoded in the symbol table, so my debugger would try to load, like... C:\Users\jacob\stm32-rust-thingy\app\solve.rs (and fail, because I'm not jacob and don't have his source code)
I'll give it another shot. I'm sure the "many eyes" principle (and better/more tutorials in the past few years) make the process easier now than then.
And German has a particularly special trap for "trying to parse out each word": trennbare Verben (separable verbs). Unless you know enough of the grammar to know when a piece of the verb has been unceremoniously punted to the end of the sentence, trying to use a dictionary to find the meaning of the verb of a sentence can be an exercise in futility.
I'm reminded of a Mexican friends mother, she and her aunts went to Europe. They loved loved Italy. Partly because if an Italian spoke slow and clearly they could understand what the person was saying.
My experience with C# for instance was it was really easy to pickup. Because snippets of code are generally C like and procedural.
Heard the above about said about go. If you know Java and JavaScript you probably know go already.
I found python not too hard because I knew perl sort of. Mostly enough experience with it to avoid it.
I suspect if you know C++ and a ML language Rust is a breath of fresh air. But if you don't it's not. It's gross. And very very slow iteration times.
As long as I stick to Embedded Rust, I can use work time to learn more. I'll probably plan that between my current projects these next few months.
You look at the code and go what? Then you cock your head and it's still what. And then you turn your head sideways and suddenly it makes sense. Then you look at code in another language and go what?
I'm not a database expert, but I became comfortable with SQL this past year so I could better understand how my company's office software is structured. I can cobble together enough Javascript to create an own online sheet music book using abcjs, a directory full of ABC files, and jquery. I was able to barge through enough C# to write a crude 2D game during a game hackathon in college.
In fact, AFTER learning Python, a lot of C became easier because I knew HOW a data structure should behave, which then influenced how I would create it from structs and functions.
However.
I get hung up on Kotlin. Kotlin reminds me a lot of Rust in its syntax, and Android has the same habit of renaming every single concept.
In general, the more keywords and syntaxes two languages have in common, the easier to read. (In SQL's case, I think the relative lack of keywords and rigid query structure made this a nonissue.)
So, mixed bag.
IMO ripgrep is a poor example of project layout both because it does "non-standard" things and because it encompasses more than the C-based grep you're comparing it to. A simple project will almost always have its source in a directory named "src", and a project with multiple subprojects (like ripgrep) will typically not shove them into a "crates" directory.
Compared to GNU grep, the ripgrep project serves as the repository for a bunch of libraries as well as the ripgrep binary. So there's already a lot more moving parts than the name "ripgrep" might otherwise suggest.
I don't know Java, but I can read the Falstad circuit simulator source.
If you can read Java, I'm surprised impl blocks threw you for a loop as the first comparison that comes to mind is that with Java interfaces. A quick look at the Falstead simulator suggests it's using a pretty small subset of the Java language (e.g. no generics, interfaces, or exceptions) which would certainly make it seem a lot like C. Not all Java is like that.& and *? Akin to reference and dereference in C, and given that you can overload both the comparison to C++ seems pretty apt.
In any case, for more direct explanations the API reference is a better place to look. Two short sentences explain what unwrap and explain do, and what the differences are.
This is not unique to ripgrep; for example, that's matklad's general recommendation https://matklad.github.io/2021/08/22/large-rust-workspaces.h...
I have split out some crates that obviously stand-alone. The `termcolor` crate was born inside of ripgrep but now it's in its own repository. The `ignore` is another candidate for splitting out because it has broad utility, but it needs a lot of work before it's something that can standalone as its own project IMO. But most of the rest of the crates, i.e., grep-{matcher,searcher,printer,regex,pcre2} are essentially what ripgrep is. Those will never get split out. If I did, the ripgrep repo wouldn't actually contain the most interesting parts of ripgrep! Not good.
With all that said, I am a monorepo guy. Not for ideological reasons. For practical reasons. I also maintain dozens of crates, most of which are in their own repositories. The only reason I do a different repository for each because that's what the custom is and it makes collaborating with others easier. If the code was just for me, it would all be in one big mono-repo. No question. It would be A LOT less work.
With all that said, I am a monorepo guy. Not for ideological reasons.
For practical reasons.
Yep. It's a balance. The big problem I have with large(r) repos is that everything git related is just slower and takes way more space. I've got some git prompt magic (using gitoxide of course) and it just drags with the rust repo.At one point I wanted to make some changes to a man page in FreeBSD. Sure, it's neat that there's thirty years of history. But it's thirty years of everything's history. Shallow cloning made it a bit more bearable, but the UX for trying to grab just a directory or two from a repo is still petty gnarly.
But it's all just emacs vs vi all over again (and I've settled on helix and sublime anyhow).
You are absolutely right. The foreign-ness you are seeing is the ML side of Rust’s roots. Rebinding, restructuring, small wrapper types, and of course all the functional programming bits and bobs that people coming from, say, Haskell might see as being quaintly simple, but arcane to anyone else.
If I see Java source, it makes sense even though I don't know Java... at least up until how javax.swing layouts are constructed or I see a really obscure annotation without comments. If I had to add something to the Arduino 1.0 IDE, I'm confident I could figure it out in a day or two, because a class is a class, lines end with semicolons and comments look /* like this */, I can guess the existence of sort() or random() or an int being either int, Int, or Integer.
In contrast, a Rust "trait" being roughly equivalent to a C "struct" would not have been in my top 20 guesses. I can't recall trait being a keyword in ANY language I've used. I have no idea what Box and Arc are, because to me, those are "what you put tools in" and "plasma from too much potential difference".
Yes, I can check the reference (constantly) but at some point looking up every word or tick mark gets in the way of absorbing the story. The punctuation in particular bothers me, because it's not searchable via engine. There's no special Google result for
rust '
I can't be more clear. My ability to read/write code extends beyond pseudocode. Rust is just not an easy transition for a "primarily C" user, in my experience and opinion.“Trait” is used for similar features in Scala and PHP. https://en.wikipedia.org/wiki/Trait_(computer_programming)
Box is used in Java and many functional languages. https://en.wikipedia.org/wiki/Boxing_(computer_science)
Arc is the most confusing name out of these for sure; most folks talk about “reference counting” in general and assume the count is atomic; with Rust’s performance and safety focus, Rc is the non-reference counted version and Arc is the atomically reference counted version. Additionally, there is a feature called “automatic reference counting” that some languages use, that uses atomic reference counting in the implementation.
> There's no special Google result for rust '
If I google for “rust apostrophe” this is my first result, which is pretty comprehensive: https://stackoverflow.com/questions/22048673/what-are-the-id...
While this syntax is unusual it is also not unique, OCaml uses it for similar purposes. Nobody loves this one, but also nobody was able to come up with something that could be agreed upon to be better.
Typo here I think Steve, Rc is reference counted, but it's not atomically reference counted. Hence the lack of an A. You've managed to instead say that it's not reference counted.
ripgrep is way more abstracted than most greps because ripgrep splits a whole bunch of its functionality into crates. The idea being that others can then re-use the reasonably sophisticated infrastructure that ripgrep uses to write their own bespoke grep tools. ripgrep internals are more like a bare-bones and under-developed grep framework. If you go back to the initial version of ripgrep (0.2.0), you'll probably find it smaller and significantly easier to read. There's a lot less abstraction. FWIW, I generally regard my abstraction attempts here to be a partial failure. The hit to code readability has been quite large. I also fucked up at least one abstraction boundary. On the other hand, it has enabled people to maintain very small patches to make ripgrep work with other regex engines.[1] (And of course, ripgrep uses the abstraction to make it work with both Rust's regex crate and PCRE2.)
I wouldn't call Rust an easy language to learn. It could be easier depending on your background. For example, if you have an ML/Haskell and C++ background, then Rust will probably introduce very few things you haven't seen before (that being the borrow checker). But if your background is, say, Python, Java and C, then there are going to be oodles of things in Rust that will be novel to you. That basic vocabulary will be more difficult to acquire.
I'm somewhat surprised you feel confident enough in your understanding of Rust to declare you could replace 50 lines with 2 or 3 lines in Python though. Meh.
> getters and setters, roadblocking readability since the dawn of computer science...
They are tools of encapsulation. I don't treat them as boiler-plate. I treat them as things that I use when I care about encapsulation. And when I don't care about encapsulation, I don't use them. My personal style is to lean heavily on encapsulation.
also, true about rewriting subject.rs, that was a bit of hyperbole about how everything spreads out vertically and seems to do one task per screen-full of text. I'd need at least as many lines as functions and classes and returns, so about 40 keeping everything clean and not combining/deleting classes. which is what you have if you combined your multiline statements.
(aside, i don't need to know how to read rust to understand subject.rs. The comments describe everything in plaintext)
As for the source code itself, I agree, Rust does rely more on library functions than language constructs on some things (e.g. unwrapping optional values), but I'd argue this is just more different form C rather than strictly worse when it comes to readability. I wouldn't complain about how difficult it is to read Greek as a person who only knows English.
I can't say I find Rust's pattern matching rules particularly pleasant. The rules are convoluted and arbitrary
https://doc.rust-lang.org/stable/reference/patterns.html
Also, it has often been surprising to me how code that looks like it's entirely read-only will try to move stuff.
E.g, this works:
let numbers = [1, 2, 3];
for n in numbers {
println!("{}", n);
}
for n in numbers {
println!("{}", n);
}
This doesn't work: let numbers2 = vec![1, 2, 3];
for n in numbers2 {
println!("{}", n);
}
for n in numbers2 {
println!("{}", n);
}
I understand why that is and I know how to fix it, but I don't like it. There is a long list of things in Rust that I find understandable but also unpleasant. In that sense it really is a worthy successor to C++. Smart people making unpleasant decisions for good reasons.The reason the first one works is that [T; N] is Copy if T is Copy, and i32 is Copy so therefore [i32; 3] is Copy. So it's actually consuming two arrays, each of those for loops consumes the array to make an iterator, but since it's Copy it just leaves a perfectly good copy of the array behind in the variable. The compiler can see what we're doing here and won't pointlessly duplicate the array (presumably) but that's conceptually what's happening.
The second one doesn't work because the Vec<i32> isn't Copy, so, we consume it and now it's gone, when we reach the second for loop the variable numbers2 is gone already.
So you shouldn't make assumptions whether something will be moved or not based on read only preconditions.
let s1 = String::from("abc");
{
let s2 = s1;
}
println!("{}", s1); //borrow of moved value: `s1`
But I do understand what you're getting at. In a sense, a move is not a runtime concept at all. The compiler simply considers the variable as no longer readable unless and until a new value is actually written to it.Uh... maybe? But Rust is more like a replacement to C/C++. So a better question is that whether C#/Java programmers could read C/C++ code and immediately understand those & and *.
It is not at all as easy for C programmers to become Rust programmers as you need to learn a lot about Rust before you are able to understand the Rust code.
Just like Rust, C# has an unsafe superset of the language. The meaning of all these * in unsafe C# exactly matches C.
This isn't some abstract "readability", though, this is "how close are your langauges in the evolutionary tree". Where ALGOL is the proto-indo-european of quite a lot of langauges, but not all.
https://doc.rust-lang.org/reference/keywords.html https://en.cppreference.com/w/cpp/keyword
And they're even both provided in (mostly) alphabetical order, which makes this easier for humans
Rust doesn't reserve type names as keywords, which seems like an eccentric choice in C++ (e.g. wchar_t is a keyword!) although perhaps given how fraught parsing is in C++ they had no choice.
The common keywords are: break, const, continue, else, enum, extern, false, for, if, return, static, struct, true, union and while.
true and false are just boolean values, although given C++ like many languages considers "false" to be true, and 0 to be false, I guess it's a surprise they bothered making these keywords.
const, enum, union, static, and struct are about types and values. The choice to have "const" mean "immutable" not "constant" in C++ is hilarious, yeah that's going to cause wider issues in how your programmers understand software. I think C++ enum is rather sad, here's a whole kind of type and it's basically abandoned, it's not allowed to have features the other types have, for no discernible reason. Rust's enum really shows how much opportunity was wasted there.
break, continue, else, for, if, return and while are indeed control flow keywords, although what they do in Rust varies quite a lot from C++. Rust's for is a for-each, it consumes an Iterator (literally, it's a syntax sugar for a loop which uses IntoInterator::into_iter and breaks when the iterator is exhausted) and Rust's break can break-label-value, which is a violently different thing than the C++ break even though the obvious beginner syntax looks identical.
try, do and virtual are reserved in Rust but are not currently used (in stable Rust) to mean anything, we can expect that try is likely to some day be used for a related but different purpose to its cousin in C++.
Immutable is immutable for some specific span of time. In a simple case, like with Python immutable objects, it is constructed once, and after that, it never changes. With Rust, it's also possible for an object to be immutable while a reference to it exists and is passed to some function, but after that function call returns and the reference ceases existing, immutability ends.
fn take_foo(x: Foo) -> Foo {
let mut x = x;
x.field = 0;
x
}
The ownership semantics ensure that because there's a single owner of Foo at all times, it is always safe to make its binding mutable. This is different than trying to turn a &Foo into a &mut Foo, that's not allowed by safe Rust (and an undefined behavior minefield in unsafe Rust).For constants, you can think of them as a "stamp", whatever the value within the const gets inlined in the usage place in your code, every time. Immutability is about whether the value can be changed. To stretch the analogy, you can stamp a pattern on paper (const) but later doodle over it (mutability).
You can see that not only can't you change these values, it doesn't even mean anything to speak about doing so, they're not variables which have values, they're the values themselves.
Whereas an example of an immutable variable would be the std::vector we're being asked to check the length of in a call to size() - this certainly is a variable, somebody else can change it, but we must not.
If we name a constant in Rust like { const TIMEOUT: u32 = 1234; }, we're not saying we want a variable with this name TIMEOUT and then we mustn't change it - instead we are giving a name to this arbitrary constant TIMEOUT (and it has to be constant, Rust won't let us make constants unless they actually are) and then any time we say that name, we get the same value, not the same variable, there is no variable, the same value, the constant one, as if we'd written it here.
So for example the C++ trick where you "cast away" the const-ness doesn't make any sense in Rust because those aren't immutable variables, we can't even try to make them mutable, they're not variables. It's not Undefined Behaviour, it's gibberish, even C++ won't let you write false = true; for example, that's just clearly nonsense.
Not quite, those are literals values.
As mentioned in the C++ standards, const-ness is a quality of variables... Not of values.
In C, wchar_t is a typedef, which in C++ would cause the problem that overloads could non-portably conflict with one of the builtin integer types. That’s why wchar_t was made a fundamental type, and thus a keyword like all the other integer types.
In C/C++, any value can be cast to void, which should be sensible as any pointer can decay to a pointer to void. Thereby, the Wikipedia definition of the void type even says it is like a unit type, and the page for the unit type notes 1) the void keyword simulates a unit type and 2) that--and I think this is very important, to get into the mindset of this term--that the actual unit object type in Java is named Void.
It does really really suck that you can't declare a variable of type void and that some compilers decided the size was 1 for long enough that the size is now undefined (instead of 0), as these mistakes lead to a ton of ridiculous code duplication in C++ templates. (This would be the first thing on my list of things to fix if I were allowed to make any arbitrary breaking change to C++.)
But like, it seems very wrong to claim it has no values at all: in C++, if you have a function that returns void, you can even call another function that returns void and use the return keyword oh whatever you consider it to have returned. It clearly has some kind of value in many contexts--and, despite having a size that is undefined (and potentially 1), that value carries no state--so there is only one possible value: it is merely the term people use for unit in this kind of language.
(To be clear, though: I am not at all claiming that Rust is wrong for using () or unit or whatever, as there is a ton of history of using such in other safe academic languages and it certainly doesn't confuse someone like me. I do, though, think that its mission isn't well served by not at least trying to look as much like the specific languages that need to be replaced--C and C++--as possible.)
You just are exposed to different languages than what was used as the base.
The comment from fauigerzigerk sums it up well. You just have to know via experience, a great teacher, or very attentive learning that vec![1,2,3] is going to pop all of its values in a "print elements" loop while [1,2,3] will retain each value.
Other languages have similar behavior, but it's often more obvious e.g. Python with the .next() keyword.
As a Rust newby I think they are more like achievements that unlock new capabilities.
"and, in fact, it is a "contested illness" due to doubts about the legitimacy of the condition."
AND
"It was noted that in this case, however, the police were perceived to have acted with little care for the hostages' safety,[6] providing an alternative reason for their unwillingness to testify."
Somehow appropriate considering this thread is about how someone's bad experience with C++ has driven them into learning rust.
This is a nice one ;)