Rust: A Critical Retrospective
bunniestudios.com
bunniestudios.com
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.
Reminds me of this section of Rich Hickey talk: https://www.youtube.com/watch?v=aSEQfqNYNAc
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.
"I don't even see the code anymore. I just see blonde, brunette, ..."
I myself have just started to get like that with my understanding of Rust:
"I don't even see the `impl<P: 'static, const PS: usize> IndexMut<usize> for SomeThing<P, PS>` anymore. I just see a mutable iterator."
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.
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) }
..or panic on None let bla = make_bla().unwrap(); ??i32 const assert = @import("std").debug.assert;
fn get12() i32 {
const opt_opt_val: ??i32 = 12;
const val = opt_opt_val.?.?;
comptime assert(val == 12);
return val;
}
[https://www.godbolt.org/z/oncd5shvP]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.
I'm not familiar enough with it to know how much is truely in the protocol vs what the editor still has to do themselves.
The problem is that most on FOSS community tend to design languages for VI/Emacs workflows.
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.
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.
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.
https://golangdocs.com/system-programming-in-go-1
EDIT: formatting
Of course, the garbage collector did not exactly make it easier - but it's an interesting piece of software.
but it's an interesting piece of software.
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...
Languages that are not systems language trade-off this capability for other benefits like concise expressiveness and ease of use.
Despite my opinion on Go's design, I rather would like to see people using Go instead of C for such use cases.
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.
https://news.ycombinator.com/item?id=31227986
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.
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.
You can write a database with it but that makes it an application language, not system.
Otherwise you could call every language a "system language" and the distinction would lose all meaning.
I also personally find Go syntax to be horrible, especially now with generics.
I personally consider better use Go than C for such purposes, even if they aren't "systems programming".
It is like that, because of all the chaining that one can do. It is also just a feeling.
Sometimes it's also the formatter though that outputs intensely sideways drifting code: https://github.com/rust-lang/rust/blob/1.60.0/compiler/rustc...
To counteract it, I write exit-early code like this:
let foo_result = foo();
if let Err(e) = foo_result {
return Bar::Fail(e);
}
let foo_result = foo_result.unwrap();
... let foo = match foo() {
Ok(foo) => foo,
Err(err) => return Err(err),
};
I was typing it out so often I made editor aliases `lmo` and `lmr` for Option and ResultThe code above can be written as:
let foo = foo()?; let foo_result = foo()
.map_err(Bar::Fail)?;You can write it like this:
let foo_result = match foo() {
Ok(v) => v,
Err(e) => return Bar::Fail(e)
}; if let Ok(x) = my_func() {
// ...
}
Or do you mean something else? let Some(x) = foo() else { return 42 };Of course, that's what goto is actually good for.
Although I may have PTSD from Rust, because lately I find myself preferring Qbasic in my spare time. ¯\_(ツ)_/¯
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.
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.
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 ).
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.
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.
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.
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.
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.
Also, the ownership mode, a concept entirely missing from Kotlin or Scala.
As GP says, Rust's syntax is pretty noisy, but much of the noise is answers to questions other languages don't even ask.
And many complains are requests for additional noise for things which are just regular in Rust, like additional syntactic sugar for Option and Result.
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.
template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int>* = nullptr>
void func(T fp) { ... }
C++20: void func(std::floating_point auto fp) { ... } fn func<T: FloatingPoint>(fp: T) { ... }
And the "auto" variant is similar to impl argument in Rust: fn func(fp: impl FloatingPoint) { ... } template<std::floating_point T>
void func(T t) { ... }
(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 template<typename T>
concept CanBlah = requires (T t) {
t.blah();
};
and still be able to do void func(CanBlah auto t) {
log << "func:" << t;
t.blah();
}
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.
> 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
C++ didn't have any other practical choice here.
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.
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.
I meant polymorphism, my bad
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".
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
That's easy then, if you mean "whatever the language is supposed to do for std::totally_ordered" you could say what that is exactly, right?
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'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).
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.
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.
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.
No memory management in Nim equals no memory safety guarantees. Or no? Well in that case the statement above is true.
> 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.
Nim is choice. :-) {EDIT: As DeathArrow also indicated! }
"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,
https://docs.microsoft.com/en-us/cpp/cppcx/ref-classes-and-s...
https://docs.microsoft.com/en-us/cpp/dotnet/how-to-define-an...
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.
https://nim-lang.org/blog/2020/10/15/introduction-to-arc-orc...
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.)
https://nim-lang.org/blog/2020/10/15/introduction-to-arc-orc...
For reference cycles (the big limitation of reference counting), there's ORC: ARC + a lightweight tracing garbage collector.
As I understand it Rust also cannot handle reference cycles without manually implementing something similar.
https://nim-lang.org/blog/2020/12/08/introducing-orc.html
https://doc.rust-lang.org/book/ch15-06-reference-cycles.html
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:
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.
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.
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.
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.
Objective-C makes everything verbose. It’s too far in the other direction. Memories of stringByAppendingString
e.g.
https://turreta.com/2019/12/24/pattern-matching-declarative-...
Here's the example from the post:
Trying::to_read::<&'a heavy>(syntax, |like| { this. can_be( maddening ) }).map(|_| ())?;
How would you prefer to write this?First, lifetimes are elided in most cases.
Second, the curly braces for the closure are not needed and rustfmt gets rid of them.
Finally, the "map" on result can be replaced with a return statement below.
So, in the end we get something like:
Trying::to_read(syntax, |like| this.can_be(maddening))?;
Ok(()) Trying\to_read\[&'a heavy](syntax, |like| { this. can_be( maddening ) }).map(|_| ())?;
I can't improve it that much```pub fn horror()->Result{Ok(Result(mut &self))}```
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.
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.)
Besides, what is the error type for Result? You haven't declared it.
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).
Are you sure? What stops Swift with its beautiful syntax and safe optionals from becoming a systems language?
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.
True, but Rust is being used for a lot more than just system programming, judging from all the "{ARBITRARY_PROGRAM} written in Rust" posts here on HN.
They can be ergonomic high level, while providing the language features to go low level when needed.
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"
One of these days. In the meantime I'm rather happy with Go because it's easy for a code monkey like myself to do the needful.
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.
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.
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.
It is surprising how far you can get with that approach without ever writing a sigle lifetime annotation.
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.
[0] https://www.buzzfeed.com/ludwigwittgenstein/fantastic-ways-t...
i.e. Java doesn't have value types and that makes the syntax a lot cleaner vs. C++, and I'm pretty sure Kotlin must be the same
So you could still have ownership in a language with references only, and the syntax could be cleaner than Rust.
In fact I think there was a post about just that: Notes on a Smaller Rust https://without.boats/blog/notes-on-a-smaller-rust/
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.
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.
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 crapTogether with Actors types/Distributed runtimes, Optional Automatic Reference Counting and good support on Linux, Swift is developing into a killer language imo.
[1] https://forums.swift.org/t/a-roadmap-for-improving-swift-per...
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:
Trying::to_read::<&'a heavy>(syntax, |like| { this. can_be( maddening ) }).map(|_| ())?;
Trying.to_read @('a heavy) syntax (\like -> can_be this maddening) >> pure ()
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 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:
syntax
.to_read::<&'a heavy>(|like| this.can_be(maddening))
.map(|_| ())?;
In my opinion, this is honestly not bad.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.
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.
Python:
array = [1, 0, 2, 3]
new_array = map(
lambda x: x * 2,
filter(
lambda x: x != 0,
array
)
)
Rust: 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.Edit: Collected iterator into Vec
array = [1, 0, 2, 3]
new_array = [x * 2 for x in array
if x != 0]
Just as a matter of style, few Python programmers will use lambda outside something like this: array = [...]
arry.sort(key=lambda ...)I still feel like chained methods are easier to read/understand, but list comprehensions aren't that bad.
one could use multiple expressions in lambda in (modern) Python
x = 1 y = 2
q = list(map(lambda t: ( tx := tx, ty := ty, tx+ty )[-1], [1, 2, 3]))
print(q)
At least it's not Go's "You gonna write a for loop or what?"
See also: Async.
Ah, the good ole generic, nested, self-referencing, unnameable state machine generator.
new_array = [x*2 for x in array if x != 0]
is much more common.2. In your example, `new_array` is an iterator; if you need to transform that into an actual container, your rust code becomes:
let new_array = array.into_iter()
.filter(|&x| x != 0)
.map(|x| x * 2)
.collect::<Vec<_>>();
And there your generic types rear their ugly head, compared to the one liner in python.While in python you have list/dict/set/generator comprehension and that's it.
But you have to admit that is a pretty noisy line that could be difficult to parse.
let new_array: Vec<usize> = array.into_iter()
.filter(|&x| x != 0)
.map(|x| x * 2)
.collect();
Isn't so bad. 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.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.
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.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'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.
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.
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.
As for until Rust, Cyclone and ATS did it first.
https://en.m.wikipedia.org/wiki/Substructural_type_system
Its biggest achievement is making them more well known to mainstream devs without CS background.
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.
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.
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.
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 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.
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.
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.
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.
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.
exactly. always noticed people interested in excessive syntax tricks are only good at precisely that. excessive syntax tricks.
zig?
Optimize for readability. Rust doesn't seem to do this.
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.
I thin Rust isnt nearly as complex
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).
I don't have solid evidence for that - more of a feeling. But I wonder if switching to reference counting would lose that.
Anyway, just an observation.
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.
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 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 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.
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.
let std::ops::Range { start, end } = 5..10;
So the 'if' is "just" allowing you to also write refutable patterns.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.
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();
}
with match get_name(group_index) {
None => {}
Some(name) => {
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 {'.
[1]: https://github.com/BurntSushi/regex-automata/blob/fbae906823...
[2]: https://github.com/BurntSushi/regex-automata/blob/fbae906823...
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 }) {
// ...
}> 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.
Suppose that we have:
struct Foo(i32);
A few more examples of irrefutable binding:1. As a local variable:
let Foo(x) = Foo(42);
2. As a function parameter: fn bar(Foo(x): Foo) {}> 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.
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.
Regarding the reproducible builds concern around paths being integrated into the binary, a flag exists to get rid of paths: --remap-path-prefix
https://doc.rust-lang.org/rustc/command-line-arguments.html#...
On nightly, there is also remap-cwd-prefix added by the chromium team to address some of the shortcomings with remap-path-prefix: https://github.com/rust-lang/rust/issues/89434
Overall I'm really impressed that an individual wrote 100 thousand lines of Rust. That's a lot!
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.
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.
Oops, that's the bit I must have missed. That does sound like an ordeal and I don't have an easy answer.
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.
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.
He is referring to the allocator api[1], not the std lib module
> 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.
For reference, the Erlang VM, which is often joked as being "an operating system unto itself" has 11? IIRC allocators.
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.
"In the long term, the philosophy behind Xous is that eventually it should “get good enough”, at which point we should stop futzing with it."
I wished more people would get this.
I thought `cargo vendor` already did this?
https://doc.rust-lang.org/cargo/commands/cargo-vendor.html
> 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 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.
If they don't, I wonder if one of the auditing commands should support drawing attention to build.rs and proc macros like this.
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!
NixOS/guix are gonna solve this issue once and for all (famous last words)
- 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?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.
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.
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.
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.
They are almost always irreversible too. Like you can't undo the steps the shell scripts have done.
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.
0: https://www.rust-lang.org/tools/install#rustup
1: https://github.com/rust-lang/rustup
2: https://forge.rust-lang.org/infra/other-installation-methods...
On most languages, you must decide to do it to create a mess. Bash is almost alone on the place where you can do it by accident.
The one mess you see from other languages is creating files on the wrong place (or all over the place). But not those above.
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.(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.
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.
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.
[0] https://github.com/valvesoftware/steam-for-linux/issues/3671
Should we take bets on whether this happens first, or whether nuclear fusion becomes mainstream first?
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"?
I don't have a strong opinion that it's good or bad practice, I just thought it was a clever thing to do about it.
Edit: I think I was thinking of https://news.ycombinator.com/item?id=17636032 / https://www.idontplaydarts.com/2016/04/detecting-curl-pipe-b.... Wow, it's been a while.
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.
People copy and paste these commands. They don't type them out. There's a big "copy to clipboard" button next to the rustup one.
that's what rust is about in my own experience. Especially with threads.
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!
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.
i'm sure rants are cathartic for the writer, but i rarely find them compelling.
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.
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.
The first such change was implemented in 1.47.0 in 2020 where a bunch of traits were made work on all array sizes: https://github.com/rust-lang/rust/blob/master/RELEASES.md#ve...
It took a few releases, until 1.51.0 in 2021, until custom traits could be implemented for arrays of any length: https://github.com/rust-lang/rust/blob/master/RELEASES.md#ve...
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.
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.
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).
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?
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.
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.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
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.
For folks who aren't familiar with Rust, implementing Default yourself looks something like: https://play.rust-lang.org/?version=stable&mode=debug&editio...
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.
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.
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.
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.
As the person you replied to said, usage by other industries. Replacement to MISRA C maybe even.
• 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.
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?
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++.
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.
[1] https://github.com/Rust-GCC/gccrs
[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...
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.
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".
ISO Standards are not generally required. https://news.ycombinator.com/item?id=28366670
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.
https://www.stroustrup.com/JSF-AV-rules.pdf
Also, when something is an ISO standard, then governments can't legislate that some countries may not be allowed to use it.
> The scenario is somewhat hyperbolic, but the US and European centric nature of Rust gives people in less developed nations pause.
This point is correct with every semi-major programming languages (top 100 popular?), so I don't think it's just a Rust problem.
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.
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.
Well, for one Rust is open source, so you could download the source code and comment out the country ban yourself?
Want to catch on? Be a virus. Not some gosh-darned international standard.
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.
Then I realized the OP was THE "Bunnie" of Xbox reverse engineering fame [1]. <3
Dude wrote more code per week than me in last 6 month at daily job
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).
I do agree with his points, but I don't think it's enough to disqualify it for mainstream use.
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.
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.
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.
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.
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.
See arudino for one example.
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.
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!").
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.
As much as I like Rust, I am keeping an eye on Zig. But Zig 1.0 is too far away to be considered as a contender right now.
I always thought it was because of the added cost from increased design complexity. Is it something else?
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.
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.
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?
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.
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.
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.
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.
[1]: https://www.cs.cmu.edu/~rdriley/487/papers/Thompson_1984_Ref...
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.
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.
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.
curl > install.sh
less install.sh
sh install.shI 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.
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.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.
Compare this to the Python docs, or some truly excellent Python library docs (like Trio: https://trio.readthedocs.io/en/stable/, or Flask: https://flask.palletsprojects.com/en/2.1.x/, or Django: https://docs.djangoproject.com/en/4.0/https://docs.djangopro...), which are all written using Sphinx and integrate properly with crossrefs and such rather than writing manual markdown links as an example.
Of course for a more serialized tutorial, rustdoc is not a good fit so we have mdbook.
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]
Internal APIs need loving too.
For examples, its improving with the example scraping work (e.g. https://docs.rs/clap/latest/clap/struct.ArgMatches.html#meth...) but testing of example is still lacking. I've written trycmd to help (https://github.com/assert-rs/trycmd).
For derive reference and tutorial documentation, your choices are
- A very long, hard to navigate top-level documentation, see https://docs.rs/structopt/latest/structopt/
- External documentation, see https://serde.rs/
- 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.
Yeah I've done this and it works fantastic: https://docs.rs/csv/latest/csv/tutorial/index.html
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.
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 +.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
You can write your own debug_assert! though: https://play.rust-lang.org/?version=stable&mode=debug&editio...
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.
I was kinda worried the release case would end up being worse, but it seems okay to me. Does that work for you?
> how do you detect overflow in a debug_assert statement?
compare to usize::MAX cast up to the larger number.
It also will happily vectorize and all the rest:
https://rust.godbolt.org/z/hn888ezj4
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!
> Rust Is Powerful, but It Is Not Simple
This is exactly why I haven't gotten into Rust.
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.
:)
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 :).
Rust is complex, but very good.
Conversation from last week:
Me: "So what are you working on at $company?"
Friend: "We're building a complete HVAC management system for all types of buildings, from hardware to software"
Me: "Cool! What technologies are you building it on?"
Friend: "Swift"
Me: "...for like an iOS app to monitor the system?"
Friend: "No, everything is written in Swift. The entire backend too."
Me: "Interesting... Have you shipped anything yet?"
Friend: "No but the founder is running a prototype in his house and we just secured another round of funding..."
**
Is this a common thing?