They weren't even born when we were using them, or if they were their toys were actually toys not systems programming languages.
Rust was their first encounter with systems programming, and Standard ML linage, after lots of Python, Ruby and JavaScript, and most likely never seen anything else other than C and C++. Most likely that also believe the fairy tale of C being the very first systems programming language.
For example, serde (and serde_json, toml, etc.) is a gamechanger, as is tracing, reqwest, and many others.
So, maybe less about the language itself for many people, but more about the human factor of overcoming fear of being stuck on an island
To quote Hoare in 1980,
"A consequence of this principle is that every occurrence of every subscript of every subscripted variable was on every occasion checked at run time against both the upper and the lower declared bounds of the array. Many years later we asked our customers whether they wished us to provide an option to switch off these checks in the interests of efficiency on production runs. Unanimously, they urged us not to--they already knew how frequently subscript errors occur on production runs where failure to detect them could be disastrous. I note with fear and horror that even in 1980 language designers and users have not learned this lesson. In any respectable branch of engineering, failure to observe such elementary precautions would have long been against the law."
===> I note with fear and horror that even in 1980 language designers and users have not learned this lesson.
Referring naturally to C, by the way 1988 is the birth year of the Morris Worm, taking advantage of C's buffer management capabilities.
Yes, a pretty low bar that 40 years after those events is still largely used across the industry.
And the copy paste compatibility of Objective-C and C++ with C doesn't help, because even though those languages provide better capabilities and safer alternatives than writing raw C, there are always those folks that will write C style code no matter what.
Network effects and business' risk aversion.
Nothing to do with technical superiority / inferiority really. The MBAs only hear "we must replace everything from scratch" and immediately shout "NO!".
Offensively simple I am afraid. That's all there is to it.
Believe me, I'd jump with joy if we scrap like 75% of all UNIX crap, especially "untyped text rulz" in the terminal -- and introduce one good terminal protocol in the process. But it ain't happening in yours and my lifetime.
Though to me it seems that a good percentage of those efforts will improve a general area. Still your point stands.
Sure, you still get use-after-free and manually memory management, which does not hold folks to now race into rewriting stuff in Zig.
However it gets you covered on:
- proper strings with bounds checking
- proper arrays with bounds checking
- no pointer decays, you need to be explicit about getting pointers to arrays
- less use cases of implicit conversions, requires more typecasts
- reference parameters reduce the need of pointers
- variant records directly support tags
- enumerations are stronger typed without implicit conversions
- modules with better control what gets exposed
- range types
- set types
- when using COM like interfaces, reference counting
- arenas
Sure C++ also covers some of that, that is why I went C++ after Turbo Pascal, but too many folks still use C like idioms instead of improved C++ ones, it is like adopting TypeScript, but still write JavaScript instead.
EDIT: typo, missing not
I find the allocate / try / finally / free paradigm in Delphi makes things easier, it just becomes a habit.
eg
var obj := TMyObject.Create
try ...do stuff
finally obj.free //(or for bonus points freeandnil(obj))
end;I kinda vaguely remember Erlang being mentioned, but I don't recall Ada being discussed. To be fair, I didn't attend many of these conversations.
Otherwise Rust would not have inherited a ton of obsolete and useless features of C.
For example, when I have seen for the first time a "Hello, world" written in Rust, I have been appalled to see that in a language designed in the 3rd millennium one still has to specify whether function parameters are passed by value or by reference, which is the wrong thing to specify about function parameters, exactly like specifying in old C language whether a local variable was "register" or "auto" was the wrong thing to specify. (The right thing to specify about function parameters has been established in 1977, almost a half of century ago, by DoD "IRONMAN", which has been implemented only in a few languages, the most important being Ada. In my opinion, most difficulties and complexities of C++ have been generated by its failure to comply with "IRONMAN".)
In my opinion, attempting to design a new programming language without first studying carefully the history of programming languages, in order to know well the many good and bad choices that have already been used in various programming languages and to understand which have been their advantages and disadvantages, is an amateurish approach that cannot result in real progress.
Most recent programming languages have been built around some innovative great idea that makes that programming language better than the previous in some respect, e.g. Rust for compile-time checking of memory accesses, but because in the other areas of the language the best previous features have not been chosen, no language is uniformly better than older languages, all of them are better for something than older languages, but worse than older languages for other things, which makes the choice of a programming language very frustrating.
7.2.F. (7F.) FORMAL PARAMETER CLASSES
There shall be three classes of formal data parameters:
input parameters, which act as constants that are initialized to the value of corresponding actual parameters at the time of call,
input-output parameters, which enable access and assignment to the corresponding actual parameters, and
output parameters, which act as local variables whose values are transferred to the corresponding actual parameter only at the time of normal exit. In the latter two cases the corresponding actual parameter must be a variable or an assignable component of a composite type.
7I. RESTRICTIONS TO PREVENT ALIASING
Aliasing (i.e., multiple access paths to the same variable from a given scope) shall not be permitted. In particular, a variable may not be used as two output arguments in the same call to a procedure, and a nonlocal variable that is accessed or assigned within a procedure body may not be used as an output argument to that procedure.
These are very reasonable!7I is important (in the mutable case), and solving the problem that bought forth this rule is the core idea behind Rust.
7F draws useful semantic distinctions: Output parameters are return values. Input-output parameters are mutable references. Input parameters act as a copy or readonly reference – which are equivalent if you can't mutate or observe mutation through readonly references.
Your complaint is that Rust draws a distinction between whether input parameters are implemented via a copy or a reference, but Rust draws many more distictions here – arbitrarily many so, because it's part of the type system. This makes the system both applicable everywhere instead of only in function parameters and allows expressing more different semantics.
For example I have a hard time seing how one would, while following the Ironman requirements, distinguish parameter whos type is "dynamic array of elements of T" from one of type "dynamic array of elements of type mutable reference to T". Likewise you can define a record that holds a mutable reference and an immutable reference, or define a new reference type (such as a reference counted pointer) without new language support.
I said that passing a copy and a readonly reference are equivalent. However, in Rust there are no readonly references. Rather, there are non-aliasing references, which allow mutation, and aliasing references, which by default don't allow mutation – but types can make use of interior mutability to allow mutation through aliased references according to they rules they need. For example you can access data protected by a mutex if and only if you hold the lock, which means that even if other references to the mutex exist, there are no other references to the inner data.
> Input-output parameters are mutable references.
In/out can also be by value, it would copy any updates back after the call.
> Input parameters act as a copy or readonly reference
This is what your parent is getting at: the idea is that in/out/inout describe intent, not mechanism. The compiler chooses the mechanism for you.
I think in a language that's intended for lower-level tasks, describing mechanism is important. That said, outside of STEELMAN, there's an argument to be made for in/out/inout, and in fact, there's been some discussion over the years, for example, &uninit T as a sort of variant of out.
> For example I have a hard time seing how one would, while following the Ironman requirements,
You're right. STEELMAN updated this section to say
7I. Restrictions to Prevent Aliasing. The language shall attempt to prevent aliasing (l.e., multiple access paths to the same variable or record component) that is not intended, but shall not prohibit all aliasing. Aliasing shall not be permitted between output parameters nor between an input-output parameter and a nonlocal variable. Unintended aliasing shall not be permitted between input-output parameters. A restriction limiting actual input-output parameters to variables that are nowhere referenced as nonlocals within a function or routine, is not prohibited. All aliasing of components of elements of an indirect type shall be considered intentional.
Ada has "access types," which are pointers. You can declare them as aliased or not. Via this mechanism, you can pass both an aliased variable as an in parameter, and an access type that points to it as an in out, and still modify the variable, even though it's in. This will give you surprising behavior, but it's not UB, so you get "oh hey that number is not what it should be" not "this means half your program is optimized away.However, unlike Rust, Ada does do strict aliasing, and so you can also get miscompilations that way. It requires the moral equivalent of unsafe, though. See https://gcc.gnu.org/onlinedocs/gcc-7.5.0/gnat_ugn/Optimizati... for an example.
Ada does prevent data races, because it has a built-in task system, and that task system only allows multiple tasks to access "protected objects" that are basically RWLock<T> in Rust terms.
Rust is very much designed as a low-level language. If you want a higher-level language with similar guarantees, OCaml, Haskell, F# or Scala do the job very well already.
The mechanism of argument passing has its uses in terms of both performance control and avoiding unwanted aliasing, something that not many languages do properly (not even Ada, iirc).
It's not the right thing to do for all languages/applications, but I believe that it's the right thing to do for Rust. Do you have a better design in mind?
In Ada, there are also requirements that some types are always passed by value or by reference. And as of Ada 2012 there's a way to force the "always pass by value" types by reference.
A while back someone wrote a post comparing Rust to the sort of design your parent prefers https://web.archive.org/web/20241009004034/https://jedbarber... I remember it being... fine.
The short of it is, Rust doesn't do great because STEELMAN wants subtyping and contracts (though I see contracts may be coming to Rust...) as well as return values instead of exceptions.
Also, yay for contracts in Rust :) Looking forward to seeing them proven by model-checkers, too!
Graydon is incredibly knowledgeable about programming languages.
Just because you have certain opinions about language design does not mean others do not know things.
Speaking of knowing history, Ada implements STEELMAN, not IRONMAN.
Rust's original designer credits these languages as the inspiration: http://venge.net/graydon/talks/intro-talk-2.pdf
Mesa (1977), BETA (1975), CLU (1974), Nil (1981), Hermes (1990), Erlang (1987), Sather (1990), Newsqueak (1988), Alef (1995), Limbo (1996), and Napier (1985, 1988).
The original Rust compiler was written in Ocaml.
> has to specify whether function parameters are passed by value or by reference
You specify whether arguments are borrowed or moved, and whether the access is shared or exclusive. This is not an implementation detail, it's an API contract that affects semantics of the program. It adds or removes restrictions on the caller's side, and controls memory management and thread safety.
People unfamiliar with Rust very often misunderstand Rust's borrowed/owned distinction as reference/value, but in Rust these two aspects are orthogonal: Rust also has owning reference types, and borrowing types passed by copying. This misunderstanding is the major reason why novices "fight the borrow checker", because they try to avoid copying, but end up avoiding owning.
There are different possible approaches to achieving similar results for argument passing, but Rust prefers to be explicit and give low-level control. For example, Mutable Value Semantics is often cited as an alternative design, but it can't express putting temporary loans in structs. The syntax needs a place to declare lifetimes (loan scopes), as otherwise implicit magic makes working with view types tricky or impossible: https://safecpp.org/draft-lifetimes.html
Rust is objectively better than what I tried before, in several ways.
From where I was standing several years ago, there was almost zero "human factor" in adopting Rust for many companies (and devs). People simply saw the better features, performance and security and got excited and wanted to work with that language. And then many mistook that for hype. Yourself included apparently?
Sometimes things get popular just because they are good, you know. Not super often historically, mind you, but it does happen. IME Rust is one of these outliers.
It also wasn't the point. There was a number of completely unnecessary annoyances.
Academics writing compilers actually seemed like a good idea to people back then. Still makes me smile thinking of it sometimes.
A lot of modern compilers don't seem to be directly done by academics though. If you know of such modern examples I'd be happy to be proven wrong.
It is full of PhD paper contributions.
For example, research students doing internships at Embecosm (https://embecosm.com), as many others, plenty of material from LLVM Developers Meeting sessions.
Obviously a lot of what passes for "AI" these days is researched and created by academics.
Thanks.
Ada does not offer anything similar. It's designed for programs that are mostly static. It also does not compose well, as you can't dynamically allocate without losing most of Ada touted advantages.
To be fair, they recognized it. Their plan is to adopt a borrow-checker.
For example I just ripgrep. Arc is used 29 times, compared to 185 uses of Vec.
Is that true, though? Many memory-safe languages are very fast (fast enough for the vast majority of use-cases). Feels like mostly they compromise on memory, don't they?
Rust seems to be a good replacement for those use-cases where C++ is needed. But given the popularity of Rust, it seems to me that many people use Rust where they don't actually need it.
(Disclaimer: I do like Rust)
That depends on what "fast enough" means. A better way would be to say "memory-safe with (near) zero overhead".
I would think that the main cause for wasted CPU cycles is the code written by the developer, not the language.
My point was that it's not enough to say "If you use Rust, your code will run faster", because the language does not do it all. Most code out there is largely inefficient. The tendency is to pile up dependencies and frameworks to be more productive.
No need for Rust to make an ElectronJS app vastly more efficient.
Similarly, I recently wrote a TUI with a popular Rust TUI library, and it takes up to 10% CPU just by refreshing the view when typing a text. It's not my code, it's the (again, popular) library that explicitly doesn't consider that a problem. It's not because it is written in Rust that it is efficient, is it?
no, but 1) Rust significantly raises the boundary and on average it's probably more likely too
I don't see why they would care more, and in my experience they don't, on average.
Your experience is a singular event not representing the average.
My original opinion was that people should use more Rust to write more efficient software, because it's beneficial to humanity.
You're welcome to use Python for whatever slow scripting you personally need, but it's not suitable for writing an OS and libraries because it's going to make every program slower, and while that might be ok for you, it's not going to be ok for a lot of people.
> without compromising on speed
I was just trying to explain why that matters to you.
Nobody is saying you have to use Rust for everything.
> Rust is the first (yes, first) mainstream language that offered reasonable and composable memory safety, without compromising on speed.
My point is that I am not completely sure if we can say that Rust is the first (yes, first) at that.
Before you get at me for "fast enough", let me say that "without compromising on speed" doesn't mean anything either. I can write super slow code in Rust, it's not like the language prevents me from doing it.
My point is that in many cases, the speed is really not the problem with the JVM.
It means you couldn't write faster code if you did it by hand in assembly. In C++ they call it "zero-cost abstraction" (you can Google that).
Adding GC adds all sorts of overheads that you can avoid in C and Rust.
Obviously in some cases you can beat the compiler, but in general it means there aren't any systematic overheads.
I Googled "without compromising on speed" and didn't find a formal definition. Would you mind helping me there?
"Zero-cost abstraction" is a concept I understand. But... are you sure that it would be impossible to write faster code (even if you did it perfectly in assembly) than what Rust generates when you use `RefCell`?
Feels like it is compromising on speed, to some extent.
> but in general it means there aren't any systematic overheads
Or maybe I am wrong and there are absolutely no systematic overheads in any concept similar to `RefCell` in Rust? Is `RefCell` zero-cost? That's not my understanding.
It's not a formal term. The meaning was clear though.
> But... are you sure that it would be impossible to write faster code (even if you did it perfectly in assembly) than what Rust generates when you use `RefCell`?
Absolutely!
https://godbolt.org/z/aMPfqz396
The second example is what you'd do if you were writing C or assembly. Rust takes the safer option by default.
> Is `RefCell` zero-cost? That's not my understanding.
I think you are misunderstanding what "zero-cost" means. Or maybe what RefCell does. RefCell checks are runtime if a value is borrowed by some other code. It's kind of like a single-threaded mutex. You can't implement that in assembly any better, so it is zero cost.
You are probably thinking "but in C you don't need to use RefCell at all!", and that is true but unsafe. You can avoid that in Rust, but nobody does because it's 2 instructions (which will be perfectly branch predicted) so it essentially costs nothing.
It's kind of like how `std::vector::at()` is still a zero-cost abstraction in C++. The thing you are abstracting is "array access with bounds check". You can't perform that function any better with hand-written assembly. Saying "aha, but in C we never bother with bounds checks!" doesn't invalidate that.
Was it? You are telling me that it is actually possible to write faster code if it is unsafe, but for some reason it doesn't count as faster in your book, and therefore it's not compromising on speed ("just ignore the faster solutions").
With that kind of definition, all the code I ever wrote is provably optimal: if you ignore all the solutions that are more efficient than my code, then my code is the fastest.
I think you understand at this point and are just nitpicking to avoid admitting it, so goodbye.
You said "you're wrong, it is optimal".
Now you say "it is not optimal, but it is very good, and I think you are nitpicking when you say it is not optimal".
Sure, I'm nitpicking. I was nitpicking from the beginning on, I'm not sure what you were trying to say now.
For all my 23 years of work a language like Elixir (or even Ruby) was "fast enough" but there exist a ton of other world out there that needs more.
Because when I said "for the vast majority of use-cases", I expected people to understand that I agree that some use-cases can't run e.g. a garbage collector.
I just feel like I read a lot of "Rust is the only worthwhile language our there", and actually there are many languages that are a better fit in their use-case.
Pay no attention to zealots. Every community has them. I love Rust beyond what's rational and I still am sensible enough to reject it for projects where it would only prolong development time for not much gain (the projects in question do not need the super-duper speed or to use much less memory than you would get with my favorite Elixir, for example). And for a lot of script-ish / one-off / I-want-to-finish-it-quickly projects I just opt for Golang.
IMO projects like rewriting the UNIX userland utilities in Rust makes perfect sense; the originals are super old and nobody wants to seriously find security bugs in them and since they are written in C you can bet your neck they absolutely do have buffer over-/under-flow bugs that can likely lead to escalation of privileges. Is anybody fuzzing those tools 24/7 for years? They are the buildings blocks of at least 90% of all servers out there!
Rust makes sense in embedded as well, in financial trading, or in any system that has to squeeze every last drop of its hardware in general.
But web applications / API endpoints / API gateways and such? Meh, a compiled language in a VM like Elixir is plenty enough and always will be.
"Right tool for the job" should be the first catechism of the future Tech Priests and their cult to the Omnissiah (insert "Warhammer 40,0000" reference here ㋡).
And adding a GC is a hugely impactful step.
I've seen more segfaults in my Python code than I have in any other language ecosystem. Yes, that's in the C modules that Python libraries so often wrap, but the result is still that Python is far from the best choice for memory safety.
JavaScript actually tends to be better at avoiding dropping into C unless absolutely necessary.