> This is a superficial complaint, but I found Rust syntax to be dense, heavy, and difficult to read
I'm a huge Rust fan, but sort of agree. First, I dislike C-style syntax in general and find it all very noisy with lots of unnecessary symbols. Second, while I love traits, when you have a trait heavy type all those impl blocks start adding up giving you lots of boilerplate and often not much substance (esp. with all the where clauses on each block). Add in generics and it is often hard to see what is trying to be achieved.
That said, I've mostly reached the conclusion that much of this is unavoidable. Systems languages need to have lots of detail you just don't need in higher level languages like Haskell or Python, and trait impls on arbitrary types after the fact is very powerful and not something I would want to give up. I've even done some prototyping of what alternative syntaxes might look like and they aren't much improvement. There is just a lot of data that is needed by the compiler.
In summary, Rust syntax is noisy and excessive, but I'm not convinced much could have been done about it.
IMHO it's at least somewhat better than "modern" C++ where you end up having to wrap virtually every single thing in some kind of template class, and that's without the benefit of much stronger memory and thread safety.
Overall I think Rust is a hands-down win over C and C++. People who want it to be like Go are probably not doing systems-level programming, which is what Rust is for, and I have severe doubts about whether a rich systems-level language could be made much simpler than Rust and still deliver what Rust delivers. If you want full control, manual memory management with safety, other safety guarantees, a rich type system, high performance, and the ability to target small embedded use cases, there is a certain floor of essential complexity that is just there and can't really be worked around. Your type system is going to be chonky because that's the only way to get the compiler to do a bunch of stuff at compile time that would otherwise have to be done at runtime with a fat runtime VM like Go, Java, C#.NET, etc. have.
Go requires a fat runtime and has a lot of limitations that really hurt when writing certain kinds of things like high performance codecs, etc. It's outstanding for CRUD, web apps, and normal apps, and I really wish it had a great GUI story since Go would be a fantastic language to write normal level desktop and mobile UI apps.
For what purpose would you need to wrap everything in a template class? In my work, I've only touched templates a couple of times in years. They're useful, but I don't see how it's always needed.
Oh. I misunderstood. I was thinking of user-made templates, not the built-in ones from the standard library. I don't see the issue though. Something like vector feels intuitive. I have a vector<int> or a vector<myType> etc. A pointer to an int, a unique_ptr<int>. It's convenient and fairly flexible. I don't really see the downside, or how it could be done better given static typing.
Something like Kotlin but with a borrow checker might be the ultimate in developer ergonomics for me. I sat down at some point to wrap my head around Rust and ended up abandoning that project due to a lack of time. And because it was hard. The syntax is a hurdle. Still, I would like to pick that up at some point but things don't look good in terms of me finding the time.
However, Rust's borrow checker is a very neat idea and one that is worthy of copying for new languages; or even some existing ones. Are there any other languages that have this at this point?
I think the issue with Rust is simply that it emerged out of the C/C++ world and they started by staying close to its syntax and concepts (pointers and references) and it kind of went down hill from there. Adding macros to the mix allowed developers to fix a lot of issues; but at the price of having code that is not very obvious about its semantics to a reader. It works and it's probably pretty in the eyes of some. But too me it looks like Perl and C had a baby. Depending on your background, that might be the best thing ever of course.
The borrow checker doesn’t really work without lifetime annotations. When I see complaints about rust, that’s seems to be the thing most are talking about. The issue is the notion of an object lifetime is a hard thing to express with familiar type systems. It’s an unfamiliar concept.
Yep. And the struggle learning rust is that you sort of need to learn the borrow checker and lifetimes all at once. You can’t use rust at all without any of that stuff, and there’s a lot to learn before you feel productive! Even a year in to rust I can still throw things together a lot faster in javascript. That may always be true.
So I doubt rust will ever displace python and javascript for everyday app development. But it’s an excellent language for operating systems, game engines, databases, web browsers, IDEs and things like that where stability and performance matter more than cramming in ever more features.
The trick is to avoid long living references as much as possible. You need to unlearn the Java/JS/Python style where everything is a reference. In Rust you should default to value (fully owned) types and move semantics and use references for temporary borrows only. Think about cloning first when you need the data in more than one place, before considering references.
It is surprising how far you can get with that approach without ever writing a sigle lifetime annotation.
I’m nosy sure I agree. There definitely isn’t a “one-size-fits-all” to writing Rust, but I do think that falling back to a “error here? try .clone()” mentality ends up defeating the purpose.
Maybe what Rust needs is a “build a program with lots of allocations” and then “whittle down allocations with smart use of references” section of the book.
> Maybe what Rust needs is a “build a program with lots of allocations” and then “whittle down allocations with smart use of references” section of the book.
Rem acu tetigisti. This is what needs conveying. I think with Rust people are often speaking past one another. One person says "hey, you can't write EVERYTHING with meticulously-planned-out lifetimes, cows for every string, etc", and what they have in mind is a junior/novice programmer's first-time experience (or any combination thereof).
Whereas another person says "you can't clone everything everywhere, or wrap everything in a RefCell", and they are right about what they mean: that properly written software eventually, to be respectably efficient, needs to replace that kind of code with code that thoughtfully uses appropriate lifetimes to truly benefit from what Rust provides.
As usual, Wittgenstein is right, specifically in what he says in s.4 of his famous Buzzfeed article[0]: most so-called problems are merely confusions of language, where two people mistake an inconsistency in their use of signs for an inconsistency in their opinions. If they set it all out more fully and explicitly, I don't think there would be much disagreement at all about what's appropriate, at least among the 80% of reasonable people.
You very well may be able to, but any language that wants to position itself in C's niche won't get away with not having value types. When it comes to systems languages, it's a sad fact (for language designers, anyway) that elegance often loses to mechanical sympathy.
That document is a few years old now and was intended as a design document. But Value classes shipped with Kotlin 1.5. Apparently they are compatible with the project Valhalla value objects that will be added to the JVM at some point. So, this stuff is coming.
I had to look it up because even though I write Kotlin a lot, value classes are not something I have used at all. Looks useful but not that big of a deal and doesn't really solve a problem I have. Data classes and records (in Java) are a bigger deal IMHO.
In practice, the way you deal with immutability in Kotlin is to keep most of your data structures immutable by default unless they need to be mutable. E.g. there's a List and a MutableList interface. Most lists are immutable unless you create a MutableList. Same with val vs. var variables. Val variables can't be reassigned and you kind of use var only by exception when you really have to. The compiler will actually warn you if you do it without good reason. A data class with only vals can't be modified. Java is a bit more sloppy when it comes to mutability semantics. It has records now but all the fields have setters by default. It has var but no val assignments (you can use final to force this but few people do). And so on.
Semantically this is not as strong as what Rust does of course but it's good enough to make e.g. concurrency a lot easier. Mostly, if you avoid having a lot of mutable shared state, that becomes a lot easier.
You could imagine a Kotlin like language with much stronger semantics implementing borrow checking instead of garbage collection. It wouldn't be the same language of course but I don't think it needs to be very different. Using it would not be a massively different.
Kotlin doesn't really have value classes, those are wrappers around primitive types.
That is the thing with guest languages, they cannot invent something that the plaform doesn't support, and if they indeed come up with lots of boilerplate code to fake features, they risk to become incompatible when the platform actually does provide similar features with incompatible semantics.
Swift is getting ownership/borrow checker mechanisms in Swift 6[1]. Kotlin and Swift have very similar ergonomics [2] (little outdated link).
Together with Actors types/Distributed runtimes, Optional Automatic Reference Counting and good support on Linux, Swift is developing into a killer language imo.
Is the syntax really that much of a burden? I don't have a perfect grasp of the syntax and would have trouble if I was having to write rust code with zero reference, but as long as I have other rust code open it is pretty easy to remember what each part does.
fn apply<A, B, C, G>(mut f: impl FnMut(B) -> G, a: A) -> impl FnMut(&B) -> C
// must still be `for<'r> impl FnMut(&'r B) -> C`, because that’s what filter requires
where
G: FnMut(A) -> C,
B: Copy, // for dereferencing
A: Clone,
{
move |b| f(*b)(a.clone()) // this must do any bridging necessary to satisfy the requirements
}
I wrote this code myself, and it's SLOW for me to read. Each part isn't hard, it's just too much crap
The same information can be communicated in different ways, trading one form of noise for another. I have a personal preference for Pascal-like or PL/I syntax. Instead of int *char x or int&& x, there's x: byteptrptr. It's more to type and read, sure, but sometimes having an english-like keyword really helps clarify what's going on.
The english words preference is a cliché that has been argued back and forth, to heaven and hell, since Cobol. I'm sympathetic to your opinion in some cases, but ultimately it fails in my view. Terse notations requires an investment in the beginning but then pay off massively with increased bandwidth, you can hold more of the code in your head. You don't see math being done by (x.plus(y)).pow(3), you see it done by (x+y)^3, it gets even worse when expressions increase in size.
Ideally, the language should have enough syntax-bending facilities so that you can still simulate what you want, this is mostly just operator overloading and not treating custom types like second class citizens. For example, your example of byte ptr ptr can be easily done in C++ by a bytePtrPtr struct, or even better, a Ptr<Ptr<Byte>> instantiated class from the template Ptr<T> for any T. Overloading the dereference and conversion operators will completely hide any trace of the fact it's not a built in type, and compiler optimization and inlinning will (hopefully, fingers crossed) ensure that no extra overhead is being introduced by the abstraction.
As for the 'byte ptr ptr' syntax specifically, in F# generic instantiation can be done by whitespace concatenation of type names in reversed C++/Java/C# order, so the above C++ type would (if translated to F# somehow) literally be written out as you want it to be, so even what seems like it would require language support (whitespace between related identifiers, generally a tricky thing in PL design) can actually be accomplished with clever and free minded syntax.
That is a good point about typedefs, and I would hate to be using 'ADD 1, a, b TO x ROUNDED' instead of 1 + a + b + round(x). I'll also have to check out F#.
I agree, and my ideas for alternative syntax were effectively this. They were, in my opinion, a slight improvement, but still result in lots of syntax. My point is that while I might want a more "python-like" or "ML-like" syntax we often forget that it simply isn't possible in the same way those languages use it, and by the time we add all the extra things we need, it doesn't look that much less "noisy".
I wouldn't even say that. Using words (or multi-letter symbols generally) may be more to type, but virtually everybody uses editors with completion features and those completion features tend to work better with words than symbols. Furthermore, despite there being more characters, I don't think it's actually more to read. People who are fluent in reading languages written with alphabet systems don't read letter-by-letter, but instead read word-by-word, using effortless word recognition.
> People who are fluent in reading languages written with alphabet systems don't read letter-by-letter, but instead read word-by-word, using effortless word recognition.
It all adds up. Languages like COBOL, PASCAL or ADA (originally designed for terminals with very limited character sets, sometimes even lacking lowercase text - thus requiring case-insensitive syntax) make it a lot harder to survey larger code blocks.
If that's true, I would expect logographic writing systems to cause less reader fatigue than alphabetic writing system. But as far as I'm aware that isn't the case, and the two are roughly equivalent.
Considering what XQuery does the syntax does a great job of being readable. Every time I look at React components using JSX I'm unfavourably comparing it to XQuery.
There are similar issues in Rust to the one Hickey talks about in Java, in terms of cognitive overload and difficulty in a human parsing the program. However, I've found rust largely avoids issues with a bunch of different classes and with their own specific interfaces with a bunch of getters and setters in the HTTP servlet example because of Trait interface reuse.
Familiarity also alleviates the issue. I can remember when I first encountered TeX in the 80s and Perl in the 90s and thought the code looked like line noise and now I no longer see that (even in Larry Wall–style use-all-the-abbreviations Perl).
The problem is that familiarity needs to be maintained or you can lose it. As someone that doesn't get to use Rust at my day job that can be hard to keep fresh.
I only occasionally dabble in Rust in my free time and coming back to a project of mine after months of not having used any Rust, yeah lets just say that line noise made me prematurely murder some of my pet-projects.
Sure it gets probably better with time but still it is a cost that one pays.
It alleviates the issue the way the frog doesn't notice in the "boiling frog" fable. That is, not in a good way. The cognitive load to parse and understand it is still there; you're just not as aware of it distracting from other matters. Some (me) would say it distracts from more important things, like how units of code compose and what the purpose of a program is.
I find that Rust tends to have code that goes sideways more than downward. I prefer the latter and most C code bases, that I find elegant are like that.
It is like that, because of all the chaining that one can do. It is also just a feeling.
While generally, yes, features are bloating up a language, one could argue these two particular features reduce complexity. Like why am I forbidden to use && in a if let? And why does if let support both irrefutable and refutable patterns but deconstructing let requires them to be irrefutable?
There is surely some "law" in some place of the internet that says something like: "Every programming language valid construction no matter how ugly, obscure,verbose,old,etc will find its way to lots of codebases". See C++.
Yes, John Carmack has said as much, at least in the context of syntactically and semantically valid constructs that are actually bugs making it into codebases. What does that have to do with let chains and let else?
The problem are not the new traits per se. The problem is that the complexity of a language grows super-linearly with the number of things added to it. There is beauty (and productivity) in simplicity. When you need to do 2 things and there are 2 ways of doing each, now you have 4 different combinations. All languages are more or less guilty of this (even those which promise "Just 1 way to do things") but it is undeniable that Rust is joining C++ in the right side of the complexity Bell curve.
let else and let chains aren't new traits, they are syntactical features that make things that people would expect to work, Just Work™. People keep bringing up the complexity of Rust (a valid argument to be made there) but then point at some feature that is removing an arbitrary limitation from the language. And even for cases where a new feature is being added, I can point at the case of ? for error handling that was a new feature, that "complicated" the language, but that very clearly improved the ergonomics of the language for reading and writing. Should ? have been left in the cutting floor because it didn't look like anything else and was a "redundant" feature?
Let me put it another way: what Rust feature would you remove to "simplify" the language?
As I pointed here originally, you need to be very careful about what you ADD to a language, because once the cat is out of the bag there is no going back, people are going to use that stuff. That's why I dont begrudge the attitude of the golang maintainers to be very slow in introducing stuff, because it is basically an irreversible step.
I suppose every thing in Rust has a raison d'etre but you pay with complexity that versatility. I think there is space now for a modern, memory-safe, SIMPLE, systems programming language. C has the backwards compatibility problem (although I am still bullish on its future) and a language like Zig never got any traction. Hopefully the future will bring new, interesting stuff.
As far as I can tell, Go was kinda rushed, and they called it 1.0 before they get to add the generics it obviously needed. And I bet adding those generics in a way that preserves backward compatibility was a bear.
I think it's because of the expression focus. Isn't it easier to make the code flow like a waterfall when it's imperative, but is harder to reason about values and state.
The result type is the return from Foo -- Bar::Fail does not need to wrap Result. Foo is Result<T, E> and map_err() would convert it to Result<T, Bar::Fail>. I think GP's `map_err()?` is the most straightforward way of writing this idea (and it's generally speaking how I would suggest writing Rust code).
You'd be surprised. For every person that things exit early is good, you'll run into another that prefers a single exit. At worked at a C++ shop that preferred "single exit", and some methods with an ungodly amount of conditions just to make this possible. Ugh.
In my experience, a preference for single exit comes from C where you always need to make sure to clean up any resources, and an early exit is a great way to have to duplicate a bunch of cleanup logic or accidentally forget to clean things up.
I love Rust and use it everyday but the syntax bloat is something I will never get over. I don't believe there's nothing that could be done about it. There are all sorts of creative grammar paths one could take in designing a language. An infinite amount, in fact. I would really like to see transpiler that could introduce term rewriting techniques that can make some of that syntax go away.
It's a pain to write all that boilerplate, I agree. I don't think it's bloat though - I've been doing rust for a few years now, and when I revisit old mostly forgoten code, I love that boilerplate. I rarely have to do any puzzling about how to infer what from the current file, it's just all right there for me.
I feel this way about all the verbosity in rust - some of it could likely be inferred, but but having it all written down right where it is relevant is great for readability.
Having done a bit of C lately (lots in the past) and quite a bit of Rust, Rust is not verbose!
The functional syntax the author of this (good) article complains about is what this (long experience in procedural C like languages) old programmer has come to love.
>when I revisit old mostly forgoten code, I love that boilerplate. I rarely have to do any puzzling about how to infer what from the current file, it's just all right there for me.
This is going to sound absurd, but the only other language I had this experience with was Objective-C.
Verbosity is super underrated in programming. When I need to come back to something long after the fact, yes, please give me every bit of information necessary to understand it.
This is a really good point, IMO. I've never written extensive amounts of Objective-C, but in my adventures I've had to bolt together GUIs with Objective-C++ and I learned to love the "verbose" nature of Obj-C calls whenever I had to dive back into the editor for a game engine or whatever because it meant I didn't have to rebuild so much state in my head.
That's true, I found this writing F# with an IDE vs reading F# in a PR without IDE it really becomes easier to read if you at least have the types on the function boundary.
F# can infer almost everything. It's easier to read when you do document some of the types though.
> F# can infer almost everything. It's easier to read when you do document some of the types though.
F# is also easier to avoid breaking in materially useful ways if (like TypeScript) you annotate return types even if they can be inferred. You'll get a more useful error message saying "hey stupid, you broke this here" instead of a type error on consumption.
"Creative" grammar introduces parsing difficulties, which makes IDE tooling harder to build and less effective overall. My overall guess is that Rust made the right choices here, though one can endlessly bikeshed about specifics.
Creative grammar can introduce parsing difficulties, but it doesn't have to.
I've made a couple small languages, and it's easy to end up lost in a sea of design decisions. But there are a lot of languages that have come before yours, and you can look to them for guidance. Do you want something like automatic semicolon insertion? Well, you can compare how JavaScript, Python[1], Haskell, and Go handle it. You can even dig up messages on mailing lists where developers talk about how the feature has unexpected drawbacks or nice advantages, or see blog posts about how it's resulted in unexpected behavior from a user standpoint.
You can also take a look at some examples of languages which are easy or hard to parse, even though they have similar levels of expressivity. C++ is hard to parse... why?
You'd also have as your guiding star some goal like, "I want to create an LL(1) recursive descent parser for this language."
There's still a ton of room for creativity within constraints like these.
[1]: Python doesn't have automatic semicolon insertion, but it does have a semicolon statement separator, and it does not require you to use a semicolon at the end of statements.
> you can look to them for guidance. Do you want something like automatic semicolon insertion? Well, you can compare how JavaScript, Python[1], Haskell, and Go handle it
You can't look at JavaScript/Python/Go (I don't know about Haskell), because Rust is a mostly-expression language (therefore, semicolons have meaning), while JavaScript/Python/Go aren't.
The conventional example is conditional assignment to variable, which in Rust can be performed via if/else, which in JS/Python/Go can't (and require alternative syntax).
> You can't look at JavaScript/Python/Go (I don't know about Haskell), because Rust is a mostly-expression language (therefore, semicolons have meaning), while JavaScript/Python/Go aren't.
I have a hard time accepting this, because I have done exactly this, in practice, with languages that I've designed. Are you claiming that it's impossible, infeasible, or somehow impractical to learn lessons from -- uhh -- imperative languages where most (but not all) programmers tend to write a balance of statements and expressions that leans more towards statements, and apply those lessons to imperative languages where most (but not all) programmers tend to write with a balance that tips more in the other direction?
Or are you saying something else?
The fact that automatic semicolon insertion has appeared in languages which are just so incredibly different to each other suggests, to me, that there may be something you can learn from these design choices that you can apply as a language designer, even when you are designing languages which are not similar to the ones listed.
This matches my experience designing languages.
To be clear, I'm not making any statement about semicolons in Rust. If you are arguing some point about semicolon insertion in Rust, then it's just not germane.
Not the parent, but you can certainly have an expression-oriented language without explicit statement delimiters. In the context of Rust, having explicit delimiters works well. In a language more willing to trade off a little explicitness for a little convenience, some form of ASI would be nice. The lesson is just to not extrapolate Rust's decisions as being the best decision for every domain, while also keeping the inverse in mind. Case in point, I actually quite like exceptions... but in Rust, I prefer its explicit error values.
> I have a hard time accepting this, because I have done exactly this, in practice, with languages that I've designed.
I don't know which your languages are.
Some constructs are incompatible with optional semicolons, as semicolons change the expression semantics (I've given an example); comparison with languages that don't support such constructs is an apple-to-oranges comparison.
An apple-to-apple comparison is probably with Ruby, which does have optional semicolons and is also expression oriented at the same time. In the if/else specific case, it solves the problem by introducing inconsistency, in the empty statement, making it semantically ambiguous.
Have you also written tooling - e.g. code completion in an IDE - for those small languages? There are many things that might be easy to parse when you're doing streaming parsing, but a lot more complicated when you have to update the parse tree just-in-time in response to edits, and accommodate snippets that are outright invalid (because they're still being typed).
Yes, that's a good example of exactly what I'm talking about. Code completion used to be really hard, and good code completion is still hard, but we have all these different languages to learn from and you can look to the languages that came before you when building your language.
Just to give some more detail--you can find all sorts of reports from people who have implemented IDE support, talking about the issues that they've faced and what makes a language difficult to analyze syntactically or semantically. Because these discussions are available to sift through in mailing lists, or there are even talks on YouTube about this stuff, you have an wealth of information at your fingertips on how to design languages that make IDE support easier. Like, why is it that it's so hard to make good tools for C++ or Python, but comparatively easier to make tools for Java or C#? It's an answerable question.
These days, making an LSP server for your pet language is within reach.
I'm not an expert as I do not work on these tools but I don't think IDEs can rely solely on ASTs because not all code is in a compilable state. Lots of times things have to be inferred from invalid code. Jetbrains tools for example do a great job at this.
In practice though, getting the AST from the text is a computational task in and of itself and the grammar affects the runtime of that. For instance, Rust's "turbofish" syntax, `f::<T>(args)`, is used to specify the generic type for f when calling it; this is instead of the perhaps more obvious `f<T>(args)`, which is what the definition looks like. Why the extra colons? Because parsing `f<T>(args)` in an expression position would require unbounded lookahead to determine the meaning of the left angle bracket -- is it the beginning of generics or less-than? Therefore, even though Rust could be modified to accept`f<T>(args)` as a valid syntax when calling the function, the language team decided to require the colons in order to improve worst case parser performance.
It's not impossible to handle the ambiguity, it's just that you may have to look arbitrarily far ahead to resolve it. Perhaps C# simply does this. Or perhaps it limits expressions to 2^(large-ish number) bytes.
Rust macros are one of the more annoying features to me. They're great at first glance but whenever I want to build more fancy ones I constantly bump into limitations. For example they seem to be parsed without any lookahead, making it difficult to push beyond the typical function call syntax without getting compiler errors due to ambiguity.
But proc macros are limited by requiring another crate (unless things have changed in the last year). Sure, it’s just one extra crate in the project, but why must I be forced to?
Why is that weird? Procedural macros are compiler plugins. They get compiled for the platform you're building on, not the one you're building for, and so they need to be a separate compilation unit. In Rust, the crate is the compilation unit.
Nim allows you to chose what memory management method you want to use in a particular piece of software. It can be one of various garbage collectors, reference counting or even no memory management. It allows you to use whatever suits your needs.
I don’t understand why you all are posting tedious details and well actuallys when the original assertion was (way back):
> Nim, which technically accomplishes all (I assume) of the Rusty things that require syntax, manages to do it with quite a lot nicer syntax.
Nim does not have something which gives both memory safety and no ((tracing garbage collector) and/or (reference counting)) at the same time. End of story.
The fact that Nim has an off-switch for its automatic memory management is totally uninteresting. It hardly takes any language design chops to design a safety-off button compared to the hoops that Rust has to jump through in order to keep its lifetimes in check.
You are simply incorrect, appear unwilling to research why/appear absolutist rather than curious, and have made clear that what I think is "clarification" or "detail expansion" you deem "tedious" or "nitpicking" while simultaneously/sarcastically implicitly demanding more details. That leaves little more for me to say.
You have managed to point out that tracing garbage collection and reference counting are indeed two ways to manage memory automatically. Three cheers for your illuminating clarification.
While tracing garbage collection is indeed one possible automatic memory management strategy in Nim, the new --mm:arc may be what darthrupert meant. See https://uploads.peterme.net/nimsafe.html
Nim is choice. :-) {EDIT: As DeathArrow also indicated! }
Terminology in the field can indeed be confusing. In my experience, people do not seem to call reference counted C++ smart pointers "garbage collection" (but sure, one/you might, personally).
"Automatic vs manual" memory management is what a casual PL user probably cares about. So, "AMM" with later clarification as to automation options/properties is, I think, the best way to express the relevant ideas. This is why I said "tracing GC" and also why Nim has recently renamed its --gc:xxx CLI flags to be --mm:xxx.
Whether a tracing collector is even a separate thread or directly inline in the allocation code pathway is another important distinction. To muddy the waters further, many programmers often mean the GC thread(s) when they say "the GC".
What runtimes are available is also not always a "fixed language property". E.g., C can have a tracing GC via https://en.wikipedia.org/wiki/Boehm_garbage_collector and you can get that simply by changing your link line (after installing a lib, if needed).
People don't call reference counted C++ smart pointers "garbage collection", because they aren't managed by the runtime, nor optimized by the compiler, rather rely on basic C++ features.
But they call C++/CX and C++/CLI ref types, automatic memory management, exactly because they are managed by the UWP and CLR runtimes respectively,
I doubt you are, exactly, but I think it's really hard to argue that the terminology, as often used by working programmers, does not confuse. ("Need not" != "does not"!) All that happened here is darthrupert made vague remarks I tried to clarify (and j-james did a better job at [1] - sorry!). Most noise since has since been terminology confusion, just endemic on this topic, embedded even in your reply.
I may be misreading your post as declaration rather than explanation of confusion, but on the one hand you seem to write as if "people not calling RC smart ptrs 'GC' is 'reasonable'" yet on the other both your two books include it as a form of "direct GC" - GC Handbook: The Art of AMM with a whole Chapter 5 and the other early in the abstract. darthrupert just reinforced "working programmer usage" being "not academic use" elsewhere. [2] GCHB even has a glossary - rare in CS books (maybe not in "handbooks"?) So, is your point "Academics say one thing, but 'People' another?"
C++ features you mention were intended to blur distinctions between "compiler/run-time supported features", "libraries", and "user code". Many PLs have such blurring. Such features, basic or not, are optimized by compilers. So, neither compiler support nor "The Runtime" are semantic razors the way I think you would like them to be (but might "explain people/working programmers"). If one "The" or "collection" vs. "collector" are doing a lot of semantic work, you are in confusing territory. Also, human language/terms are cooperative, not defined by MS. MS is just one more maybe confusing user here.
Between intentional blurriness, loose usage, and many choices of both algos & terms used in books, papers, documentation and discussions, and the tendency for people to just "assume context" and rush to judgements, I, for one, don't see existence of confusion as mysterious.
Given the confusion, there seems little choice other than to start with a Big Tent term like "memory management" and then qualify/clarify, though many find "not oversimplifying" tedious. I didn't think this recommendation should be contentious, but oh well.
I see now that the GP wrote “a garbage collector” (not the article). Oops! “A reference counting method” doesn’t roll off the tongue. So it appears that your nitpicking was indeed appropriate.
See the neighboring subthread: https://news.ycombinator.com/item?id=31438134 (which has details/links to more information and is more explicit than just the 4th footnote at the end of the mentioned twice before peterme link.)
I'm curious what that assumption is based on. Rust and Nim are pretty different, and both of them have features that the other doesn't even try to have.
Nim's modern memory management (ARC/ORC) is fairly similar to Rust. ARC functions by reference-counting at compile time and automatically injecting destructors: which is broadly comparable to Rust's ownership + borrow checker.
(A big difference is that Nim's types are Copy by default: this leads to simpler code at the expense of performance. You have control over this, keeping memory safety, with `var`, `sink`, and others, as highlighted in the above link.)
Nim passes by value by default, which eliminates much of the complexity overhead of lifetimes and borrowing in most programs. (the compiler does optimize some of these into moves.)
But when you do want to pass by reference: that's where Nim's move semantics come in. These are what are fairly similar to Rust's lifetimes and borrowing, and what the paste.sr.ht link briefly goes over.
If you're interested, you can read more about Nim's move semantics here:
Tongue in cheek: Then it's exactly like (modern) Nim, only that Nim does the fallbacking automatically as needed ;) There are lots of devils in the details, I assume.
Rust had a lot to go against so I can't blame them for somehow subpar syntax. Maybe it's gonna be revised.. maybe some guys will make a porcelain layer or a rustlite.
There's definitely a space for native languages that are not as dense and performant possibly as Rust. I will trade some readability when I need strict memory guarantees and use Rust, but most of my time I'd like to use something readable and fun to use, which Rust ain't.
I used to use Go, not much of a fan anymore, but I'm liking Crystal a lot to fill this space. Eventually Zig when it's more mature.
Not enough syntax sugar, not functional enough, and a few smaller papercuts.
It feels dry, like a humourless android. Not very fun to write but probably the most pragmatic choice for the problem. I prefer having fun when I program.
I tend to see that as an advantage - the language gets out of the way and lets me just have fun with the problem itself. Especially if I'm referencing code other people wrote (ie libraries) where I can pretty much instantly grok what's going on and focus on the logic. Different strokes
Your summary is the thing I struggle with as well. How do you deal with the issues of density without either making it more verbose by a wide margin (which also hampers readability) or hiding information in a way that makes the code less obvious which is, IMO, worse.
Software is becoming more and more complex and unless there are entirely different design patterns we have failed to find, managing and understanding that during both the writing and the maintenance of software is the fundamental problem of our time. Someone else in these comments mentioned leaning more heavily into IDE tooling and I do wonder if we are coming to a point where that makes sense.
> unless there are entirely different design patterns we have failed
It’s not that we’ve failed to find different design patterns, it’s that we found these patterns in the 70s and haven’t done much with them since. Since C there has been a pretty constant march toward more imperative programming, but imperative programming I feel has reached its peak for the reasons you describe.
We’re only just starting to explore the functional programming space and incorporate those learnings into our work. But what about logic programming, dataflow programming, reactive programming, and other paradigms that have been discovered but not really fully explored to the extent imperative programming has been? I think there’s a lot of room for improvement just by revisiting what we’ve already known for 50 years.
The imperative design matches the hardware too well to just dispense with. Rust's abstractions are probably the closest we've gotten to a composable design that fits closely to the hardware while mechanically preventing writing the most common bugs at that level.
That said I agree that we've barely scratched the surface of the space of good paradigms; I'm partial to logic programming but most are underexplored. Perhaps other approaches can use Rust or its IR (MIR?) as a compilation target. As an example, this strategy is being used by DDlog ( https://github.com/vmware/differential-datalog ).
> The imperative design matches the hardware too well to just dispense with.
I don't think we should dispense with it for that reason, but we also then have to admit imperative programming doesn't match the design of promised future hardware as well as it has past hardware. The future will be focused around manycore distributed heterogenous compute resources like GPGPU, neural cores, computer vision accelerators, cloud compute resources, etc.
Yeah future computing hardware will be more diverse, but most of the things you mentioned are ultimately be programmed imperatively. GPGPUs are converging to look like many tiny general purpose CPUs, neural cores and accelerators are implemented as specialized coprocessors that accept commands from CPUs, distributed cloud compute is just farms of CPUs. All of these things have an imperative kernel, and importantly every one of them care very much about path dependencies, memory hierarchy, and ownership semantics, the very things that Rust brings to the table.
Even languages with 'exotic' execution semantics like prolog's Unification is actually implemented with an imperative interpeter like the Warren Abstract Machine/WAM, and there's not an obvious path to implementing such unorthodox semantics directly in hardware. Low-level imperative programs aren't going away, they're just being isolated into small kernels and we're building higher level abstractions on top of them.
Sure, but that's not really imperative programming. That's imperative programs finding their correct niche. It's a shift in perspective. Today we do imperative programming with a little distributed/async/parallel/concurrent programming sprinkled in. In the future distributed/async etc. will be the default with a little imperative programming sprinkled in.
> That said, I've mostly reached the conclusion that much of this is unavoidable. Systems languages need to have lots of detail you just don't need in higher level languages like Haskell or Python, …
I am not convinced that there's so more to Rust than there is to GHC Haskell to justify so much dense syntax.
There's many syntax choices made in Rust based, I assume, on its aim to appeal to C/C++ developers that add a lot of syntactic noise - parentheses and angle brackets for function and type application, double colons for namespace separation, curly braces for block delineation, etc. There are more syntax choices made to avoid being too strange, like the tons of syntax added to avoid higher kinded types in general and monads in particular (Result<> and ()?, async, "builder" APIs, etc).
Rewriting the example with more haskell-like syntax:
It's a tortuous example in either language, but it still serves to show how Rust has made explicit choices that lead to denser syntax.
Making a more Haskell-like syntax perhaps would have hampered adoption of Rust by the C/C++ crowd, though, so maybe not much could have been done about it without costing Rust a lot of adoption by people used to throwing symbols throughout their code.
(And I find it a funny place to be saying _Haskell_ is less dense than another language given how Haskell rapidly turns into operator soup, particularly when using optics).
> In more plain terms, the line above does something like invoke a method called “to_read” on the object (actually `struct`) “Trying”...
In fact, this invokes an associated function, `to_read`, implemented for the `Trying` type. If `Trying` was an instance `Trying.to_read...` would be correct (though instances are typically snake_cased in Rust).
I'll rewrite the line, assuming `syntax` is the self parameter:
> like the tons of syntax added to avoid higher kinded types in general and monads in particular (Result<> and ()?, async, "builder" APIs, etc).
Rust is not "avoiding" HKT in any real sense. The feature is being worked on, but there are interactions with lifetime checking that might make, e.g. a monad abstraction less generally useful compared to Haskell.
I wanted to learn Go while working professionally with PHP and Python. I loved the simplicity and syntax of Go overall. I learned Go enough to build a small internal tool for our team and it is Production ready (at least internally). Then I wanted to learn Rust since it is so popular and always compared with Go and the syntax made me lose interest. Rust may be amazing and I will be more open minded to try later but it didn't spark the interest. Superficial I know since the real power is in functionality etc but just an anecdote from an amateur.
Another point is just about the maturity of language and libraries.
I started learning Rust recently and when searching how to do some pretty basic stuff, the answers are like "well you used to do this but now you do this and soon you'll be able to do this but it's not stabilized"
I figure I'll just check back in 5 years, I don't have the time or patience to be on the bleeding edge when I'm trying to get things done.
The frontend frameworks used to have really short lifecycles "Oh,you're still using FooJS? That's so last year. Everyone's on Bar.js now- it's so much better"
Completely agree. I think of the extra syntax as us helping the compiler check our code. I have to write a few more characters here and there, but I spend way less time debugging.
Although I may have PTSD from Rust, because lately I find myself preferring Qbasic in my spare time. ¯\_(ツ)_/¯
>That said, I've mostly reached the conclusion that much of this is unavoidable. Systems languages need to have lots of detail you just don't need in higher level languages like Haskell or Python, and trait impls on arbitrary types after the fact is very powerful and not something I would want to give up.
Have you checked out C++20 concepts? It supports aliases and doesn't require explicit trait instantiations, making it possible to right such generic code with much less boilerplate.
My experience in C++ prior to 20 is that it is a lot more verbose/boilerplatey than Rust. I'd love to see that get better, but I think C++ is starting from significantly behind.
There is an equivalent syntax in Rust to both of those examples, and in both cases I find it less verbose. The template variant is roughly equivalent to:
fn func<T: FloatingPoint>(fp: T) { ... }
And the "auto" variant is similar to impl argument in Rust:
I really don't see in which way the second case is less verbose especially if you add a non-void return type, e.g. i32. The first case would also be doable like this, which is pretty munch the exact same than your first example with the added "template" keyword
(also, it's not really equivalent - if I'm not mistaken with traits you can only use what the trait declares ; in C++ you can for instance do something like
> instead of polluting the prototype with all possible side concerns)
C++ Concepts are duck typed, and Rust's Traits are not, so in Rust you are expressing meaning here, and in C++ only making some claims about syntax which perhaps hint at meaning.
WG21 seems to dearly wish this wasn't so, offering Concepts which pretend to semantics they don't have, such as std::totally_ordered and I'm glad to see your "CanBlah" concept doesn't do this, to be sure all things which match this requirement can, indeed blah() although we've no idea what that can or should do.
Once you've accepted that you only have duck typing anyway, you're probably going to have to explain in your documentation the actual requirements for this parameter t, as the prototype merely says it CanBlah and that's not actually what we care about.
In contrast the Rust function we looked at actually does tell us what is required here, something which "implements FloatingPoint", and that implementation (plus the data structure itself) is all that's being exposed.
I don't understand - you seem to say that duck typing is a bad thing. In my experience, some parts of a program have to be strongly typed and some have to be "lightly" - I'd say that a good 5% of my work is to make C++ APIs that look&feel closer to dynamic languages with even less typing checks than the C++ baseline.
> WG21 seems to dearly wish this wasn't so
how so ?
> Once you've accepted that you only have duck typing anyway, you're probably going to have to explain in your documentation the actual requirements for this parameter t, as the prototype merely says it CanBlah and that's not actually what we care about.
the documentation having to state "t can be logged" would just be useless noise and a definite no-pass in code review aha
> In contrast the Rust function we looked at actually does tell us what is required here, something which "implements FloatingPoint", and that implementation (plus the data structure itself) is all that's being exposed.
my personal experience from other languages with similar subtyping implementation (ML-ish things) is that this looks good in theory but is just an improductive drag in practice
But this introduced another case where C++ has a "false positive for the question: is this a program?" as somebody (Chandler Carruth maybe?) has put it. If something satisfies a Concept, but does not model the Concept then the C++ program is not well formed and no diagnostic is required.
> how so ?
I provided an explanation with an example, and you elided both.
> the documentation having to state "t can be logged" would just be useless noise and a definite no-pass in code review aha
In which case it's your responsibility to ensure you can log this, which of course CanBlah didn't express.
> similar subtyping implementation
The only place Rust has subtyping is lifetimes, so that &'static Foo can substitute for any &'a Foo and I don't think that's what you're getting at.
> WG21 seems to dearly wish this wasn't so, offering Concepts which pretend to semantics they don't have, such as std::totally_ordered and I'm glad to see your "CanBlah" concept doesn't do this, to be sure all things which match this requirement can, indeed blah() although we've no idea what that can or should do.
I don't understand how random assumptions on what WG21 may or may not think counts as an example (or anything to be honest)
> If something satisfies a Concept, but does not model the Concept then the C++ program is not well formed and no diagnostic is required.
uh... no ?
I think that you are referring to this blog post: https://akrzemi1.wordpress.com/2020/10/26/semantic-requireme... for which I entirely disagree with the whole premise - the only, only thing that matters is what the compiler understands. The standard can reserve itself the right to make some cases UB, such as when trying to sort un-sortable things just like it can state that adding to numbers can cause UB and that's fine: it's the language's prerogative and is all to be treated as unfortunate special-cases ; for the 99.99999% remaining user code, only the code matters and it makes no sense to ascribe a deeper semantic meaning to what the code does.
> In which case it's your responsibility to ensure you can log this, which of course CanBlah didn't express.
A metric ton of side concerns should not be part of the spec, such as logging, exceptions, etc - everyone saw how terrible and counter-productive checked exceptions were in java for instance. Specifying logging explicitly here would be a -2 in code review as it's purely noise: the default assumption should be that everything can log.
> The only place Rust has subtyping is lifetimes, so that &'static Foo can substitute for any &'a Foo and I don't think that's what you're getting at.
Actually, yes. I was hoping people like you were familiar, but that was actually more to ask of you than I'd assumed since C++ has a tremendous amount of such language in the standard, going in I'd figured hey maybe there's a half dozen of these and I was off by at least one order of magnitude. That's... unfortunate.
Exactly which language ended up being in the "official" ISO standard I don't know, but variations on this are in various circulating drafts through 2020 "[if] the concept is satisfied but not modeled, the program is ill-formed, no diagnostic required", if you're trying to find it in a draft you have, this is in the Libraries section in drafts I looked at, although exactly where varies. [ Yes that means in principle if you completely avoid the C++ standard library this doesn't apply to you... ]
> I think that you are referring to this blog post
It's possible that I've read Andrzej's post (I read a lot of things) but I was just reporting what the standard says and all Andrzej seems to be doing there is stating the obvious. Lots of people have come to the same conclusion because it isn't rocket science.
> only the code matters and it makes no sense to ascribe a deeper semantic meaning to what the code does.
This might be a reasonable stance if the code wasn't written by people. But it is, and so the code is (or should be) an attempt to express their intent which is in fact semantics and not syntax.
But let's come back to std::totally_ordered, although you insist it doesn't "count as an example" for some reason, it is in fact a great example. Here's a standard library concept, it's named totally_ordered, so we're asking for a totally ordered type right? Well, yes and no. Semantically this is indeed what you meant, but C++ doesn't provide the semantics, C++ just gives you the syntax check of std::equality_comparable, and if that's a problem you're referred to "No diagnostic required".
> "[if] the concept is satisfied but not modeled, the program is ill-formed, no diagnostic required",
Couldn't find anything ressembling this in the section of the standard describing concepts and constraints. The spec is very clear (C++20 7.5.7.6):
> The substitution of template arguments into a requires-expression may result in the formation of invalid
types or expressions in its requirements or the violation of the semantic constraints of those requirements. In
such cases, the requires-expression evaluates to false; it does not cause the program to be ill-formed.
Maybe the stdlib has different ording, but the stdlib can literally have any wording it wants and could define std::integer to yield 2+2 = 5 without this being an issue.
> [ Yes that means in principle if you completely avoid the C++ standard library this doesn't apply to you... ]
in just a small library i'm writing, there's already ten-fold the number of concepts than there are defined in the standard library, so I'd say that this does not apply in general ; the stdlib is always an irrelevant special case and not representative of the general case of the language, no matter how hard some wish it. E.g. grepping for 'concept [identifier] =' in my ~ yields 2500 results, with only a small minority of those being the std:: ones.
> This might be a reasonable stance if the code wasn't written by people. But it is, and so the code is (or should be) an attempt to express their intent which is in fact semantics and not syntax.
I think this is very misguided. I am not programming for humans to process my code, but for computers to execute it. That's what comes first.
> Semantically this is indeed what you meant,
no, if I type std::totally_ordered, I mean "whatever the language is supposed to do for std::totally_ordered", and exactly nothing else
As someone who has actually tried writing a kernel in Swift, the issue is purely the runtime. While you can technically build a module without it needing to link to external Swift standard library binaries, the second you try and do anything with an array or optionals, you suddenly need to link in a 15MB behemoth that requires SIMD to even compile (at least on x64 and arm64). Porting this to bare metal is possible (and some have done it for a few microcontrollers) but its a pain in the ass.
I do love Swift and would use it for systems stuff in a heartbeat if I could, but there are also some downsides that make it pretty awkward for systems. The performance isn't always the best but it's (generally) very clear and ergonomic.
Perhaps not that that much. Swift’s arrays are refcounted. And you can’t store an array on the stack. Classes are refcounted too, but you could avoid them. It also has a bit of a runtime, and you don’t know when it will take locks or allocate (though there is work to tag functions so they can’t do either).
The main Rust syntax is OK, but as the author points out, macros are a mess.
The "cfg" directive is closer to the syntax used in ".toml" files than to Rust itself, because some of the same configuration info appears in both places. The author is doing something with non-portable cross platform code, and apparently needs more configuration dependencies than most.
Maybe we've reached the limits of the complexity we can handle in a simple text-based language and should develop future languages with IDEs in mind. IDEs can hide some of the complexity for us, and give access to it only when you are digging into the details.
This just plasters over the underlying problem, which in case of Rust is IMO that features that should go into the language as syntax sugar instead are implemented as generic types in the standard library (exact same problem of why modern C++ source code looks so messy). This is of course my subjective opinion, but I find Zig's syntax sugar for optional values and error handling a lot nicer than Rust's implementation of the same concepts. The difference is (mostly): language feature versus stdlib feature.
Rust developers are doing an awesome job of identifying those things and changing the language to meet it. Today's Rust is much cleaner than it was 5 years ago (or 8 if you count nightly).
But yes, there is still a lot of it.
Anyway, most of the noise comes from the fact that Rust is a low level language that cares about things like memory management. It's amazing how one is constantly reminded of this by the compiler, what is annoying, but the reason it doesn't happen on the alternatives is because they never let you forget about that fact.
For instance a function which returns an optional pointer to a 'Bla':
fn make_bla() ?*Bla {
// this would either return a valid *Bla, or null
}
A null pointer can't be used accidentally, it must be unwrapped first, and in Zig this is implemented as language syntax, for instance you can unwrap with an if:
if (make_bla()) |bla| {
// bla is now the unwrapped valid pointer
} else {
// make_bla() returned null
}
...or with an orelse:
const bla = make_bla() orelse { return error.InvalidBla };
...or if you know for sure that bla should be valid, and otherwise want a panic:
const bla = make_bla().?;
...error handling with error unions has similar syntax sugar.
It's probably not perfect, but I feel that for real-world code, working with optionals and errors in Zig leads to more readable code on average than Rust, while providing the same set of features.
I don't see how that is all that different from Rust.
The main difference I see is that in Rust it will also work with your own custom types, not just optional.
fn make_bla() -> Option<Bla> {
// this either returns a valid Bla, or None
}
if let Some(bla) = make_bla() {
// bla is now the unwrapped valid type
} else {
// make_bla() returned None
}
..or with the '?' operator (early return)
let bla = make_bla().ok_or(InvalidBla)?;
..or with let_else (nightly only but should be stable Soon(tm))
let Some(bla) = make_bla() else { return Err(InvalidBla) }
LSPs are great, I think they've proven fairly easy to integrate into many text editors. But consider something like the Scratch programming language. How many editors support Scratch? Once you stray from code-as-text, adding support to old editors often becomes infeasible and the effort needed to create new editors is a significant barrier to entry.
People seem allergic to anything that isn't superficially ALGOL like. I still remember Facebook had to wrap Ocaml in curly braces because it would apparently blow peoples minds.
A function returns a Result. This concept in Rust is so ubiquitous that it should be a first class citizen. It should, under all circumstances, be syntactically implicit:
```pub fn better->self```
No matter what it takes to make the compiler smarter.
That is not, in fact, a core concept in Rust. Plenty of functions have no reason to return Result. (And some that do also have a reason for the inner class to be a result.)
> This concept in Rust is so ubiquitous that it should be a first class citizen. It should, under all circumstances, be syntactically implicit:
“Implicit” is an opposed concept to “first-class citizen”. Result is first-class in Rust, and would not be if function returns were implicitly Result.
If you don't see std::result::Result as a core concept in Rust, which might be fair, one can still argue that it _should_ be a core concept, given its ubiquitous usage.
You misquoted, I never said Result is not a core concept.
What I said is that “A function returns Result” in the universal sense (that is, everything that is a function returns Result) is not a core concept in Rust.
Some functions return Result<T,E> for some <T,E>. Some functions return Option<T> for some T. Some functions have no reason to use that kind of generic wrapper type (a pure function that handles any value in its range and returns a valid value in a simple type for each doesn't need either; Option/Result are typically needed with otherwise non-total functions or functions that perform side effects that can fail.)
> I remain convinced that the whole Result concept was just created by people butt-hurt over the concept of exceptions
I wouldn't use the emotionally-loaded dismissive language, but, yes, Result is a solution to the same problem as exceptions that deals with several problems of exceptions, including:
(1) Unchecked exceptions obscure what is going on, and frustrate analysis because things remote from the code may bypass it in the call-stack without any evidence visible in signatures.
(2) Checked exceptions are clear, but create a separate syntax for expressing type-like constraints, also limiting what you can do around them because they aren't the same thing as types.
The sad part is that Rust still has exceptions, they're just called panics.
I love Rust to death, but I feel this is a huge flaw in its design. Writing any unsafe code that takes a callback function is a massive pain in the ass, because anytime you call this callback, it could panic, so you must restore into a safe state if stack unwinding occurs.
For example, I'm currently writing a fast sorting algorithm, using mostly unsafe code (by necessity). Every time I compare two elements, a panic could occur, and all elements have to be placed back in the input array to restore to a safe state. It's hellish.
There is the panic = abort flag which is great if you're writing a binary. But as a library you can not assume this.
I'll certainly grant that unchecked exceptions are problematic for static analysis, but in regards to your second point, I don't feel like Rust has actually avoided creating "a separate syntax". It's created a different, more complex syntax which must be adopted inline in your actual normal code path, obfuscating what your code is actually expected to do under non-error conditions.
IMO, one of the most valuable pieces of exception handling is a distinct separation between your error logic and your non-error logic, which makes methods easier to comprehend. I also feel like the existence of the ? syntax is a dead giveaway in this regard because it's a fig-leaf trying to cover up the most egregious parts of the code where you'd otherwise have to be write the frequent "if error then early return error" statements which plague Golang.
The main reason behind the panic/Result distinction is that systems programmers, particularly those working in embedded, want to be able to turn off unwinding support entirely. Unwinding adds control flow edges everywhere, inhibiting optimizations, and it either adds code size bloat for the unwind tables or runtime overhead at every function call, depending on how it's implemented. I don't know of any way to implement exceptions that doesn't have this overhead. So although I like exceptions myself, I agree with Rust's decision not to embrace them in its domain.
> but in regards to your second point, I don't feel like Rust has actually avoided creating "a separate syntax"
It avoids creating a separate syntax from the usual return-type declaration syntax for declaring the existence of errors, when compared to checked exceptions.
It also avoids creating a separate syntax for error handling, compared to (checked or unchecked) exceptions (pattern matching is ubiquitous in Rust for other purposes).
> It's created a different, more complex syntax which must be adopted inline in your actual normal code path, obfuscating what your code is actually expected to do under non-error conditions.
Pattern matching isn't an additional syntax (indeed, many languages with exceptions also have it), and it (IMO) does less to obscure non-error code than the visual noise of handling errors with try/catch.
It is more visual noise in the case of functions that do the equivalent of not handling exceptions, compared to exception-using langauges where that is implicit.
This would break the principle that you always know how to invoke a function by looking at its signature. Option of T and Result of T are not the same type as T. You would have to look at the body of the function, or rustdoc, to know how to invoke it, which would be very annoying.
Besides, what is the error type for Result? You haven't declared it.
Others have addressed the problem with "implicit", but I might be on board with "lightweight"; maybe in a type context `T?` can mean `Result<T>` for whatever Result is in scope? That way you can still define functions with various distinct error types the same as today, but the common (idk just how common, not claiming a majority) case of using the same error across a module or package with a Result type alias will get cleaner.
That's a good point, but I am not sure it would actually be a problem.
For one thing, we already have syntactic collisions that don't seem to cause much problem (consider `foo?.bar` in .ts vs .rs), and this one would probably be prevalent enough that it would quickly be familiar to anyone using the language.
For another, if we squint I'm not sure those other languages aren't "really" using it for the same thing. If in some module we define `type Result<T> = Option<T>` then we have the same behavior in Rust, and we can imagine that those other languages have basically done so implicitly, meaning it's a less flexible version of the same mechanism (put to slightly different purposes).
From the modern systems programming languages set, Go does better in this respect. But admittedly it doesn't reach to quite as low in fitness for low level programming as Rust.
Oh, not even close. It does what most languages do and just elides, ignores, or hard-codes the answers to all the questions Rust has. That's a solution, sure, a very valid one chosen by many languages over many decades, but certainly not "much better". We absolutely need at least one language that doesn't hide all that and I think the programming language community as a whole will really benefit from the list of choices for that being expanded from "C++", which is getting really long in the tooth And I'm not even sure C++ was ever really designed to be this language, I think a lot of it just sort of happened by default and it sort of backed into this role, and frankly, it shows. Rust being designed for this can't hardly help but be nicer, even if we completely ripped out the borrow checker aspect.
Depends if one considers compilers, linkers, networking stacks, kernel emulation, unikernels, userspace drivers, databases, GPGPU debugging systems programming or not.
I personally consider better use Go than C for such purposes, even if they aren't "systems programming".
I think this is a matter of opinion not fact. I have worked as a Go programmer for three separate companies and it may be the least readable, least understandable language I have encountered.
Yes. The only people for whom this is controversial are message board nerds. The actual language designers don't have much trouble over the concept. Here's a link to the designers of Rust, C++, Go, and D on a panel having little problem working through the nuances:
This perpetual debate reminds me of the trouble HN used to have with the concepts of "contractors" and "consultants", where any time someone mentioned that they were doing consulting work there'd be an argument about whether it was in fact contracting. It's a message board tic, is what I'm saying, not a real semantic concern.
To be fair, that first question about 'what is a systems programming language' is answered by Rob Pike then Andrei Alexandrescu as
Pike: When we first announced Go we called it a systems programming language, and I slightly regret that because a lot of people assumed that meant it was an operating systems writing language. And what we should have called it was a 'server writing language', which is what we really thought of it as. As I said in the talk before and the questions, it's turned out to be more generally useful than that. But know what I understand is that what we have is a cloud infrastructure language because what we used to call servers are now called cloud infrastructure. And so another definition of systems programming is stuff that runs in the cloud.
Alexandrescu: I'm really glad I let Rob speak right now because my first question was 'go introduces itself as a systems programming language' and then that disappeared from the website. What's the deal with that? So he was way ahead of me by preempting that possible question.
So it seems to me that they struggle with the nuances of the concept as much as the commenters here, particularly as it pertains to Golang.
Depends if one considers compilers, linkers, networking stacks, kernel emulation, unikernels, userspace drivers, databases, GPGPU debugging systems programming or not.
Despite my opinion on Go's design, I rather would like to see people using Go instead of C for such use cases.
To be fair, compilers and linkers can be thought of as pure functions (if we ignore stuff like including timestamps in builds e.g.). They have no special requirements language-wise. You can write them in any general purpose programming language. Even Brainfuck will do (for one-file programs fed through stdin). No argument about the others though.
Although I guess JIT compilers may be classified as systems programming, since you need to do some OS-specific tricks with virtual memory during execution of the compiler.
Yes, as it's used for that a lot. Eg many databases (CockroachDB, Prometheus, InfluxDB, dgraph etc), gVisor, Kubernetes, Fuchsia, etcd, and so on. And also in the origin story it was aiming to compete with C++ for many use cases IIRC.
That's tricky to answer, because it depends a lot on what you count as "system software". If you mean literally "the operating system", then arguably not. But if you include middleware, databases and other "infrastructure" stuff, then arguably yes.
A proper database can be implemented in python -- I've done it -- but that doesn't make it a systems language. A "systems language" comes with the strong implication that it is possible to write an implementation of most software that is competitive with the state-of-the-art in terms of performance, efficiency, and/or scalability. That is only possible in languages like C, C++, Rust, and similar, hence the "systems language" tag.
Languages that are not systems language trade-off this capability for other benefits like concise expressiveness and ease of use.
Lots of real world systems have other design tradeoffs than aiming to be state of the art in those axis. Eg cost, security, maintainability, adaptability to changes, etc.
Agreed. And I didn't mean to imply that it's impossible to use Go that way, but I think it's fair to say that it's less common and perhaps even less desirable to do that.
OTOH, people have written (at least parts of) Operating Systems in Java[1] even, so never say never...
Yes it is, but not a a good low level systems language mainly due to garbage collection and runtime requirements. It still is used for writing keyword here systems.
It looks like I'm on the minority here, but I generally like Rust's syntax and think it's pretty readable.
Of course, when you use generics, lifetimes, closures, etc, all on the same line it can become hard to read. But on my experience on "high level" application code, it isn't usually like that. The hardest thing to grep at first for me, coming from python, was the :: for navigating namespaces/modules.
I also find functional style a lot easier to read than Python, because of chaining (dot notation) and the closure syntax.
let array = [1, 0, 2, 3];
let new_vec: Vec<_> = array.into_iter()
.filter(|&x| x != 0)
.map(|x| x * 2)
.collect();
I mean, I kind of agree to the criticism, specially when it comes to macros and lifetimes, but I also feel like that's more applicable for low level code or code that uses lots of features that just aren't available in e.g. C, Python or Go.
I guess you're right, list/generator comprehensions are the idiomatic way to filter and map in python, with the caveat of needing to have it all in a single expression (the same goes for lambda, actually).
I still feel like chained methods are easier to read/understand, but list comprehensions aren't that bad.
I realize that "Guido van Possum" was almost certainly a typo here, but it _does_ make for an amusing mental image. I wonder what other forest creatures might have built programming languages? C++ built by Björn "the bear" Stroustrup? C# built by Anders Honeybadger? Ruby by Yukihiro "Catz" Cat-sumoto?
Oh, yeah, you're right! If you want to collect into a Vec you may need to specify the type, but usually, you can just call `.collect()` and the compiler will infer the correct type (as I suppose you're collecting it to use or return).
If it can't infer, it's idiomatic to just give it a hint (no need for turbofish):
let new_vec: Vec<_> = array.into_iter()
.filter(|&x| x != 0)
.map(|x| x * 2)
.collect();
I don't think that's ugly or unreadable.
About the Python list comprehension, I answered your sibling, I think you're both right but it also does have it's limitations and that may be personal, but I find chained methods easier to read/understand.
They maybe rear their ugly head but they also allow you to collect the iterator into any collection written by you, by the standard library or by any other crate.
While in python you have list/dict/set/generator comprehension and that's it.
I don't think it's bad thing. In fact one of my favorite features is that you can do `.collect::<Result<Vec<_>, _>>()` to turn an interators of Results, into a Result of just the Vec if all items succeed or the first error. That is a feature you just can't express in Python.
But you have to admit that is a pretty noisy line that could be difficult to parse.
I believe I have the habit of putting it on the end because the final type might be different. Consider:
let array = ["DE", "AD", "BE", "EF"];
let new_array: Vec<u32> = array.into_inter()
.map(|x| u32::from_str_radix(x, 16))
.collect()?;
In this case you need to specify the Result generic type on generic. This has come up for me when working with Stream combinators. Most projects probably end up in needing some lifetime'd turbofish and you have to be able to parse them. They aren't rare enough, IME, to argue that Rust isn't noisy.
let new_array: Vec<_> = array.into_iter()
.filter(|&x| x != 0)
.map(|x| x * 2)
.collect();
I'm actually a bit confused by the `&x` given that `into_iter()` is used, which would take ownership of the array values, but assuming that it was supposed to be just `iter()` (or that it's an array of &i32 or something I guess), you're going to be copying the integer when dereferencing, so I'd probably just use `Iterator::copied` if I was worried about too many symbols being unreadable:
let new_array: Vec<_> = array.iter()
.copied()
.filter(|x| x != 0)
.map(|x| x * 2)
.collect();
There's also `Iterator::filter_map` to combine `filter` and `map`, although that might end up seeming less readable to some due to the need for an Option, and due to the laziness of iterators, it will be collected in a single pass either way:
let new_array: Vec<_> = array.iter()
.copied()
.filter_map(|x| if x == 0 { None } else { Some(x * 2) })
.collect();
This is definitely more verbose than Python, but that's because the syntax needs to disambiguate between owned and copied values and account for static types (e.g. needing to annotate to specify the return type of `collect`, since you could be collecting into almost any collection type you want). It's probably not possible to handle all those cases with syntax as minimal as Python, but if you are fine with not having fine-grained control over that, it's possible to define that simpler syntax with a macro! There seem to be a lot of these published so far (https://crates.io/search?q=comprehension), but to pick one that supports the exact same syntax in the Python example, https://crates.io/crates/comprende seems to do the trick:
let array = [0, 1, 2, 3];
let new_array = c![x * 2 for x in array if x != 0];
println!("{:?}", new_array); // Prints [2, 4, 6]
I'm not trying to argue that Rust is a 1:1 replacement to Python or that if Python suits your needs, you shouldn't use it; I think it's worth pointing out that Rust has more complex syntax for a reason though, and that it has surprisingly good support for syntactic sugar that lets you trade some control for expressiveness that can alleviate some of the pain you might otherwise run into.
Ah, you're right! I had forgotten filter worked like that. I'll have to concede that equality with references to `Copy` is somewhat annoying when dealing with closures (and closures in general are one of the few parts of Rust I do consistently wish had better ergonomics, although I understand why it's hard).
You can just specify the type along with the variable itself instead of relying on type inference in this case, which makes it look a lot better I think.
There is also the collect_vec method from Itertools that avoids this. I normally am not a big fan of pulling crates for little things like this, but the Itertools crate is used in rustc itself, so you already are trusting it if using rust.
I do agree that rust syntax can be a bit verbose sometimes but I actually prefer the syntax to most other languages! I would have preferred if it would have been less inspired by the C family of languages syntax wise, but that would have likely hindered adoption.
I think part of this comes down to: Does your Rust code make heavy use of generics? I find myself deliberately avoiding generics and libraries that use them, due to the complexity they add. Not just syntactic noise, but complicated APIs that must be explicitly documented; rust Doc is ineffective with documenting what arguments are accepted in functions and structs that use generics.
I want to like rust but the fact that there are replies with 9 different ways to write that loop posted here alone, and no consensus on which one is the idiomatic, is not a good sign.
minor point but your python code creates a generator here not an array, you'd have to wrap it in a `list()` to get the same data type and be able to for example assert its length (of course you can just iterate over the generator)
> This is a superficial complaint, but I found Rust syntax to be dense, heavy, and difficult to read.
I'm not sure this is a superficial complaint. People say the hard thing about learning Rust is the new concepts, but I haven't found that to be true at all. The concepts are easy, but the combinatorial explosion of syntax that supports them is untenable.
Back when I wrote C and C++ for a living I'd occasionally meet someone who thought their ability to employ the spiral rule or parse a particularly dense template construct meant they were a genius. I get the same vibe from certain other groups in this industry, most recently from functional programmers and Rust afficionados, for example. Nobody gives a damn if you can narrate a C spiral or a functional-like Rust idiom.
And this syntax density is one of the reasons I stopped advocating for the use of Rust in our systems. First, I don't want to work with languages that attract this kind of person. Second, I don't want to work with languages that require a relatively heavy cognitive load on simply reading the lines of the source code. Units of code (i.e. statements, functions, structures and modules) are already a cognitive load--and the more important one. Any extra bit I have to supply to simply parsing the symbols is a distraction.
"You get used to it," "with practice it fades to the background," etc. are responses I've seen in these comments, and more generally when this issue comes up. They're inaccurate at best, and often simply another way the above mentioned "geniuses" manifest that particular personality flaw. No, thank you. I'll pass.
> I don't want to work with languages that attract this kind of person
I haven't used Rust professionally, but I find the community extremely inclusive and helpful. I joined the Discord server and asked all sorts of stupid questions and people always helped me and explained to me what was wrong with my code (or my assumptions). But, again, I haven't used Rust professionally and it may be different in that context
> I don't want to work with languages that require a relatively heavy cognitive load on simply reading the lines of the source code
Strongly agree on this, I haven't tried to introduce it where I work for the same reason. The cognitive load is massive compared to a language like C# or JS and the gain is minimal for the average developer writing microservices for React frontends. In this context you need a JSON serializer, iterators and maybe generics, and Rust is not much better than C# on this front.
It may be my limited experience with C#, but I really missed Rust/serde when deserializing a config file into a class/struct. (Actually INI not JSON.) It feels like doing error handling twice: first catching parser exceptions, and later checking for null fields everywhere. (Basically I was forced to default-construct the object before parsing, which either means nullable fields, or having hardcoded defaults for every field.) I guess the pragmatic thing is to turn off null warnings and accept that it may fail later.
> I get the same vibe from certain other groups in this industry, most recently from functional programmers and Rust afficionados, for example.
Another trait in programmers that is worth avoiding is the false equivalency between C++ template metaprogramming and generic programming in languages with expressive static typing.
It's not clever or inscrutable like templates, quite the opposite. It's explicit about constraint. Generic Rust makes it easier to understand complex code and write it correctly. An immediate red flag for me are programmers who don't "get it" because they equate that to some kind of SFINAE or compile time magic they once saw in C++. They're not the same feature, except superficially.
>Second, I don't want to work with languages that require a relatively heavy cognitive load on simply reading the lines of the source code. Units of code (i.e. statements, functions, structures and modules) are already a cognitive load--and the more important one. Any extra bit I have to supply to simply parsing the symbols is a distraction.
The weird thing about these comments to me (as someone who doesn't use Rust) is that the most difficult syntax in the original examples represents a semantic detail that most languages don't have to deal with: the lifetime. The amount of times I think about the lifetimes of variables I write in Python is zero. Parsing the symbols and understanding the code here aren't separate; that weird apostrophe thing in angle brackets is a symbol I don't use referencing a concept I don't use, which fits. If you replaced the symbols with keywords or something, it would just be longer, not simpler.
Also, it's a choice to write your code like he did. You can define local variables that hold intermediate results and subexpressions and give them descriptive names, if you want. You could assign `drop = (|_| ())` for example.
Yea, so pretty much every programmer needs to be aware of thinking about lifetimes.
The classic issues are creating a local and then giving a reference to it, to something much longer lived, or even undying. In that last case, if your doing it a lot, with no regard to the objects change in lifetime, your effectively creating a leak (I can guarantee you someone, somewhere, is making this mistake in js right now). Most collection strategies won't touch an object that still has a reference to it.
The second issue that became really common when people stopped paying attention to lifetimes, is that many resources you may be using (file handles, db connections, etc...) have very different constraints than memory. So you have to be cognizant of the fact that even though something has gone out of scope, it's lifetime really doesn't end until the gc gets around to collecting it. This is the reason special syntax and functions, like "with" and Dispose had to be added to languages like C# and Java. Python with it's reference counting maybe somewhat less susceptible to this second issue than the others, but it's not immune to it.
Finally in many cases being aware of object lifetimes and specifically manipulating them can get you performance speed ups.
> Back when I wrote C and C++ for a living I'd occasionally meet someone who thought their ability to employ the spiral rule or parse a particularly dense template construct meant they were a genius. I get the same vibe from certain other groups in this industry, most recently from functional programmers and Rust afficionados, for example. Nobody gives a damn if you can narrate a C spiral or a functional-like Rust idiom.
I think one problem is dealing with "just because you can doesn't mean you should". It is easy to be nerd-sniped into optimizing everything in Rust. I've seen complain about an arg parser using dynamic dispatch when anything the program actually does will dwarf the time that that takes. I feel we need a reset; a stdlib-alternative that optimized for those learning and prototyping at the cost of performance. I suspect people using that will help break them of the feeling to optimize the trivial but to instead focus on what profilers tell them.
I’m with you. I think people that treat syntax as some completely unimportant detail are forgetting that reading code is a more important use case than writing code.
No matter how much you internalize the syntax of language X, as the sheer number of syntactic structures in the language increases, the higher the likelihood you’ll misread something.
> I get the same vibe from certain other groups in this industry, most recently from functional programmers and Rust afficionados
Perl one-liner guys used to exemplify this. But I don't really agree that functional programmers do, except for Haskell and people who use lots of the car and cdr compositions, or those who use too much metaprogramming, or... okay maybe you're right. But at least the fundamental premise of functional programming is simple..
I don't use Rust a ton, certainly not enough that the syntax density fades into the background, but something I'll say for the ecosystem is rust-analyzer is really good and pretty much always knows and warns you when you're writing something incorrectly that won't compile. The worst parts of the syntax effectively become self-writing, though it does nothing to help reading.
> And this syntax density is one of the reasons I stopped advocating for the use of Rust in our systems.
Trouble is I've found this type of genius is most languages. There are always some esoteric functionality that few people understand that some people will choose because its "the most appropriate" but largely because its a challenge. Of course such talented people move on to the next project quickly as maintaining their crap is not fun.
Depends on the application, really. And I wouldn't call it "advocacy" so much as being resigned to accepting a less odious bad option. In that case, typically Go or Python, unless we need that last bit of performance and can't get it with a pre-built library: then I'd argue for C, C++, and Rust (in that order).
Not the OP, I rather use managed languages with AOT/JIT toolchains.
C++ and Rust I leave for scenarios where choice is imposed on me due to platform SDKs, or having any kind of automatic memory management isn't an option.
Rust's memory semantics are definitely a kind of 'automatic memory management' though. I mean, that's the whole premise - to have the kind of guarantees about memory safety that until Rust where only available in GC'ed languages running on some runtime.
Not sure if Cyclone and ATS don't predate it, but Rust's memory management is a bit like 1/3rd of Linear Lisp (specifically, the compil time garbage optimizer)
While I personally agree, I think the bar for readability gets slowly lowered over time. In the Typescript/JavaScript world a for loop used to be (and still is to some) considered more readable than using the functional array functions.
Our reasoning for readability is usually based on what the average programmer is able to read, so the more programmers get used to dense syntax the more readable it is. There's rarely any "real argument" to be had for or against readability.
Haskell and other languages that are inspired by math look very noisy to me, but at the same time I understand that math people don't agree.
It's not a superficial complaint but it is relative to one's experience. Something that's "difficult" for me might be "easy" for you and vice versa. I find it very much related to understanding the core concepts.
I personally find Rust syntax to be quite enjoyable, or at least it fades into the background quickly - with a few exceptions. The syntax for lifetime annotations can be challenging. And not surprisingly explicit lifetime annotations are a rather unique concept, at least among mainstream languages. IOW the syntax is difficult because it's an entirely new mental model (for me), not because `<'a>` is an inherently bad way to express it.
Readability seems to mean different things to different people. You (and many others!) seem to interpret that word as "there's only relevant information and nothing else in sight". Personally I interpret it as "I have all the relevant information available to me in a way I can scan for quickly". Rust has a higher syntactic load, there are more things present to the reader, but it also means that everything the reader might need is always available, and the syntactical patterns are unique enough that it is "easy" (we can argue this point forever) to skip things you don't care about. When I look at type signatures, sometimes I care about the trait bounds, sometimes I don't. Sometimes I care about the lifetime relationship between different arguments and the output, sometimes I don't. Languages that make these relationships completely implicit make it easy to focus on some aspects of the code's behavior, while obscuring others.
It's hard to optimize for readability, performance, and safety. Rust chose to go with performance and safety. In the future, maybe we can have a language that gives all three but not today.
Shameless relevant plug: that's the exact goal of Vale! [0]
It turns out, when one removes the borrow checker, they get something that's much more readable, because a lot of Rust's complexity was added to help support the borrow checker.
Ironically, we can then add back in a different, easier form of borrow checking to get the speed benefits.
It definitely is. You've probably just had more time to discover the complexity of C++ (and read about it, since it's actually specified).
Of course Rust's complexity is much less dangerous because if you forget some obscure rules you get a compile error instead of UB (in safe Rust at least).
Vale sounds very promising (I'm also closely following Koka). But one thing I've found is that Rust's ownership/borrowing model does more than just eliminate GC. It also seems to encourage good program structure that is less prone to logic bugs.
I don't have solid evidence for that - more of a feeling. But I wonder if switching to reference counting would lose that.
You've repeated this in several comments. I seem to have a completely different experience to you because I find it extremely easy to read and understand rust. For sure, some constructs are tricky, like heavily generic iterators or something, but that's not rust so much as the concept that's tricky.
I don't think that Rust has much redundant syntax.
I guess you could do things like replace &'a Type with Ref<'a, Type> and *Type with Ptr<Type>, and get rid of some sugar like "if let" and print!, but I'm not sure that would have much of an impact.
Correct, this is more or less like remarking that having to learn Kanji/Hanzi makes learning Japanese/Mandarin very difficult is a superficial complaint.
> but the combinatorial explosion of syntax that supports them is untenable.
I wouldn't go quite that far myself, but it's definitely one of the sharper edges of the language currently--particularly because some of the features don't work together yet. E.g., async and traits.
I use rust weekly and I find it to have the best DX. I have done work with Oracle Java 5-8, IBM XL C99, MSVC++11, CPython 2-3, C# .NET Core 3.1. Stable Rust 2021 is overall the most readable, least surprising, BUT only with the right tool which also makes it the most discoverable, with rust-analyzer. My only gripe is the lack of consensus on strongly typed error handling (anyhow+thiserror being the most sensible combination I found after moving away from bare Results, to failure, to just anyhow).
I find Rust code hard to read...to the point where I don't feel motivated to learn it anymore. Line noise is confusing and a distraction. Random syntactic "innovations" I find are just friction in picking up a language.
For example, in the first versions of Virgil I introduced new keywords for declaring fields: "field", "method" and then "local". There was a different syntax for switch statements, a slightly different syntax for array accesses. Then I looked at the code I was writing and realized that the different keywords didn't add anything, the array subscripting syntax was just a bother; in fact, all my "innovations" just took things away and made it harder to learn.
For better or for worse, the world is starting to converge on something that looks like an amalgam of Java, JavaScript, and Scala. At least IMHO; that's kind of what Virgil has started to look like, heh :)
I wholeheartedly agree that rust’s syntax is way noisier and uglier than I’d like, and it’s nice to see someone else raise the point seriously. People tend to act like syntax is an ancillary detail in a language, but actually it’s fundamental! It’s our direct interface into the language itself and if it’s painful to read and write the language won’t be pleasant to use, no matter how great it’s semantics may be.
Beyond the line noise problem. I feel some of rust’s syntactic choices are confusing. For instance:
let x = 2
Introduces a new name and binds it to the value 2 while
if let Some(x) = y
Is a shorthand for pattern matching. Meanwhile other matching structures have no need of “let” at all. Likewise this extends the semantics of what “if” means and also overloads “=“ (e.g, glancing at this, would you say equals is binding a value to a pattern, performing a Boolean check, or both?) Rust has a couple of one-off weird syntactical devices that have been introduced as shorthand that imo quickly increase the cognitive load required to read code because several structures and keywords are reused in slightly different ways to mean entirely different things.
There are a lot of similar syntactic hoops around type signatures because they didn’t go with the old “type variables must be lowercase” rule which leads to subtle potential ambiguities in parsing T as a variable or proper type in some cases that thus forces additional syntax on the user.
I also think there are too many ways to express equivalent things in Rust, which again leads to more cognitive overhead. Reading the current docs, I get the sense the language is becoming “write biased”. Whenever they introduce some syntactic shortcut the justification is to save typing and eliminate small amounts of repetition, which is great in theory but now we have N ways of writing and reading the same thing which quickly makes code hard to grok efficiently imo.
This minor gripe comes with the big caveat that it remains probably the most interesting language to become vogue since Haskell.
> I feel some of rust’s syntactic choices are confusing. For instance:
> let x = 2
> Introduces a new name and binds it to the value 2 while
> if let Some(x) = y
> Is a shorthand for pattern matching.
It won't be as confusing once you realize that both do the same thing: variable binding. The difference is that the former is an irrefutable binding, whereas the latter is a refutable binding.
It might help you to think of 'if let' as an extension of 'let' rather than an extension of 'if'. That is, 'let' by itself supports irrefutable patterns. e.g.,
let std::ops::Range { start, end } = 5..10;
So the 'if' is "just" allowing you to also write refutable patterns.
That is a useful way to think about it for sure, I’m mostly using it as an illustration of what is probably a philosophical difference between myself and the Rust maintainers; in other words, I don’t see why we need if let when we already have match with _ wildcards. It’s the sort of syntactic shortcut that gives authors of code relatively little benefit (save a few keystrokes) and readers of code yet one more syntactic variation to contend with.
I guess another way of putting it is that I think Rust has a lot of sugar that’s confusing.
Kotlin is an example of a language that has a lot of similar syntactic shortcuts and functional underpinnings that implements them in a more readable and consistent fashion imo.
I could live without 'if let'. I'm not a huge fan of it either, although I do use it.
Its most compelling benefit to me is not that it saves a few keystrokes, but that it avoids an extra indentation level. Compare (taking from a real example[1]):
if let Some(quits) = args.value_of_lossy("quit") {
for ch in quits.chars() {
if !ch.is_ascii() {
anyhow::bail!("quit bytes must be ASCII");
}
// FIXME(MSRV): use the 'TryFrom<char> for u8' impl once we are
// at Rust 1.59+.
c = c.quit(u8::try_from(u32::from(ch)).unwrap(), true);
}
}
with:
match args.value_of_lossy("quit") {
None => {}
Some(quits) => {
for ch in quits.chars() {
if !ch.is_ascii() {
anyhow::bail!("quit bytes must be ASCII");
}
// FIXME(MSRV): use the 'TryFrom<char> for u8' impl once we are
// at Rust 1.59+.
c = c.quit(u8::try_from(u32::from(ch)).unwrap(), true);
}
}
}
The 'for' loop is indented one extra level in the latter case. With that said, I do also use 'if let' because it saves some keystrokes. Taking from another real example[2], compare:
if let Some(name) = get_name(group_index) {
write!(buf, "/{}", name).unwrap();
}
(I could use '_ => {}' instead of 'None' to save a few more.)
I do find the 'if let' variant to be a bit easier to read. It's optimizing for a particular and somewhat common case, so it does of course overlap with 'match'. But I don't find this particular overlap to be too bad. It's usually pretty clear when to use one vs the other.
But like I said, I could live without 'if let'. It is not a major quality of life enhancement to me. Neither will its impending extensions. i.e., 'if let pattern = foo && some_booolean_condition {'.
I don't code in Rust--and thereby might be expected to not know all of these patterns and want to have to learn fewer bits--and yet I agree with you. I feel like removing this "if let" variant would be similar to saying we don't need if statements as they are equivalent to a loop that ends in break. I actually even will say the if let is much easier to read as with the match I have to check why it is a match and verify it has the None case--similar to checking if a loop is really going to loop or if it always ends in break--whereas I can skip all that work if I see the "if".
I'm overall a rust fan but I've always agreed with you about `if let`. What I don't like is that it reads right-to-left and starts getting awkward if either side is much longer than just a variable name.
if let Some(Range { start, end }) = self.calc_range(whatever, true) {
// ...
}
I feel it would read much smoother if you switched the two sides so execution flows left-to-right
if self.calc_range(whatever, true) is Some(Range { start, end }) {
// ...
}
Agree, this is something that I wish was changed and it's something that C# got right.
I think I tried to look up why this syntax was chosen and found some old github issues when people were actually suggesting the latter syntax (pattern on the right) and I think there were some syntax ambiguities in this syntax. Not sure if this was the main reason. Maybe the lang team just didn't thought the difference is important enough (and it is for me! ;-)).
C# lang designers think about IDE experience when designing language syntax (that's why we have "from", "where", "select" order in LINQ, for better IDE code completion), hope other language designers were more thoughtful about it too.
> Introduces a new name and binds it to the value 2 while
> if let Some(x) = y
> Is a shorthand for pattern matching.
Both introduce a new name (x) and both pattern match, it's just that the pattern in let x = 2 is simply match anything and assign it the name x, you could just as well write
let t@(x, y) = (2, 4);
Which binds t to (2, 4), x to 2 and y to 4 and there it's perhaps more clear that normal let is pattern matching as much as if let is pattern matching.
You can write libraries against alloc on stable, but not any executables, because executables not using std need to specify the alloc_error_handler, which you can't do on stable yet: https://github.com/rust-lang/rust/issues/51540
Yep, this is a great article, but that section (the whole "Rust Isn’t Finished" section) jumped out as a place where there were some simple ways he could have made his life easier. It could also have been a failure of the Rust community to teach a good workflow.
You don't need to force every contributor to upgrade every six weeks in lockstep, since releases of Rust and std are backwards compatible. Upgrade at your leisure, and run tests in CI with the minimum version you want to support. If you're doing something crazier that requires ABI compatibility between separate builds (or you just want consistency), you can add a `rust-toolchain` file that upgrades the compiler on dev machines automatically, as seamlessly as Cargo downloads new dependency versions.
To clarify a bit, the key thing here is that the OP is maintaining their own patches to Rust's standard library. While the API of std is itself backwards compatible, its implementation uses a whole mess of unstable nightly features. That means that std for Rust 1.x can't necessarily be built with Rust 1.(x-1). EDIT: Nor can std for Rust 1.(x-1) be necessarily built by Rust 1.x.
It's true that you don't have to force every contributor to upgrade every six weeks, but you do very likely need to have every contributor use the same version of Rust. (Which can be accomplished with a rust-toolchain file, as you mention.)
The problem here is that if you don't do this upgrade whenever a new Rust release is made, you're just putting off that work to some other point. Maybe you do it every 12 weeks instead of 6 weeks, that would probably be okay. But I'd imagine waiting a year, for example, could be unpleasant.
When you tell someone to install Rust, they go to rustup.rs and install the latest version. Therefore, we need to have a libstd port for the latest version. Which effectively means we need to release libstd as soon as possible after the compiler is released. Our `sys` directory is at https://github.com/betrusted-io/rust/tree/1.61.0-xous/librar... and isn't too complicated. It's about 50 patches that need to be carried forward every six weeks.
Fortunately libstd doesn't change too much, at leaset not the parts we need. And I can usually pre-port the patches by applying them to `beta`, which means the patches against the release version usually apply cleanly.
It's still better than requiring nightly, which has absolutely no stability guarantees. By targeting stable, we don't run into issues of bitrot where we accidentally rely on features that have been removed. Rather than adjusting every service in the operating system, we just need to port one library: libstd
I've considered trying to upstream these, but I'm not sure how the rust team would feel about it.
This is one thing I struggle with when learning Rust.
I want to have some examples of purely idiomatic Rust code solving some bog-standard problems, that way I can copy what that project's doing while I get comfortable enough with the language and learn to make my own decisions.
It doesn't seem to me that this feature is what the blog post is referring to:
> I often ask myself “when is the point we’ll get off the Rust release train”, and the answer I think is when they finally make “alloc” no longer a nightly API. At the moment, `no-std` targets have no access to the heap, unless they hop on the “nightly” train, in which case you’re back into the Python-esque nightmare of your code routinely breaking with language releases.
You can absolutely do your own allocator with no-std. All you need for this is the alloc crate and the global_alloc feature, while global_alloc was stabilized before the alloc crate. Then you can call your own custom OS routines from that global allocator. No need to fork std over that.
Now, maybe their custom use case needs something different, and then it's a fair criticism, but for that I would have expected a different wording of the issue, hopefully together with a more detailed explanation of those use cases and how the stable part of the existing alloc crate does not meet them.
If, like OP, you're writing an operating system (or a language VM) it is absolutely a thing that you will want to have different allocators for different use cases, so being able to set a global allocator is "not quite enough". You will want certain generics (like hashes) to be able to take advantage of different allocators, or even different instances of allocators (say, give each thread it's own arena). This is very not easy in rust, which effectively requires data structures to be associated with specific allocators at the type-level - which makes code sharing between the "same" data structure tied to different allocators quite difficult.
For reference, the Erlang VM, which is often joked as being "an operating system unto itself" has 11? IIRC allocators.
The rust compiler makes use of custom arenas for allocation, quite heavily in fact. And does it without using the nightly-only custom allocator alloc types. Instead, there are functions that let you build structures inside the arena, plus a bunch of macro logic that builds on it. And while rustc generally uses a lot of nightly features, there is nothing fundamental about it that requires nightly.
Also, again, it's a fair concern that you want to be doing custom allocators, but this is not the same as claiming that no-std applications can't use the heap at all, which is what the blog post did. For simple heap usage, a global allocator is enough.
I'm not sure I understand what you mean here. If anything a sponsor should be happy if there is an end goal. Far to many projects feel the need to constantly futz with their product long past when its "done". I think the most people understand this about windows. No one is asking for yet another UI change, or intrusive snooping. Windows could have been done the day they released 64-bit windows XP (or windows7 depending on your politics), and we would have a far better OS, if they had simply laid off 90% of the team leaving the remaining people in a bug fix only maintenance mode.
> I wrote a small tool called `crate-scraper` which downloads the source package for every source specified in our Cargo.toml file, and stores them locally so we can have a snapshot of the code used to build a Xous release.
> This cargo subcommand will vendor all crates.io and git dependencies for a project into the specified directory at <path>. After this command completes the vendor directory specified by <path> will contain all remote sources from dependencies specified.
Maybe he doesn't want to depend on Cargo. Fair enough, it's a big program.
The big thing I wanted was the summary of all the build.rs files concatenated together so I wasn't spending lots of time grepping and searching for them (and possibly missing one).
The script isn't that complicated... it actually uses an existing tool, cargo-download, to obtain the crates, and then a simple Python script searches for all the build.rs files and concatenation them into a builds.rs mega file.
The other reason to give the tool its own repo is crate-scraper actually commits the crates back into git so we have a publicly accessible log of all the crates used in a given release by the actual build machine (in case the attack involved swapping out a crate version, but only for certain build environments, as a highly targeted supply chain attack is less likely to be noticed right away).
It's more about leaving a public trail of breadcrumbs we can use to do forensics to try and pinpoint an attack in retrospect, and making it very public so that any attacker who cares about discretion or deniability has deal with this in their counter-threat model.
I often wonder about what priorities lead to the kind of focus on the build system as a supply chain attack vector. It seems unusual that you are in a position where you have a chunk of code you want to build and have to trust the system that builds it but not the code, especially in a situation where such concerns can't be adequately addressed through sandboxing the build system. Personally if I was concerned about the supply chain I wouldn't worry about 5.6k lines of rust code running during the build and more the >200k (extremely conservative estimate) lines running on the actual system. (not that you can ignore the build system since of course it can inject code into the build, just that it's such a small part of the workload of reviewing the dependencies it shouldn't really be worth mentioning).
I guess the major thing is opening up the code to review it in an editor of choice and then having an LSP server running the build scripts automatically without you realizing it.
Reviewing code that you don't trust seems to be a pretty logical thing, and most people probably wouldn't expect that opening the code up in their favorite editor could cause their system to be harmed!
About the installation method ('hi! download this random shell script and execute it'), I agree this is really dangerous but mere installing stuff is a hairy thing on linux distros. I mean what is the practical alternative? Distro package manager versions are almost always way behind.
NixOS/guix are gonna solve this issue once and for all (famous last words)
But it's not really dangerous, no more so than downloading an arbitrary binary and executing it at least. The script is delivered over https, so you're not going to be MITM'ed, and you're trusting rustup to provide you the valid install script. If you _are_ MITM'ed, it doesn't really matter what your delivery method is unless you do a verification from another device/network, and if you don't trust rustup then why are you downloading and executing their installer?
If they `shellcheck` their bash script, then sure. Aside from unquoted $vars, usually random shell scripts have a habit of polluting home and creating arbitrary dirs under god-knows-where and not respecting XDG.
They are almost always irreversible too. Like you can't undo the steps the shell scripts have done.
Any software you choose to run could not respect your desires and leave a mess. This is not a random shell script. It's the officially recommended way to install Rust [0], vetted and maintained by the community. You're free to audit the script before running it, or even check out the source [1]. If this doesn't satisfy you, check out the other installation methods [2].
Edit: I realize you're not speaking specifically about rustup, but what I said can and should apply to anything you choose to install this way.
Problems like removing a large directory instead of a file, creating your files on random places instead of the directory you pass on, or creating more files than you intended?
The one mess you see from other languages is creating files on the wrong place (or all over the place). But not those above.
Any tool can be dangerous in inexperienced or careless hands. The issues you described could just as likely be caused by logic errors or typos in any other language.
You’re talking as if all bash scripts are hacked together carelessly and work by accident. You can actually learn bash. Thankfully the script we’re discussing is written with care and vetted by the community.
Problems like removing a large directory instead of a file
The rm command doesn’t even remove directories by default, you have to specify a flag. Not knowing a tool is not a good reason to bash it.
Isn't Rust one of those languages based on the idea that tools matter and that should either be correct or obviously wrong?
(And no, those problems do usually not appear due to logic errors or typos in other languages. It's very, very rare.)
I'm well aware that the Rust installation script is well vetted and stable enough to be reliable. Bootstraping a development environment is also a real problem, with no good answers. It's understandable that they want to bootstrap from Bash. But as understandable as it is, it still carries the Bash issues with it.
Of course, the optimum solution would be to do it from your system's tools. That is something that will probably happen naturally given enough time.
> Isn't Rust one of those languages based on the idea that tools matter and that should either be correct or obviously wrong?
It doesn't really matter, if you combine `/home/myuser` and some unsantized input variable, and then call `remove_dir_all` [0], it doesn't matter how safe the language is, you're going to delete your entire home directory with absolutely no warning, whether it's in bash, go, python, rust or haskell. Yes bash makes this very easy to do, but so does pretty much every language in existence.
> (And no, those problems do usually not appear due to logic errors or typos in other languages. It's very, very rare.)
They absolutely do. Here's an explosive script in golang (deliberately doesn't compile just in case) - running this in func main() will ruin your day most likely.
dirToRemove := "~/" + os.Getenv("BAD_ENV_VAR")
os.RemoveAll(dirToRemove
I can write one of these in bash, python, go, you name it.
> Problems like removing a large directory instead of a file
rm doens't do that unless you explicitly tell it to.
> Problems like removing a large directory instead of a file, creating your files on random places instead of the directory you pass on, or creating more files than you intended?
But yes, all of these can and do exist in other languages. Using python as an example, if you read an environment variable without checking it's set (as in the infamous steam bug) [0], you'll end up with pretty much the exact same behaviour. You can misindent your loop in python and not create/remove files that you intend to, or your script can have a syntax error halfway through and the interpreter will happily proceed until it halts, and leave you in a half baked state just like bash does.
- the domain in the curlbashware URL could be less shady than sh.rustup.rs
- the "rustup is an official Rust project" claim on https://rustup.rs/ could be a link to a page somewhere on rust-lang.org that confirms that rustup.rs is the site to use
- the domain in the curlbashware URL could be less shady than sh.rustup.rs
The domain is only as shady as it is unfamiliar. It's not shady to me since I recognize it as the canonical domain of the recommended installer for Rust, "rustup".
- the "rustup is an official Rust project" claim on https://rustup.rs/ could be a link to a page somewhere on rust-lang.org that confirms that rustup.rs is the site to use
It links to rust-lang.org, whose installation page then describes rustup as the recommended way to install [0]. I suppose it could link directly to the page, but what really does that gain?
It's shady because it's under the TLD for Serbia, while having no obvious connection to Serbia. I have nothing against Serbia, but the Rust project doesn't seem to have any special relationship to that country.
In HN and similar places, it is pretty normal to see a cc-tld used purely because the abbreviation fits. Not everyone is used to that, though. If it were e.g. https://rustup.dev/, that would mitigate this concern.
Also, a bad actor could just as well register https://rustup.dev. Rather than judging a URL in a vacuum based on the TLD, you should instead cross reference the official docs and confirm that the URL is correct.
Is it not? If GitHub were asking me to download and run code from a github.io subdomain without checking a signature, or something of similar risk level, I'd be concerned. I'd also be correct to be concerned, since anyone can put anything in a github.io subdomain -- I'd need to make sure that github actually owns that repo. Strictly speaking that's orthogonal, and github does actually own the github.io domain. The domain still seems suboptimal to me, but I don't make those decisions.
And yes, a bad actor could just as easily register rustup.dev. Nobody ever claimed that checking the TLD is sufficient to make a site trustworthy; only that it appears a bit shady. Unless you're already familiar with Rust (or at least with a particular aspect of startup culture), there's no obvious reason to choose .rs. On the other hand, domains in somepopularsite.unrelatedtld have been a phishing staple for decades -- making the shady vibe at least a little bit reasonable.
I meant that the logic implies that https://github.io is shady because it uses the ccTLD of British Indian Ocean Territory despite being unrelated.
Of course you should cross reference the authenticity of any URL you are about to execute as a shell script. No one is saying not to.
But your point seems to agree with mine: it’s only as shady as it is unfamiliar. The answer shouldn’t be to come up with a URL that lowers your guard. Instead, users should get familiar.
"the domain in the curlbashware URL could be less shady than sh.rustup.rs"
Relying on a familiar looking domain doesn't get you much security, especially with internationalized domain names where what a domain name appears like in one language could actually be very different in another.
People repeat this a lot but really it just seems dangerous. Can you give an example of a scenario where offering a download via `curl | bash` is more dangerous than "download this installer with the hash 01234 and then execute it"?
The site could detect that it's invoked as part of a `curl | bash` and sometimes serve a different script than you would get if you manually downloaded the script or the installer for manual inspection/auditing, making it harder to detect shenanigans. I think someone wrote this up as a PoC/blog post at some point.
A binary that you download could be obfuscated to make it hard to audit. A site could offer different binaries to different people, locations, times of day, user agents, etc. That's not really a realistic risk.
You could mistype the url by a letter or two and hit enter before realizing the mistake. Creating malicious domains that are very close to popular ones is somewhat of a common thing for exactly this reason.
If you downloaded and peeked into the script before running it, this would be a lot less likely to happen.
There is really no downsides to downloading it, checking it out and then running it other than it not being able to be blindly copy pasted into a terminal.
Me too. A lot of people who try Rust encounter a very steep learning curve, and tend to question whether the borrow checker and strict typing is even worth it. For me, it's allowed me to build larger threaded and distributed systems than I've ever been able to before. I've tried to build such systems in C/C++ but I've never been able to make something that isn't incredibly brittle, and I've been writing in those languages for 25 years. For a long time I thought maybe I'm just a bad programmer.
Rust changed all that. I'm kind of a bad programmer I guess, because Rust caught a lot of bad decisions I was making architecturally, and forced me to rewrite things to conform to the borrow checker.
This is the point at which I've found many people give up Rust. They say to themselves "This is awful, I've written my program one way I'm used to, and now it looks like I have to completely rewrite it to make this stupid borrow checker happy. If I had written in C++ I'd be done by now!" But will you really be done? Because I had the same attitude and every time I went back to C++ I surely built something, but if it got too large it would be a sandcastle that would fall over at the slightest breeze. With Rust I feel like I'm making skyscrapers that could withstand an earthquake, and I actually am because the programs I've written have weathered some storms that would have washed my C++ code out to sea.
Of course one can make stable, secure, performant systems in C++ and many other languages. But apparently I can't, and I need something like Rust to empower me. Someone else here said that Rust attracts people who want to feel powerful and smart by writing complicated code, but I like to write Rust code just to not feel inept!
I remember someone saying that "Rust skipped leg day", feeling that Rust was overly focused on the borrow checker while only solving a small number of problems.
1. I think its easy, especially for GC users, to forget that memory management is really about resource management.
2. The composability of features with the borrow checker is outstanding, like proper session types / locks or Send+Sync for safe use data with threads.
Well, that's the difference between the "I like [X], but have a few complaints that I want to get off my chest" kind of rant and the "I hate [X], and want to convince everyone how bad it is and to never ever use it again" kind of rant...
‘Before [const generic], Rust had no native ability to deal with arrays bigger than 32 elements’.
Is this a correct statement? I have seen posts talking about const generics being a new thing as of 2022. Did Rust actually lack the ability to have an array with more than 32 elements? I find it hard to believe that there was no way to have an array of longer length and Rust still being a production level language.
You have always been allowed to have arrays longer than 32 elements, but dealing with them used to be hard. Beyond the Copy trait, which is a compiler builtin, many traits weren't implemented for arrays with more than 32 elements.
And the feature is still limited. For example, legacy users like serde still can't switch to the new const generics based approach, because of the same issue that the Default trait is facing. Both traits could be using const generics, if they were allowed to break their API, but neither want to, so they are waiting for improvements that allow them to switch without doing a hard API break.
Since nobody else mentioned it, it's worth pointing out that what e.g. JS calls an array is Vec in Rust and can be as long as you want, with no ergonomic difference regardless of the length.
Array in Rust specifically refers to an array whose length is known at compile time, i.e. a bunch of values concatenated on the stack, and that's what the limitations applied to.
The quoted statement pissed me off a bit (I otherwise enjoyed the article) because it seems intended to mislead. The author should have known the colloquial meaning of "array", and "no ability to deal with" is factually incorrect.
If you set the array size to 32, then it works. You can get around this by using a macro, instead of `Default`, or implementing Default yourself, but it's still a limitation where you can't use an array of more than 32 elements.
Yes, I know, but the trait limitation only applies to arrays, not to Vec. Many people coming from other languages would reach for Vec first when they want an "array". I believe that misunderstanding the meaning of "array" is why GP was surprised that Rust couldn't (ergonomically) handle more than 32 elements in an "array".
For the first year we did not have Vec because we were no-std + stable so we literally had to use arrays and could not reach out for heap allocated Vecs.
Things got much better after we got std and could use Vec, as you note, but there are still a few locations where we have no choice but to use arrays (ie some crypto APIs that are too risky to redesign, the boot loader, and micro kernel itself which is still no-std come to mind immediately).
> we did not have Vec because we were no-std + stable so we literally had to use arrays
It's true that Vec isn't available in a no-std context, but I don't think it follows that arrays are the only other option - see heapless for one example: https://github.com/japaric/heapless
I also agree with some of the ancestors: the post seems to say that the Rust language couldn't handle arrays with more than 32 elements, and (as someone who's written a fair bit of no-std Rust, before const generic) that doesn't seem right. At first, it did seem awkward to me as well that some macros weren't defined for >32 element arrays, but in practice I haven't found it to be a significant limitation.
Was there a particular scenario where it wasn't feasible to wrap a >32 element array in your own type and implement Default on it?
We tried heapless, but ran into some problems with it. I forget the issue, but it had to do something with a bunch of unsafe code in heapless and the way we did stack frame alignments causing subtle bugs with tuples that had u8's in them. That problem may be resolved now that we're a couple years on.
If I'm not mistaken you can't implement traits on types that aren't in your crates, so, there's that limitation. But generally it's just another layer of friction that feels like it shouldn't be there, and it manifests itself as a form of technical debt. For example inside the microkernel itself there is an array that tracks the connection IDs in and out of a process. It's limited to 32 elements. Back when it was created, that seemed like a lot. Now maybe it'd be nice to bump it up just a little bit...but it would require opening up a can of worms since it never had all the traits implemented around it, so it's just sitting there until it becomes really worth the effort to upgrade that limit. There's a few spots like this.
Ah interesting. I've never dug in to the heapless implementation, but can imagine that getting it working right on a new platform might require a few changes.
It sounds the "orphan rules" that you're referring to; my understanding is that `impl SomeTrait for SomeStruct` needs to be in the same module as either `SomeTrait` or `SomeStruct`.
I bumped in to a similar situation with a project that involved a special buffer for handling digital audio. Initially, I made that buffer generic, and put it in its own module. The thing that used that buffer was in another module, and then everything was brought together in the main application. I wanted the ability to adjust parameters of the buffer from the project's top-level configuration, and can't remember the exact details, but basically with that structure main couldn't be the place that configured both the buffer and the module that dealt with the digital audio peripheral. The solution was pretty simple though: realise that the data structure is only ever used in conjunction with the peripheral, so instead of main including the buffer and peripheral (a dependency graph with edges main-buffer and main-peripheral), put the buffer in the module with the peripheral (edges main-(peripheral&buffer), or main-peripheral and peripheral-buffer, I can't remember which).
If you revisit that problem, it might be worth considering a declaration of a type for the connection IDs and with whatever impls are needed, in the module that wanted the >32 element array.
Perhaps. But in writing an OS, sometimes you genuinely do want the guarantees of an array. You especially would want to avoid the overhead that might come when the Vec gets resized.
Yes, and if you don't need dynamic size you can use an array (of any size). The lack of trait implementations is generally a minor inconvenience in the scale of the various inconveniences of writing an OS. It doesn't stop you doing anything.
You could have bigger arrays, what was missing were the trait implementations. Originally the traits were implemented using a macro, and the macro only generated implementations for up to length 32.
Before const generics most traits were only implemented up to 32 elements though, which could be quite annoying. Even more so as the compilation error was not exactly informative.
There were some awful hacks to make integer parameters to generics sort of work before "const generic" went in. There were tables of named values for 0..32, then useful numbers such as 64, 128, 256, etc. Those haven't all been cleaned out yet.
I experimented with replacing an Express server with Rust while keeping the same js syntax and still running on Node
Granted this adds overhead, but my conclusion was that the performance gain is not worth the effort. Sure, memory looks almost flat but response times aren't that much better
My experience is that choosing Rust just for performance gains usually doesn't pay off. In your case, node already uses C/C++ under the hood, so some of what you're replacing could just be switching that for Rust.
The primary reason I reach for it is when I want the stability provided by the type system and runtime, and to prevent a litany of problems that impact other languages. If those problems aren't something I'm looking to solve, I'll usually reach for a different language.
> choosing Rust just for performance gains usually doesn't pay off
Performance is a complex topic. Other languages can be fast and you’re likely right that with simple initial benchmarks, Rust isn’t going to out-perform other languages by enough to make much of a difference.
But what about consistency of performance? Is your 1,752,974,468th response going to be as fast as the ones in your benchmark? To me, that’s been the eye opener of deploying Rust in production. We saw P100 response times within 10ms of P0. The absolute worst case was below the threshold for human observability from the absolute best case over many months of heavy use. The metrics graphs were literal flat lines for months on end across tens of billions of requests. I have never seen that in any garbage-collected language.
That kind of performance may not be necessary for your needs and you may be able to tolerate or otherwise live with occasional slowdowns. But there are plenty of cases where consistent performance is necessary or extremely desirable. And in those cases, it’s nice to have Rust as an option.
Very cool, and it’s true Rust is dense. On the other hand C, the other typical option, either requires massive amounts of ancillary tooling and macros like Zephyr to get even close to doing as much at compile time. Those tools and macros add density and complexity. Arguably C++ is about equivalent with fewer pros over C and plenty of the cons of C still there along with some new ones.
I appreciate the idea of trying to create a auditable OS for an MMU capable CPU, the reality is once you have feature you care about that becomes harder and harder it seems.
Once rust stabilizes, I think it needs an ISO standard like C and C++ have. I can't see automobile manufactures using rust without one. One reason C and C++ are still widely used is due to this. When we are writing code that is expected to run for decades, having a corporate/community backed language is not sufficient. We need global standards and versions that we can rely on decades latter.
What has the standard actually gotten C and C++? Basic features needed in every single code base like type punning on structures are standardly UB, while design-by-committee results in C++ feature hell.
It doesn't get any harder to write a function exhibiting a bug just because there's a standard saying the function shouldn't have bugs in it. No matter what, you are trusting a compiler vendor that the code it compiles and the functions it links against don't have bugs.
A standard is not a magic spell that creates better software through its incantation; it provides for multiple separate compiler vendors to be able to compile the same code the same way, which is a total fiction in C/C++, and not required for languages like Python or Lua. I view it as nothing more than the streetlight effect.
Prior to the C/C++ standardization process, every compiler implemented a different dialect of those languages, and there wasn’t a complete and accurate specification for them. Some very basic C code working with one compiler might not work on another.
I don’t think Rust or any other modern language needs to be standards-org standardized, but this is a different era; there is a single solid, well-documented, versioned reference implementation for Rust. That was never the case for C or C++.
Yeah I mean this is still kind of the case today, Rust just avoids it because there is really only one reference implementation. That may not even be true forever, Rust on GCC is continuing to get more and more feature complete over time. [1][2]
Take the "defer" keyword in GNU C - it's valid in anything that has the GNU extensions but isn't standard C at all. And yet, some projects swear by it (it's not a bad feature, just nonstandard).
There's a lot of weirdness in C implementations even looking across LLVM, GNU, or even when picking which libc you want! Try porting any nontrivial project to work with musl-libc. You might find that it's not as easy as swapping in a target and building statically!
This is perhaps the whole rub with standardization - it's bought us as developers a lot, but it doesn't cover everything. The veil was kind of lifted for me when I started trying to use different Scheme implementations in a "standardized" way. I eventually gave up on that and just use whatever implementation I am most happy with (often CHICKEN, but that's a digression).
This gets more complicated with C++, which modern standards mostly requires C11, but then also doesn't support everything that C11 requires either. They're different languages but yeah, compilers are gonna disagree on some of the edges there.
[2] tangentially, Rust also avoids some UB discussion because the type system is a bit more complete in terms of its properties than C is, so they can resort to Option or Result when things get dicey in an API. Further, there's no official Rust ABI unlike C, so you don't have to worry about that either...
> Some very basic C code working with one compiler might not work on another.
I teach C and C++, and you have no idea how often I hear "But it worked on my machine!" when I give back bad grades due to code that segfaults when I go to run it.
• FILE* was a big I/O abstraction that C did not have before. With Unixes and MS-DOS there were file handles, but many other platforms had nothing like that.
• That there was a clear idea of what kind of operations were well-defined was a pretty big deal. Remember, all there was before was K&R to go off as a reference, or maybe you had access to the Portable C Compiler. It was also a time where you had a lot more oddball architectures.
• void return types and parameters. There was no idea of a procedure in early C, only functions with useless return values.
And of course more. There are definitely worse cases of ISO standards than C and C++. Both are noticably better out of it.
> No matter what, you are trusting a compiler vendor that the code it compiles and the functions it links against don't have bugs.
I guess the key factor about a standard is that as a corporation you can point fingers if something goes wrong ("the compiler and/or the MISRA C checking tools you sold me are not compliant with the standard because of this bug!").
Also the committee can point fingers back if required ("the UB is clearly specified in the standard!").
If I were a team manager at a big automotive factory in charge of the ECU system, I would go the private way, with guarantees, and paying a lot of money. In case of failures, I can point fingers and someone would answer the phone on the other side if I complain.
Who should I call or who should I point my finger at, if something goes wrong because of a bug in Rust? A Github user on the other side of the planet?
If there were a standard, you'd still be pointing at opposite-hemisphere github users. This is what I mean about the streetlight effect - the standard has jack to do with the outcome. If you are buying a product from a vendor, the vendor is responsible for the product, and if you are using an open-source community-managed product, it's much harder to point fingers. The source of truth can be an ISO standard, or it can be documentation, it doesn't matter.
A part of the safety story of any useful toolchain compliant to ISO 26262 as a SEOOC is that it verifiably implements the language as documented in the standard. The "verifiably" part is important. If there is no standard to verify against, how do you know it's doing the right thing?
The language standards themselves state outright that programs containing undefined behaviour are malformed. If you write malformed programs, you can not assume that they are safe. Don't blame language standardization for your writing bad, unsafe software if you're not going to follow it.
In addition verifiably conformant compilers for translating your programs into software, the standard allows other tools to be written to perform things like static analysis, complexity analysis, and conformance to other published guidelines (eg. MISRA). These things are not possible where the definition of the language is "whatever one single toolchain vendor decides it will be this week".
The US government has a very long history projecting it's will on other countries. Under the guise of national security, what is stopping the US government from changing Rust to prevent it from working in Russia, Iran, or Canada? The scenario is somewhat hyperbolic, but the US and European centric nature of Rust gives people in less developed nations pause.
How would one change a programming language to not work in a country?
Even assuming that is possible, the answer is the same as any open source project: you’d have to convince the teams to make that decision. Nothing special there.
A senator^Wcongressman asked some questions about Rust and its nightly toolchain whenever Facebook’s cryptocurrency was under scrutiny by regulators. A French government agency has a whole set of coding guidelines for Rust. The government of Quatar was using Rust before 1.0; haven’t heard much since, but I assume they’re still using it. A New Zealand firefighter company was using some Rust.
Programming languages are tools. Governments use tools. It shouldn’t be surprising that they may have an interest.
That said I find your parent comment also a bit silly for the other reasons you state.
They care deeply about software security and memory flaws (everyone should). If rust had an ISO standard, then it could be used in more sensitive military and aerospace systems.
Something being an ISO standard has nothing to do with being able to send OFAC after you? Fundamentally the difference is providing a service vs an idea just existing in the ether. You can't sanction Rust, it's just an idea. You could tell rustup they can't allow downloads from IPs that match sanctioned countries.
If the US gov decides to project its will on your software project, an ISO standard is not going to help you at all. They will sabotage the ISO process, or force your hosting provider (GitHub) to remove your project or apply changes to it, or just kidnap your maintainers and beat them with wrenches until they comply[0].
If your threat model legitimately considers the US gov to be a hostile actor, you need far more than a piece of paper that claims what the behavior of your compiler is.
really interesting read, and nice to see people writing operating systems on rust and have also plus points besides grievances. particularly enjoyed you found rust sometimes spares you the 'damn i need to rewrite this entire thing' tour that C always hits me with :D. now i am more hopeful my re-write-the-entire-thing-in-rust was an ok'ish choice.
Took me a full year of questioning life choices before it felt worth it, but fearless refactoring is so nice. I may have trouble going back to C just for that.
IMO the author underplays the visual ugliness of some Rust code. Programmers tend to look at code for hours a day for years, and so it should not be visually taxing to read and parse. This is why syntax highlighting exists, after all.
But the gist I got from it is that Rust is really a very good static analyser.
" Yes, it is my fault for not being smart enough to parse the language’s syntax, but also, I do have other things to do with my life, like build hardware."
and
"Rust Is Powerful, but It Is Not Simple"
among all the other points, should be enough to disqualify it for mainstream use. The core of most arguments against C++ boil down to those two points too. If a large percentage of the engineers working in the language have a problem understanding it, they are going to have a hard time proving that their aren't any unexpected side effects. Of which both C++ and rust seems to be full of, given the recent bug reports in rust and projects people are using it in.
So, I'm still firmly in the camp that while there are better system programming languages than C, rust isn't one of them (hell even Pascal is probably better, at least it has length checked strings).
> among all the other points, should be enough to disqualify it for mainstream use. The core of most arguments against C++ boil down to those two points too.
Nope not at all, that’s not a valid comparison.
I argue that there is no simple solution that affords what rust does. Engineers have to use their heads to write correct and fast software. I’m so tired of people just accepting lack of memory safety because it’s “hard” to do correctly. There are real consequences to the amount of insecure trash that exists because of this mindset.
> The core of most arguments against C++ boil down to those two points too.
No, the core arguments against C++ boil down to it not providing enough value for these costs, and that its complexities are not orthogonal and interact sub-optimally with one another so the complexities compound superlinearly.
The basic problem with C++ is that it has hiding without memory safety.
C has neither hiding nor memory safety. Most newer languages have both. C++ stands alone as a widely used language with high level unsafe abstractions. This is the source of most buffer overflow security advisories.
The usual euphemism is "abstraction". It's doing something inside, it's hard to see what that is, and it has constraints on its use which are relevant to memory safety but are not enforced by the language.
Which completely misses how people use C++ as a systems programming language. For the most part those users treat it like a better C, only reaching for C++ features when its an overwhelming advantage over C and generally banning significant parts of the language.
In that case we need to disqualify: Linux, threading, networking, anything graphical, anything involving a database, anything that has the ability to write memory that is read by other lines of code, and probably any computer that allows input and/or output just to be safe.
I guess my point isn't really clear. Its more a case, of your just swapping one set of problems for another. People shouldn't avoid hard problems, but they should be seeking to solve them with better tools, not ones that just translate the problem domain without providing a clear advantage.
In the grand scheme your looking for the optimal intersection of simple/expressive/performant/safe and rust seems to fail on the simple/expressive axis vs just simple C which people chose over languages like C++ which are more expressive because that expressiveness is a source of bugs. And on the safety side, rust fails miserably when compared with more fully managed environments. So, it becomes a question of whether that additional cost provides much vs just spending more putting guardrails around C/C++/etc with more formal methods/verifiers.
> And on the safety side, rust fails miserably when compared with more fully managed environments.
That's a rather extreme, unsubstantiated, and imo false, claim to just throw out there as a matter of fact.
And I'd also be curious how you can square putting enough formal methods/verifiers around C/C++ without creating a far worse entry into the simple/expressive axis than rust.
> a question of whether that additional cost provides much vs just spending more putting guardrails around C/C++/etc with more formal methods/verifiers.
So the conclusion (or the closest you get to proposing an alternative strategy) is to just to pour more tens of millions down the black hole called Cartographing The Wild West of Pointers. Hardly pragmatic.
C++ is one of the most used languages, and it does seem to me that Rust has enough momentum going for it to be a commonly used system programming language as well.
I do agree with his points, but I don't think it's enough to disqualify it for mainstream use.
> The core of most arguments against C++ boil down to those two points too. If a large percentage of the engineers working in the language have a problem understanding it, they are going to have a hard time proving that their aren't any unexpected side effects.
That's true for C++ but not for Rust, because Rust will tell you if there's some kind of unexpected behaviour that you didn't think about, whereas C++ will allow UB or whatever without telling you.
That's the big difference between (safe) Rust's complexity and C++'s complexity. They are both very complex, but in Rust it doesn't matter too much if you don't memorise the complexity (complicated lifetime rules, etc.) because it will just result in a compile error. Whereas in C++ you have to remember the rule of 3... no 5... etc. (that's a really simple example; don't think "I know the rule of 5; C++ is easy!").
Good article. I have some things to say, because that's what I do.
To start: I have to say that I find some of the comments here a little odd -- the competition for Rust is not Go or TypeScript or Kotlin or whatever. If you're using Rust in your full-stack webdev world to serve, like, database queries to webpages or whatever... I don't know why. Rust is clearly for things like: writing an OS, writing a browser, writing a low latency high throughput transaction server, writing a game. For the other things I'd say there's plenty of other options. It's been years since I worked in web applications, but I struggle to see the need for Rust there.
Rust is for the same niche that C++ and C sit in now. A similar niche that Zig is targeting. I don't think D with its <admittedly now optional> GC or Golang sit in this same space at all. Also, having spent a year working in Go I don't understand how anybody could complain about Rust encouraging boilerplate but propose Go with a straightface. Go (at least the Go I was working on at Google) was just a pile of boilerplate. Awful. The syntax of the language is... fine. Generics will fix most of my complaints with it. The culture around the language I found repulsive.
Anways, for years (prior to C++-11) I whined about the state of C++. Not just its lack of safety but the idiosyncracies of its syntax and its lack of modern language features I was familiar with from e.g. OCaml and from hanging out on Lambda The Ultimate. By modern features I mean pattern matching & option / result types, lambdas, type inference, and a generics/parameterized type system which wasn't ... insane. Remember, this is pre-C++11. It was awful. C++-11 and beyond addressed some concerns but not others. And I actually really love writing in C++ these days, but I'm still well aware that it is a dogs breakfast and full of foot guns and oddities. I've just learned to think like it.
Anyways, back to Rust...before C++11, when I saw Graydon Hoare had kickstarted a project at Mozilla to make a systems programming language (that is without a GC) that supported modern language features I was super stoked. I tended to follow what Graydon was doing because he's talented and he's a friend-of-friends. Rust as described sounded like exactly what I wanted. But the final delivery, with the complexities of the borrow checker... are maybe something that I hadn't gambled on. Every few months I give another wack at starting a project in Rust and every few months I tend to run up against the borrow checker with frustration. But I think I have it licked now, I think I will write some Rust code in my time off work.
So my personal take on Rust is this: on paper it's the fantasy language I always wanted, but in reality it has many of the complexity warts that other people have pointed to.
However it is better than all the alternatives (other than maybe Zig) in this space in many many ways. But most importantly it seems to have gained momentum especially in the last 2-3 years. It seems clear to me now that the language will have success. So I think systems developers will probably need to learn and "love" it just like they do C/C++ now. And I don't think that's a bad thing because I think a culture will build up that will get people up to speed and some of the syntactical oddities just won't look that odd anymore. And the world of software dev will hopefully be a bit safer, and build systems less crazy, and so on.
> It’s 2022 after all, and transistors are cheap: why don’t all our microcontrollers feature page-level memory protection like their desktop counterparts?
I always thought it was because of the added cost from increased design complexity. Is it something else?
Secretly, I suspect the answer is market differentiation. You can charge a higher royalty for a CPU core that has an MMU, and bundling it into the low end stuff erodes margins.
The complexity is real, but in a typical SoC the actual CPU core is maybe 10% of the area and tossing in an MMU impacts maybe 1-2% of the total chip area. I haven't seen the pricing sheets, but I suspect the much bigger cost is the higher royalty payment associated with instantiating the MMU.
Haha, no: It's because if you put an MMU in a microcontroller it's not a microcontroller anymore! That makes it an "application processor" of which there are many tens of thousands to choose from these days.
There's nothing stopping you from taking say, a Raspberry Pi 4 and using it as a bare metal device (no OS) just like an Arduino. Or taking that same chip (BCM2711) and putting it in your own board to do something similar. It just wouldn't be economical for that sort of purpose most of the time.
I loved this perspective from a hardware oriented engineer coming to appreciate the enormous complexity and difficulty in providing a stable and useful data structures and algorithms library as part of, say, rust std.
Each complaint is valid, but some of it is (they admit) coming from a bit of naivete. They're surprised red black trees are included in the Linux kernel? why? They were surprised at how useful rust std and a good? data structure foundation was? why?
You can do that! But you may run into a problem where one of the crates you use detects that you're using nightly, and then opts in to nightly-only features. Except it may be using features that are only available on the latest version of nightly, or an ancient version that you don't have, and there is no version that makes both your code and the dependent package happy.
It's much better to just use stable. Then you're guaranteed to have forward compatibility, and you only have one place where you need to deal with the nightly weirdness.
> “Hi, run this shell script from a random server on your machine.”
You shouldn't run scripts from a random server but you probably have to consider running scripts from a server you trust. If you don't trust the server you run the script from, are you really going to run the executables this script installs? If we ignore the idea of downloading and building every program from source, then you'll download and run programs compiled by someone else. And you need to trust them, or sandbox the programs. There are no alternatives.
Yes, the bash script or msi can kill your dog and eat your homework but there isn't much we can do about that without running things in in sandboxes - and the (old/normal) windows app model doesn't have that.
Auditing the script won't help you, because it'll say it will install a program somewhere. Which is what you want, so you'll consider the audit "ok". But the people who wrote the script/installer are the same people that created the program (or have compromised the computers producing both) and now you'll run the rustc.exe program you just installed and that will eat your homework!
To most people there is no difference in how transparent a bash script is compared to an msi. Downloading an msi from a https server I trust, signed with a cert I trust, is something I'm mostly comfortable with. The same applies to running a bash script from a location that is trustworthy.
> Auditing the script won't help you, because it'll say it will install a program somewhere. Which is what you want, so you'll consider the audit "ok" [but that program is made by the same people as the installation script].
Your argument doesn't take into consideration that build artifacts / software releases have culture and best practices behind them. Such releases are often considered, tested, cut, digested, signed and included in package managers delegating trust.
Many one-off installation shell scripts are not afforded that culture, especially when maintained from within (static) websites that update frequently. On the other hand, they are small enough for you to audit a bit. If you'd compare the script with one that someone else downloaded a month earlier (i.e. archive.org), that would help a lot to establish trust.
> If we ignore the idea of downloading and building every program from source
Your argument is equally valid when building every program from source. You will not be able to review the source code of moderately large programs. You will need to delegate your trust in that case as well.
To me, the problem has never been that you're running a shell script from some remote source, but that you're expected to pipe it directly into an interpreter so the existence of what you actually ran is ephemeral.
There are the various levels of trust that you need to account for, but as you and others bite, that isn't specifically different to most people than some installer.
What is different is that there's no record of what you ran if you pipe it to an interpreter. If, later, you want to compare the current script available against what you ran, there's no easy way.
Yes, that's the point. You can, and probably should do that, but the guides don't bother with what is essentially making one command two because two commands is one more for someone to screw up.
I think the trade off they are taking is fundamentally a bad one with respect to security and accountability. It's not even about checking the script ahead of time. It's about checking what was actually run at a later date to see what happened. If the script is never stored to disk, determining whether a short lived hack of the script source affected you is much harder.
No, they don't bother because most people won't understand the script anyway, and just rely on the fact that many people have installed Rust this way and nothing bad happened to them.
You don't need to understand the script to have it in the directory and run a sha1 or md5 against it, and compare it against what should have been returned when there's an announcement that there was a problem.
That is what I mean by accountability. When nothing is left on disk of what was specified to execute, there's severely limited recourse in figuring out what happened.
I'm not suggesting every person does:
curl > install.sh
less install.sh
sh install.sh
I'm suggesting they should be directed to do:
curl > install.sh
sh install.sh
and then later if there's a known problem, there are fairly easy ways for them (even a novice) to determine whether what they ran was legitimate or not. Piping a web request directly to a shell is a poor trade off WRT security to request of anyone, IMO. By that I mean that the gain in ease of use is extremely small, but the loss in accountability is fairly large in the case that there's a problem.
With the other languages the apps on my machine are built with (C, and to a large degree Python) I have the benefit of the distribution maintainers at least looking in the general direction of the source for things I install (including development libraries.) Tools like Cargo shortcut that and open me up to a lot of nastiness. It's very similar to the problem on Windows really and I wouldn't be surprised if you started seeing malware disturbed that way like we're currently seeing on NPM and Pypi.
rustup in particular is well-behaved. It installs itself in a single directory, without modifying the system, apart from PATH which it warns you about, lets you skip it, and when it does it, it does with care.
OTOH many distros don't take care to build and package Rust properly. For example, Rust patches its version of LLVM to avoid known bugs. The particular combination of Rust version + LLVM version is most tested. Distros that insist on unbundling build Rust with a stock LLVM that doesn't have these bugfixes, and often is an older version of LLVM that hasn't been thoroughly tested with Rust.
Then there's the mismatch of upgrade approach between the Rust project and most distros. Rust uses an "evergreen" (Chrome-like) approach to upgrades, in which the focus is on making upgrades seamless and not breaking anything so that they can be small and frequent. Most distros prefer infrequent big upgrades, so they package unusably old and officially unmaintained versions of Rust for no good reason.
This is threat modeling. Bunnie Huang's threat model for Precusor is considerably more stringent than the ordinary, to put it mildly.
Compare this to a C program where love it and hate, it's just a bunch of files that get included by concatenation. There's no magic to make your life easier or get you in trouble, everything is done via manual transmission.
The article goes into why they haven't been able to apply this approach to Rust, even though they would like to.
Auditing the script can certainly help, just not against malice. E.g. if the script is not set up in such a way that it protects against partial execution, then this represents a kind of vulnerability (truncation) that signed MSI/.deb/etc files simply do not, by the design of the file format.
Yes, it's possible (even easy) to write a curlbash script that doesn't have this issue (or the various other issues). Reviewing the script still buys you something.
> This is in part because all the Rust documentation is either written in eli5 style (good luck figuring out “feature”s from that example), or you’re greeted with a formal syntax definition (technically, everything you need to know to define a “feature” is in there, but nowhere is it summarized in plain English), and nothing in between.
I wish I wish that Rust had a better documentation system. It's rather telling that any serious project has to use an entirely separate static site generator because the official doc system is so crippled.
'cargo doc' is absolutely one of my most favorite things about Rust. I've never once seen it as crippled and I've never once reached for an "entirely separate static site generator" to write docs despite maintaining several serious projects.
Writing out explicit links sucked, but we have intradoc links now. It was a huge win. But my first paragraph above was true even before intradoc links too.
Also, I hate Sphinx. It's awesome that folks have been able to use it to produce great docs, but I've never been successful in using it. I disliked it enough that I wrote my own tool for generating API documentation in Python.[1]
I find rustdoc lacking for clap. rustdoc does a good job with API reference documentation and is improving in its handling of examples but derive reference and tutorial documentation are a weak point.
- Dummy modules to store your documentation (I've seen this used but can't remember one off the top of my head)
For clap, my documentation examples are best served as programs and we've had a problem with these being broken. The Rust CLI book has a decent strategy for this by pulling in code from external files (https://rust-cli.github.io/book/index.html). I was tempted to do that for clap where example code and output (all verified via trycmd) are pulled into an mdbook site but I've stopped short and just have a README that links out to everything (https://github.com/clap-rs/clap/blob/master/examples/tutoria...). Its not great.
All the examples are tested too. So not sure about the problem there.
Can't speak to 'derive' docs. I rarely use derives outside of what std/serde give you, and I never publish any.
But even so, I didn't say that rustdoc has zero weaknesses. :-) I said it is one of my favorite things about Rust because it is just so damn good. I've tried writing docs in several other languages before and they are leagues behind IMO. I would absolutely not call it "crippled." Not even close.
intradoc links are great, and I disagree with GP that rust's documentation tools are bad, they are pretty great. However, intra doc links have the limitation that you can't link to downstream crates: https://github.com/rust-lang/rust/issues/74481
It is a good thing that the documentation for different projects looks entirely different. I find that for me, it makes it much easier to keep track of and remember everything you're looking at if everything has a different aesthetic anchoring it, both in working memory (if you're looking at documentation for several projects simultaneously) and long-term memory.
Let me takes this opportunity to explain that among the many contraints of rust, it is the undertalked one about the absurd no cast promotion from smaller integer (e.g. a char) to a bigger integer that made me quit and save my sanity. Having to make explicit casts a dozen times per functions for basic manipulations of numbers on a grid (and the index type mismatch) is an insult to the developer intelligence. It seems some people are resilient and are able to write nonsensical parts of code repeatedly but for me, I can't tolerate it.
I don’t mind a few “as usize” casts because usually you can cast once and be done with it. But the cast that kills me is this one:
How do you add an unsigned and a signed number together in rust, in a way which is fast (no branches in release mode), correct and which panics in debug mode in the right places (if the addition over- or under-flows)? Nearly a year in to rust and I’m still stumped!
You didn't specify sizes, or if you wanted the result to be signed or unsigned, but "assume two's compliment wrapping in release and panic in debug on over/underflow" is the default behavior of +.
not as nice, but it does work. If you were doing this a lot you could macro it up, impl as a method on all the various types you want... a pain, but it is possible.
Ah. I should have specified - I want to do this with usize / isize so the trick of using a larger integer type wouldn’t work reliably. I can use wrapping_add, but how do you detect overflow in a debug_assert statement?
Huh! I was expecting adding u128 integers to be slower because of the cast; but it looks like llvm is (correctly) realising the upcast + downcast has no effect and replacing it with a single u64 add in release mode.
I want to do some additional testing to check if it also optimizes correctly for wasm and in 32 bit contexts, but generally I'm shocked that works so well. Thanks!
Considering all the type casting bugs prevalent in other languages, I would have more trust in the compiler than programmers at this point. You can always pick javascript of course, which happily returns you what ever it feels like. Frankly this explicit casting makes the next developer's life easier.
I did not read the post, but scanned for the first contra-argument: A very dense syntax. This is the reason Rust did not attract me.
I want to raise the following: Rust is overengineered. If these highly-intelligent contributors would settle on D, I think humanity/developer-community would archive more collaborations on essential pieces of software.
Imo a statically-typed language is required to develop maintainable code. Human communications, read documentation, is much easier to extend than compilation-restrictions of a programming language.
What are the non-fixable downsizes, which prevent serious adaptation of D? readsupuponthepostbecauseaCcomparsionwasspotted
My personal opinion is: The convenience of tooling. Currently I am developing a language agnostic language server, which aims to be integrated in unix environmets without requiring exorbitant memory (currently 8 MB + file-contents). I feel, that this is my only contribution I can submit to the community iff I suceed.
> This is a superficial complaint, but I found Rust syntax to be dense, heavy, and difficult to read
:)
If you think that Rust is dense and difficult to eyeball, please do try... Swift - purely for therapeutic reasons. But not the usual, trivial, educational-sample, evangelism-slideshow Swift, please, but real-world, advanced Swift with generics. All the unique language constructs to memorize, all the redundant syntactic sugar variations to recognize, all the special-purpose language features to understand, all the inconsistent keyword placement variations to observe, all the inferred complex types to foresee, etc. will make you suddenly want to quit being a programming linguist and instead become a nature-hugging florist and/or run back to Go, Python, or even friggin' LOGO. I'm tellin' ya. And, when considering Swift, we're not even talking about a systems programming language usable with, say, lightweight wearable hardware devices, but about a frankenstein created (almost) exclusively for writing GUIs on mobile devices usually more powerful than desktop workstations of yesteryear :).
Considering the speed of adoption of server-side Swift as well as the progress of Swift's support for Linux, I am guessing we are talking about some expert variation of the pump-and-dump investment technique here ;).
I'm a huge Rust fan, but sort of agree. First, I dislike C-style syntax in general and find it all very noisy with lots of unnecessary symbols. Second, while I love traits, when you have a trait heavy type all those impl blocks start adding up giving you lots of boilerplate and often not much substance (esp. with all the where clauses on each block). Add in generics and it is often hard to see what is trying to be achieved.
That said, I've mostly reached the conclusion that much of this is unavoidable. Systems languages need to have lots of detail you just don't need in higher level languages like Haskell or Python, and trait impls on arbitrary types after the fact is very powerful and not something I would want to give up. I've even done some prototyping of what alternative syntaxes might look like and they aren't much improvement. There is just a lot of data that is needed by the compiler.
In summary, Rust syntax is noisy and excessive, but I'm not convinced much could have been done about it.