The usability of advanced type systems: Rust as a case study
arxiv.org
arxiv.org
I do accept this, and I try to be honest about it when I'm shilling Rust :) But at the same time, I think it's worth pointing out that what we mean by "learn" can really depend on context. The subjective experience of learning a language looks something like "getting my homework done and turned in for a grade". And in that sense, for sure, learning Rust is harder than learning C. (I won't make a claim about C++.) But what percentage of people who have "learned" C can actually write UB-free C with any reliability? For the folks who really can write UB-free C, how many years did it take you to get to that point?
That said I think it's fair to assume your "getting my homework done" definition, in which case it is definitely harder.
So I would describe it as "harder to learn but faster to finish projects".
An intermediate programmer coming from other high level language can confidently contribute to a big project after ~2-3 months of learning Rust, that number is almost unimaginable for C/C++.
You can have your first Actix[1]/Sqlx website up in a day, and you can have it doing some non-trivial work -- including sharing state between threads -- by Friday if you start today.
Rust's type system alone is a compelling enough reason to try this. Cargo a close second.
I'm willing to bet a lot of people start using Rust for backend web development. It's quite pleasant.
[1] Or Axum
It will happen, but it's not something you should do today. The async ecosystem in Rust is not yet ready for prime time, there are key language and stdlib features that are just missing. And this in turn holds the web ecosystem back. If you write a web backend in Rust today, you might just do a lot of pointless work that you'll have to rewrite in the future anyway as more advanced frameworks come along and the earlier ones are practically abandoned.
1. Use Rust 2. Use containers
I save a lot of time with those two decisions. My lambdas are significantly less likely to have runtime errors. And with containers I can select which version to run from my registry
I had to port async runtime APIs many moons ago and it was an easy migration. Now things are baked in and I haven't had to change anything for years.
> there are key language and stdlib features that are just missing.
Like what?
> If you write a web backend in Rust today, you might just do a lot of pointless work that you'll have to rewrite in the future anyway as more advanced frameworks come along and the earlier ones are practically abandoned.
1. If you write atop Tokio, you're fine. (You wouldn't pick anything else unless you have special case needs for your app.)
2. If you write atop Actix/Axum(Tokio), you're doubly fine -- they're abstracting away everything.
I doubt it. I suspect existing code will continue to work with minor mechanical changes, and the improvements will just open up new use cases that are either impossible or awkward to do today.
How? I would love to give it a spin.
Then move onto Zero 2 Production In Rust (https://www.zero2prod.com)
Edit to add: if there are recommendations for TS/JS front-end development, I'd love to see them.
Through Zero2Prod, it's very nice to see how easy the rust ecosystem makes it to do "high level" web programming with minimal friction. The type system makes Domain modeling fantastic. Options and Results feels like the right way to do error handling, and the `?` operator cleans things up nicely.
I only wish that rust was something I could use at work (we're mainly a Go shop; not my favorite), but I suppose I'll have to wait for the right opportunity.
Depends if one feels like trying their hand at writing asynchronous code.
Async makes it even harder, yeah.
Though a lot of what I'd use async for in Python, I'll use Rayon for in Rust, and it's... fine, really. Better than fine even; it's easy to avoid concurrency issues that way.
Let's pick a competent engineer with zero Rust experience and teach them. I wager you $1,000 that they can write a TODO app in Rust/Actix without looking at a direct example.
I'll even up the wager and concede that they must have zero C/C++ experience. Only garbage collected languages under their belt.
I'm being a bit facetious here as I don't have the time to babysit this wager given my startup, but I really want to make this bet and I'm very comfortable in thinking that I would win it. (Maybe if I offer $1000 to the engineer too as incentive to try - that could work.)
I just don’t think that counts as “learning rust”. Which means that now we’re arguing over semantics.
If your alternative is C and you really need the power of C and a guarantee of no UB, then by all means, (struggle to) learn Rust. But I would argue that limits Rust to very few niches of system/expert programming. The number of people who write these is very, very limited, hence the use of the language, tooling, libraries, etc.
The promise Rust makes nowadays is of an all around safe and productive language that can reward any effort spent fighting the type system and compiler with an error free execution.
For most programming tasks people use languages in the real world, I would say Rust fails to deliver on this, in the sense that the effort spent to master it and use it day to day only affords limited benefits. If you need to move fast and don't absolutely need the strongest reliability guarantees, then Rust is a tar-pit.
That's true, but other type systems will likely fit your needs. For instance, if you're doing web, Elm's type system is head and shoulders above the more traditional options.
It turns out things were already hard, Rust just has the tools to make it tractable to deal with all of it instead of ignoring it.
...except when it comes to build and packaging problems.
The thing missing from this conversation is that we're talking about Rust the language, C the language, and C++ the language. In practical terms, the ways you go from "this is source code" to a running program matters a lot for end users.
Point being, so far the Rust community mostly camps out in cargo land and ignores the hard problems of how to get systems code into the various ecosystems that benefit from close-to-the-metal programming. Maybe some day it will be figured out, but it's essential complexity to the broad problem space.
There are some very talented people thinking about the interoperability story, but it is also hard so they haven't committed to a solution prematurely.
More specific questions will often get extremely helpful and detailed responses, so I'm not remotely claiming the community is horrible. You just have to project a fair bit of "Shiny Happy Rust Person" to get on with it if you don't want to get bogged down with pimply arguments.
I'm around average developer of 40ish years, that learned Rust when it was at its hardest.
And yes, going from Java to Rust was hard compared to C#. I needed months to get proficient, compared to mere hours in C#.
Honestly, learning Git was harder than Rust. That took 4-5 months to re-learn it proper and get rid of training wheels.
And it was as hard as learning Haskell/Prolog. Hell, I'm still horrible in Prolog. And people don't complain as much about those two.
People seem to forget that saying "A new programming language should completely change your mental model", otherwise you're learning a new dialect not a language.
Why do you opt to label plain descriptions of Rust as being difficult as 'complaint'? There's nothing inherently wrong with something being difficult to learn. Au contraire, I would have thought, unless the difficulty is unnecessary, which isn't something I've seen levelled much against Rust. The Rust Foundation has 'Flattening the learning curve' [sic] on its 2024 roadmap: https://blog.rust-lang.org/inside-rust/2022/04/04/lang-roadm.... Is that a 'complaint', or just a reasonable response to an empirical reality?
The complaint, inasmuch as there is one, is against the loud subsection of the Rust community which swamps any reference to Rust's difficulty with "no it's dead easy! You're just wrong!" (they leave the 'and probably thick' as an obvious inference, stiffly maintaining the rictus).
See article
> [Rust's Ownership and Lifetime rules] come with a steep learning curve and have been repeatedly cited as a barrier to adopting Rust
Complaint, in the sense as this is too hard (aka too foreign) for me to learn. Because it's subjective.
Is Norwegian language difficult to learn? For native speaker - no. For European, probably not that much. For a Chinese, probably yes.
For example Prolog is nigh impossible for me to learn and non-ironicaly harder than Rust.
For me as someone who came from primarily working in Python, I really appreciate the strict type system that comes with Rust and have been quite productive in it, even if it feels like I've hit a wall at times. I think it'd be a shame for other people in my shoes to not even explore the possibility because they think it's only meant for a certain class of low-level C++-style programmer.
I haven't come across this so much. More the kind of YA male philosophy undergrad types (of all ages & genders) which tech in general and Rust in particular seems to attract. These people just like to argue. A lot. In interminable screeds, often on the Rust user forum.
The attitude is certainly more loud than universal. Many of the most thoughtful (and helpful) Rust insiders recognise the language's difficulty. The Rust Foundation certainly does, to the point that making Rust more tractable to learners is one of its current stated goals.
> I think it'd be a shame for other people in my shoes to not even explore the possibility
Rust's current cultural kudos in dev circles is such that I don't believe this is really a factor. Many want to try it. It's more that Rust is just difficult for many to actually work with. I don't mean 'learn' in the absolute beginner sense. There's no shortage of excellent and generously accessible materials (the Rust Book amongst others). They're not that difficult to get through. But many find that once they've got a minor grasp on the whole language, it's just a real slog to get anything substantial done with it. Getting through all the Rustlings doesn't mean you can get anywhere near writing real software at a motivating pace. This is certainly my experience, and that of all the pro devs I personally know who have started to learn Rust, all of whom (other than me) have dropped out in frustration. All of whom (including me) have by the way learned and used many other languages without the same troubles.
Pretending (or passively-aggressively asserting in response, as so many do) that Rust is easy does nothing other than (truthfully or not) trumpet the abilities of the arguer.
My guess is there are many factors at play. Some of it may be attachment on the part of those who have clearly exerted great efforts to learn Rust, and perceive assertions that Rust is difficult to be negative criticism (often wrongly). Partly it's the argumentative immature adults that tech fields so often attract. We all know them. It's my guess that Rust, being more towards the techy end of that spectrum, does so particularly (I don't know if this is equally true for C/C++). Partly it might be Rust's self-conscious inclusiveness: a positive in itself, like many similar ideals it can lead to aggressive denial when more clung to as an identity than pursued on as an aspiration.
There you go. My interminable screed. Maybe I would fit into the Rust 'community' after all.
This is something that I noted when I first picked up the language: The rationale and explanations made sense, I could "learn" the rules, but in practice it was much harder, because I hadn't internalized the rules, nor fully grasped the consequences of those rules.
Having said that, I wish someone had told me ".clone() early, .clone() often" and "only return borrows if they are borrowing from an argument, otherwise use owned values" for the first 3 to 6 months. Having to learn the intricacies of borrowed values and a whole FP-inspired systems language at the same time is a lot.
Another rule of thumb I always try to emphasize for beginners:
Don't add lifetime parameters to your structs. Sometimes the compiler will suggest that you do it, but ignore those suggestions. Concretely, always make your structs hold String or Vec instead of slices. In 3-6 months you can ignore this rule.
Make your types Copy. Don't even worry about cloning.
That said, there are few things you want to have lifetimes, and in that case you want to de-elide lifetimes. As practice.
You never go back. It’s nice writing code that just will not fail in certain ways.
My problem is with "comparable systems programming languages". To me the only thing that fits there today is C++ and while a great many programming languages would be easier to learn than Rust, C++ is not one of them by a long shot.
I think the C++ Quiz https://cppquiz.org/ and the Rust Quiz https://dtolnay.github.io/rust-quiz/ illustrate handily. Neither of these languages is a walk in the park, but, notice they both have "Undefined behaviour" as a possible answer? Safe Rust doesn't actually have undefined behaviour, so you get to rule out one of the possibilities any time you don't see the "unsafe" keyword, which is in fact every time on the Rust Quiz. In C++ some of the quiz questions invoke UB, but good luck correctly guessing which ones.
C++ is performance first at any price. Hence UB.
Rust is also after performance, but will specify implementation details where C++ will defer to compiler writers.
The creator of C++ would love for there to be safer abstractions, but has the baggage of the past to carry.
std::optional is relatively new, and supports dereferencing, which when it’s empty is UB instead of an abort.
There are no plans to add checks to any existing APIs, because “it didn’t have a branch before” is also considered a compatibility issue.
I don't think this is entirely correct, C++ in some cases famously preferred ABI compatibility over maximum performance.
Nowadays MSVC tries to maintain ABI compatibility too, so we should stop seeing a proliferation of those msvcrXXX dlls.
Haiku uses C++ for its OS ABI so it relies quite heavily on the fact that ABI compatibility stays very consistent across compiler versions for example.
C++11 is almost as old now as C++98 was when C++11 came out. It's evolved plenty.
This will reinforce bad patterns early on which they will have to unlearn through bug hunting or reading about the edge cases and foot guns.
Python will do that too and without reinforcing patterns that produce UB.
I'm curious if there are any newer alternatives that fit in the same niche.
Of course, that means that it doesn't fit in that niche - it fills it and then some.
The goal of software development is not to write code that compiles, it is to have an executing software system that does the right thing.
But there are many UB-laden, compilable C++ programs that produce the right result, they just are implemented with bad, non-standard, leaky code. Rust and other languages that detect this bad code and force you to remove it are harder to start with, because you need to learn everything to get to a running program.
Interactive development (especially REPLs!) have shown time and time again that it speeds up the learning process. Rust takes the inverse approach imo.
1) UB - this is when code is invoked. Basically memory mismanagement. We know the signs: segfaults, etc.
2) Unsound code - this is code that has the potential to trigger UB if called incorrectly.
I think this distinction is pretty important. It explains why small projects in c++ can get going so quickly, and they have issues when they grow. You can prevent unsound code from triggering UB, but that of course gets harder to do as a project gets bigger.
There is an easier path: you can test your C or C++ stuff with valgrind and get reasonable certainty that it work.
However nowadays we are putting our trust in the borrow checker, and quietly ignore the fact that half of the rust stuff needs to use the 'unsafe' keyword.
If this is true for the Rust code you are writing, either you are doing it wrong or perhaps you are writing lots of low-level embedded / drivers code?
Having written C and C++ for years and now learning Rust, I much prefer the safety and nice features of Rust. Coding in C and C++ now feels like driving in a car without a seatbelt: you can do it if you don't have other options, but if you do have the choice, why wouldn't you?
I haven't had to use unsafe Rust yet, but have had to do some squirrely C tricks in the past for really low-level stuff.
- If there's `unsafe` code in your dependencies, that's potentially just as bad as `unsafe` code in your own project. So you do have to be careful with what you pull in.
- There are known "soundness holes" in the compiler, i.e. compiler bugs that let you corrupt memory without writing `unsafe`. However, the ones that remain are pretty obscure and very hard to hit accidentally.
Now C and C++'s limitations:
- Valgrind and all the sanitizers depend on test coverage. So 100% line coverage is table stakes for getting certainty out of these. And you probably also need to think about coverage of your dependencies.
- None of these tools catch the UB that results from violating the "strict aliasing" rules. I believe there's something called "TypeSan" that promises to improve this situation, but it's experimental?
Is it a new fad in the Rust crowd to confound UB with memory unsafe? Some of the use here seems completely random to me.
Yes, Rust requires approaching writing code somewhat differently than most popular languages (different isn’t inherently bad), and it’s true borrow checker limitations cause it to reject code that is sound. But that increase in cost at the front end, in my experience, is significantly offset by the decrease in maintenance costs.
I’ve spent a non-trivial portion of my career hunting down and fixing memory corruption issues, race conditions, and other undefined behavior in extremely large codebases. Yes, we’ve seen great advances in tooling in Clang+LLVM, but writing that buggy C/C++ code is still quite easy (a point I didn’t see in the paper). Using validation tools to their full extent is costly from maintenance, configuration, and compute perspectives, and their correctness assessments are less strong than what an advanced type system provides.
Preventing such classes of bugs through an advanced type system seems like a better approach to reducing total cost of ownership because:
1. Some classes of (expensive to fix) bugs are eliminated by codifying system behavior into the type system. 2. The need for a variety of CI runs with Address Sanitizer, Thread Sanitizer, Undefined Behavior sanitizer, etc. and their associated corpus of coverage tests is greatly reduced because such issues are defined away by the language. This saves on CI infra costs and requires engineers to spend less time writing/maintaining tests to exercise the tools. 3. More engineers will spend more time moving the product forward and less time hunting down hard-to-reproduce bugs.
Users don't care if you wrote it in Rust (unless the target audience is developers). All they care about is if it solves their actual problem: a real world, pain-in-the-neck problem.
If you take 20% longer to write it in Rust, that's short term cost for long term gain, but users don't think that way. Everyone from the users, sales, marketing, product and even engineering departments want the product out the door yesterday, so the pressure is always on.
Users don't care if you need 30% fewer CI servers. The CEO doesn't care if one additional dev has to spend his time making sure asan, ubsan, etc are running properly. This is cost factored into pricing. Coverage metrics are used as proxy metrics for quality.
Meanwhile the devs are wrangling the complexity of the software itself, which requires mental resources. Any additional complexity introduced by Rust slows down development and frustrates the team and stakeholders.
And the bugs are still there. So many bugs from devs who were sloppy or couldn't tame the complexity that +-20% due to Rust doesn't matter. The QA team still needs to exist and test the product thoroughly. The issues still come back and require fixing, regardless of language used or frequency of bad bugs.
In the end, this software process produces acceptable, good enough software that gets fixed, patched, updated, etc Users see improvements and progress.
Operating efficiency becomes important only after the product is out and stable, with paying customers and product-market fit, but by then, why boil the ocean with a rewrite? Sometimes leadership can be sold on a new and improved v2 (now in Rust), and sometimes they see it as a distraction from the current target (if it ain't broke, don't fix it, add more features instead).
That's why I think we need languages that make it less mentally taxing for developers to write product software (because the logistics are already complicated), while features like the borrow checker, though cool, increase the mental overhead.
Totally agree! But they do care if Word corrupts a document that’s been open for 10 days (a bug that a principal engineer spent many, many weeks hunting down).
> If you take 20% longer to write it in Rust, that's short term cost for long term gain, but users don't think that way.
There are many steps between writing code and delivering a product to users. Sure, I can commit some C++ in less wall time than Rust (though I’m not actually sure that’s true!). But when we have to delay a major release by 2 weeks because there’s a heap corruption bug in the flagship feature, everyone loses.
> the pressure is always on
Sigh, yeah. It’s difficult for organizations to act in their own best interests. If a codebase primarily has architecture debt and little implementation quality debt, it’s much easier to keep taking the shortcuts to ship now. But with implementation quality debt, UB is lurking behind every corner and can cause the project to go off the rails at any time. This is why I would like to see more research about the value of Rust with respect to the predominant cost of engineering in extremely large systems: maintenance.
> Users don't care if you need 30% fewer CI servers
Sure, but DevOps does. When we have to drop jobs due to capacity constraints but mark the commit as stable anyway, it’s just a matter of time until a major regression sneaks in.
> The CEO doesn't care if one additional dev has to spend his time making sure asan, ubsan, etc are running properly
Haha, yeah, in practice it‘s always one dev maintaining the asan, ubsan, etc. loops! But, it’s up to engineers writing code to make sure it’s exercised by those systems, so in reality the loops are highly under utilized and eventually hard-to-find UB bug slips in.
> Meanwhile the devs are wrangling the complexity of the software itself, which requires mental resources.
Yes. And encoding ownership and lifetimes into the type system is a huge reduction in the mental tax when working through code that hasn’t been touched in years, or wiring up some feature to an interface another team just landed that’s not documented at all. Not having to reverse engineer the code to discern ownership and concurrency patterns is a huge time saver.
> Any additional complexity introduced by Rust slows down development and frustrates the team
I agree artificial complexity due to limitations of Rust’s soundness checks are frustrating, but a cost I’m willing to pay given the long term maintenance benefits. But the cost of system design complexity has to be paid at some point, and I’d rather pay as much as possible at compile time vs. discovering problems at run time.
> And the bugs are still there.
Certainly. But essentially eliminating classes of UB bugs that are hard to repro saves all the engineering teams time.
> Operating efficiency becomes important only after the product is out and stable, with paying customers and product-market fit
Yes. Total cost of ownership is what I want to see more research in.
And I think this is the source of our deferring perspectives. My original comment wanted more research on TCO, but I don’t think that’s particularly applicable before product-market fit.
> but by then, why boil the ocean with a rewrite?
Who said anything about a rewrite? Use Rust for new development. And, as the system grows, components will be rewritten (whether due to new business needs or to improve maintainability). Don’t keep playing with fire when it’s no longer necessary!
> Sometimes leadership can be sold on a new and improved v2
Perhaps I’m jaded, but my motto is “Rewrites always fail.”
> we need languages that make it less mentally taxing for developers to write product software
For extremely large systems whose codebases have been written over decades, we need languages that make it less mentally taxing for developers to maintain product software. The complexity of maintaining these systems is why FAANG employs tens of thousands of engineers who seem to move slower than an aircraft carrier. The difficulty in reasoning about system behavior greatly impedes progress.
Caused by memory corruption, or the remaining 30% of logical bugs present in any safe language?
1. Mismatched type definitions in different translation units caused an implicit 32 to 16 bit conversion. 2. An addition to the 16 bit value overflowed, causing it to become negative. (The “run for 10 days” thing helped the value get to the point of overflowing.) 3. The negative value led to an out of bounds write, corrupting a key bookkeeping data structure.
Rust would have failed to compile at step 1, and would have panicked at 3 (and potentially 2, depending on compilation settings). I’m not sure what other safe languages are candidates for this domain, but I would suspect these issues would likely be similarly identified.
That bug, unfortunately was identified after release. The release blocking heap corruption bug I mentioned was due to a C++ object being deleted when it called back to an event handler and wrote to its member variables after the handler returned. Ownership and lifetimes would have prevented this design error (which is surprisingly not that uncommon).
Rust would not have failed to compile at step one necessarily. Or were you assuming a full revamping of the build and dependency management systems in addition to the rewrite of the program itself?
If we need to clean up the build and dependency management system to start using Rust correctly, isn't that two major migrations?
True. In this case there was old code and new code. I assumed in a hypothetical hybrid Rust codebase, bindgen would be used to bring the types in old code to Rust, so the compiler would identify the type mismatch in perhaps the impl From.
> If we need to clean up the build and dependency management system to start using Rust correctly, isn't that two major migrations?
At the scale of Office, managing the build system is an evergreen project staffed by dozens of engineers. Adding support for Rust integration is a typical deliverable for such an org.
If suffixes that the data written for an entry metadata is not the correct one for the actual content.
For sure. But in the 10+ years I worked on Office, I spent far more time per UB bug than logic bugs. 1 UB bug could take weeks to fix. I don’t recall any logic bug taking more than a few days.
Developer productivity would be much improved if logic bugs were the only class of bugs.
I bet if Sinofsky would have had an option those AddIns wouldn't even exist, given his opinion on .NET.
But all Rust do is expose those issues? For example, do you consider a type checker a burden or a help? Is being able to check at compile time that “hey, why are you trying to pass a nullable to a string? This don’t make sense” helpful? I cant count on how many times have saved my time debugging issues.
I agree that Rust is not for everyone. Not everyone will like types even if it good for them. I would argue that a language that can accept dev/random as source to be very popular especially because you can get started very easily even if that means you will spend the rest of your life debugging it.
The type system is objectively overhead, because it forces you to think about the types in your program, so it consumes mental bandwidth.
You and I might decide the cost is well worth it, but you cannot ignore that there is a cost. In the real world, many companies didn't always want to incur that cost in the beginning, so they built their products using scripting languages, Python/PHP/Ruby, etc and only much later added types via annotations or features of the language. Even C++ recognizes that there is a sizable overhead and introduced auto types.
Your /dev/random example makes no sense because it would not generate a working program, so it would not be popular.
There is no 'good for you'. Is the type system good for you if it slows down your velocity? Are dynamic types good for you if you get an exception deep in the code one hour before launch because someone was adding a string to a number?
There are only trade-offs. You either make the right trade-off (even by luck) given some set of goals and limitations, or you don't and pay the price one way or the other.
I don't understand this point. Types is an overhead yes, but its an overhead that you _have_ to deal with. Would typing without knowing what letters would come up be a lesser burden? Seeing the code that I just wrote seems to be an overhead too. Types is simply there to tell you what you wrote. Much like when I realized I mistyped `j` instead of `k`, I would also realize that I am trying to append an `Object` to a `number`.
> Your /dev/random example makes no sense because it would not generate a working program, so it would not be popular.
My point is that a language that will literally, and I mean literally, accept any and all source code and execute them is probably going to be popular. Just imagine a language designed with this goal in mind; to make the set of possible program in the language be the entire space of possible sequences of texts. For example,
```
00000what????;
function hello() {
world
}
```
This seems meaningless. But with that mantra in mind we can simply define that any line that is undecipherable be a declaration to an unused string. Why not? So the first line is is simply something like `let _ = "00000what????";` And the second one is simply a function that returns the string `world`. Why? Oh because its so inconvenient to put quotations to declare a string isn't it?
I think you'd agree that this language is horrible. It might be faster to get to a prototype but I personally can't imagine maintaining a codebase written in this language.
Have you considered that this is exactly what JS is like (in spirit)? It defines so many things that would otherwise not be defined in other languages. For example, what does this even mean? Without looking at the spec, tell me what it does?
```
function hello() {
1.0 + "hello"
}console.log(hello())
```
Does this program even makes sense? Does this program makes any more sense than something like
```
"helloworld"();
```
Not even JS would compile this, but why not? Why not simply throw an exception here? Something like "function not found"? Since this clearly works
```
const a = {
"hello": ()=>{console.log("world")}
}a["hello"]()
```
The set of possible program is JS is much, much larger than any other languages. There's so many little tricks that the language provide that makes it so powerful and so much worse.
(In the first case there's of course also the caveat of false positives from the borrow checker)
I also use a lot of these points when I explain why I use NodeJS and avoid typescript for most of my projects.
The thing is though, it really depends on the project.
Most of my projects are web UI heavy with simple commodity CRUD RESTful APIs sprinkled with business logic.
Time to market tends to be the biggest constraint (I live in startup world), so "good enough" performance and a loose type system are perfect for that.
If I was writing SCADA controllers for a nuclear power plant, my choice of technology would be much different.
The sorts of "rah rah" debates we have on HN about languages frequently don't take the specific project characteristics into account.
Like with everything, it boils down to intelligently selecting the right tool for the job.
Totally agree we need to evaluate the trade offs of various paradigms. But, I think the most important aspect to understand is the impact on maintenance as opposed to writing.
I’m not sure what my ratios of time spent on reading vs. writing vs. debugging code are, but I am certain writing is what I do least, which is why I’d like to see more research on total cost of ownership.
Of course there are exceptions, sometimes the potential upside delivered by the software is small and capped and the requirements fixed in advance, and implementation path is well trodden...
Or you could just write it in Java, which has been around since the 1990s. Even Go is memory safe if you don't touch concurrency.
Rust Ownership and Lifetime rules aren't really that hard to learn. The only trick is that a programmer cannot learn Rust exclusively by trial-and-error (which programmers love to do), trying dozens of syntax combinations to see what works. A programmer is forced to learn Rust by RTFM (which programmers hate to do), in order to understand how Rust works. That's all there's to it. It's like trying to learn to play the guitar without reading a book on guitar cords, pulling guitar strings individually at random - the music just won't happen, even if you're Mozart.
Being a member of a super-elitist cabal that knows how to use Rust "properly" is not some point of pride, it's in fact a mortal danger for the language, just look at Haskell.
And this is why the world has descended into Javascript hell, everybody and their mother are coding "apps" and "microservices" where each click is accompanied by a stack dump because the code is written by a student doing Elance in their spare time, but hey, it's good and fast enough to ship so you get to be first on the market and 3 years later your startup is bought by my bank and I can't login on my internet banking account without seeing that same revolting stack dump that couldn't be produced by a modern and flexible strong typed language. Real world doesn't care about abstract code purity metrics.
I'm not sure I follow you, since I am saying something exactly opposite: anyone, even a mediocre, 1-year-of-experience programmer can successfully learn Rust, if only he or she tries to read a book on Rust, instead of just randomly hacking at Rust code. No "super-elitist" brain required, just a switch of learning methodology.
> just look at Haskell
The problem with Haskell is that it requires a monumental, conceptual switch from imperative programming to functional programming, which has not happened anywhere yet outside of academia (well, perhaps with the exception of functional reactive UI programming, e.g., React Native, Swift Combine, etc.), so Haskell is a tough sell in "real world", commercial organizations. Rust does not have this problem, since Rust does not rely on any "code [structure] metrics" (did I understand this right?), but instead moves as much bug detection as possible from runtime to compile-time (especially for multi-threaded applications).
Rust's ownership rules are simply an explicit form of what people already have to (and sometimes fail to) look out for in C/C++. If learning a language for a couple months makes one feel elitist that's not the language's fault. Though granted, some Rust people are...very eager at talking about its virtues.
Just a reminder of the Haskell motto: "Avoid success at all costs."
So in their case, it's working as intended.
I see this argument in many forms all the time, in a nutshell: monetary gain should be the sole metric of success for engineering. Considering we are killing the planet and perma-exploiting entire classes of society following that logic, maybe we should change approach?
But yeah ship it, pocket it. Why be idealistic if I just need to make enough money to retire and say "got mine, f you"?
Competing with unethical actors is already hard, playing the game on "super-hard" just to feel good about your ethics is actually setting yourself up for failure and betraying those ethics. Been there, done that for a decade or so, have nothing to point to as the net positive effect of my impeccable ethics, but I most certainly do lack the financial and political capital that would afford me any real impact.
So by all means, ship it, make a trillion bucks, then educate and feed the poor and invest all your cash into eco technologies.
In a very extremist fashion, of course you can define a moral standard high enough that we will always fail. But the opportunity for you to think about such absolute moral issues - as opposed to literally slaving in a coalmine or birthing you 14th child and dying before the age of 45 - was created by moral pragmatists who were "effective altruists"[1] of their times , even if the only thing they did was to seek profits by inventing some type of better machine.
We're at a cross road now where the existing mode of wealth production, focused on growth and individual material satisfaction, has reached the point of diminishing returns and even negative returns if we consider our fate as a species in the natural environment. But denying our ability for incremental improvement, forgoing our world building tools and returning to some elusive harmonious state of nature can't be the answer.
[1] though I dislike that term due to its abuse by virtue signaling sociopaths
I get it, modern life for the average western citizen is quite amazing. Using that as an argument for 'thus the western model of exploitation is superior' is not warranted at all.
I disagree. We could have wrecked the natural environment even if we had a much inequitable and oppressive society, think Nazi Germany winning the second world war and entering a nuclear race with the US. There is no single "western model of exploitation", there is a whole family of them (in an alternate history sense), all capable of destroying the planet, but with massive moral differences, and your framing loses that nuance.
This nuance matters because your implication is that the environmental costs are a tradeoff for this prosperity, where in reality they are just a direct result of a runaway species with technology. And our proven ability to control this process towards moral goals, such as material welfare for the masses, suggest to me that we can, in principle, limit and reverse the environmental damage, as long as we recognize it as a worthy moral goal.
What’s hard is not to learn the rules themselves, but to build things under the constraints of these rules. That turns out to be very different, because even though the rules are simple, they have knock-on cumulative downstream effects on the process of making software. My guess is it doesn’t fit well with how most people think, and especially the order in which a problem is broken down (such as front-loading ownership decisions).
Good point - I have to admit that, for example, the single-mutable-reference rule in Rust did force me to keep restructuring my code differently than I used to in other PLs and I did struggle with it to an extent.
It doesn't automatically mean, however, that Rust is (literally) "harder to learn than other PLs". It is possibly the question of whether "a programming concept is hard to learn since it cannot be painlessly brought over from previous experience" or "a programming concept is hard to learn since it is hard to understand in itself".
The amount of problems that Rust moves from the “runtime and unit test” domain to compile time errors is unprecedented in mainstream languages, and also that those errors are binary pass/fail showstoppers. Empirically, I’ve found that programmers coming from C/C++ are much more happy about that than other backgrounds, presumably because they are aware of the pains of having those issues appear at runtime. However, it’s also important to understand that there are a huge amount of use cases where those problems can be solved “good enough” with GCs and other runtime safeguards, which then can give other benefits that outweighs the downsides.
I learn by imitating, like a child. I got my first guitar when I was a kid as a present (no internet, no money to study or to buy books). I learnt by imitating the music I liked. Later as an adult I got technical and I closed the circle with guitar and music theory and advanced techniques.
This is the way I got into C and later C++ more than 25 years ago. Again, without internet, or with limited access. I understand ownership and lifetimes, but when I open a Rust project I cannot understand what the hell is going on.
For new languages I want to learn, I read other people's code and try to do some modifications to check what changed, or add a small new feature. And Rust is so hard on the syntax and the way it wants things to be done. The worst part (for what learning regards) is that Rust doesn't forgive.
So I started a new project. I had no problem with ownership and lifetimes, but later I realize that returning "Result<Self, Error>" and the "?" operator are no so magical after all. And "Error" sometimes is a struct defined in "io", sometimes it thinks is a trait, sometimes is defined in some other error-handling crate. Then you have to learn to map errors, boxing them, dyn dispatching, thinking about what is the overhead of all that, etc. And all that is just for returning something meaningful from a function.
So it's not all "ownership and lifetimes".
I definitely see where you are coming from when it comes to PL syntax density and PL surface area in general, Rust or no Rust. I will probably surprise you when I admit that even though I have been coding C++ for decades and I learned Rust without any major problems, I lost my small personal war to become comfortable with the modern, newcomer-friendly, memory-managed... Swift. Yes, that great-for-your-first-PL Swift. I started out as a Swift aficionado, but with time I started hating the language exactly due to the fact that "I [opened] a [Swift] project [and] I [could not] understand what the hell [was] going on". The language was just too bloated and idiom-based. I gave Rust a shot and I learned Rust much more effortlessly, perhaps due to Rust's "consistency" and "predictability". My point: it might be that any modern PL can get pretty dense and broad due to the necessity of handling imperative, functional, generics, async, multi-threaded, a capable standard library, etc., while it might be just a myth that Rust is "harder to learn" than other modern PLs. For me personally, Rust "ownership and lifetimes" turned out to be easier to learn than Swift "feature set", but it might be just me.
> The worst part (for what learning regards) is that Rust doesn't forgive.
Yes, but this is the whole point of Rust! Rust tries very hard to shift bug detection from runtime to compile-time and the only way it can do that is by strictly enforcing a set of sometimes complex rules at compile time. However, it is common occurrence that it takes tenfold more time to find a bug in a piece of code (especially bugs related to unexpected memory mutation or multi-threaded memory access) than to write that piece of code in the first place. So, wrestling the Rust compiler is "a calculated cost" designed to be worth it in the end.
Thousands of self-taught musicians, many of them famous, beg to differ. Not that this is relevant to the discussion but the example seems flawed.
Ownership is not that hard. Borrowing is a bit harder but not that much more so.
I use Rust because of the ownership and borrowing systems, not despite them. They help me design systems and write code which has very few bugs once it compiles. (The last code I had with bugs in it I wrote maybe 2 months ago.)
I hope some useful pedagogical work comes out of the whole project, but I'm really doubtful of the premise.
I guess this is a bit like Typescript then. Especially when it comes to generics, sometimes I need to sit down and think carefully about what datatype exactly is it that this method requires compared to what I have and how do I express it in a generic way.
Sometimes it ends up taking some time but the result is invariably that the code ends up easier to read, understand and modify.
I was shocked at how many memory corruption / uninitialized data errors I was hitting in C++, for what (I thought) was fairly nice, modern-ish code. This is for code that builds cleanly with -Wall -Werror, with exclusive use of .at() instead of unchecked vector/map access, etc. There are just far too many ways to end up with uninitialized data, and the ecosystem (think serialization libraries) don't really help you there.
I didn't realize how much of a cognitive burden Rust was relieving me of. Sure, the borrow checker is a pain. But I have confidence that when my code builds, it will be entirely free of certain classes of bugs. And other classes of bugs are well-managed by Rust's compiler warnings and Clippy lints.
There seems to be a sort of sleight of hand in programming communities where when Rust is hard, we blame it on the language, but when C++ is hard, we blame it on the programmer. I didn't see the article do a good job of teasing this out.
I don't think there's anything sinister here, just the classic difference between old and new. Old things are familiar, and their downsides are facts of life. New things are threatening, and their downsides get a lot of scrutiny. I'm not even complaining; it's a pretty reasonable heuristic most of the time.
Strong (static) type systems fundamentally make you do more work up front. They force you to be more explicit. They restrict what you can do. They're in your face, because you can't get your program to compile without them. No one gets through any significant amount of Rust programming without knowing that the borrow checker is there. It's so fundamental that it's just part of the experience.
C++ arguably has just as many rules for writing "correct" programs, but they're not enforced by a compiler. So much stuff happens by convention. If you follow the convention, great, your code is safe. If you mess up, no one catches you. Because it's your responsibility, it's easy to attribute this failure to your programming skill rather than to the language that made that mistake possible in the first place.
I'm not saying stronger type systems are always better, but I do think there is a fundamental human tendency to attribute the up-front as hard and the latent as easy, even if the up-front work ultimately helps you avoid the latent work down the line.
It might get easier without all that but is that even possible? The concept of borrowing at it's core isn't that hard to grasp: you taking ownership of something means that somebody else doesn't have it until you release it back to them. Easy. But we seem to have a sample of 1 when it comes to rust style ownership. At least, I'm not aware of other languages doing this. It seems to have lots of advantages. Which raises the question why this obviously good idea is not being replicated by others.
With that in mind, you don't have to remove the model, you just need defaults that can get you past that 1 hour mark, and people will stick around long enough to put in the time and effort to figuring it out.
Most simple problems don't need shared state in order to work, so a language that makes assumptions, such as no sharing by default, could kick that can down the road for a bit. A lot of recursive and some iterative examples only ever accumulate state on the stack. You don't need shared state until you interact with an external system, such as another computer or a human.
Pure functions should be able to share objects. The problem I suspect is working out the calculus of transitioning from stateful into stateless code, but the plus side is that you end up with a programming language that enforces functional core, imperative shell.
You can not say you can write a bubble sort in C and you know C, and that’s indeed relatively easy. But you can not actually use it anywhere.
Again a good C programmer will be a better rust programmer if he wants to. But sometime the luxury of just work with people with the same technical calibers is a sufficient reason to stay.
I forgot who said this but something like “if reading code is ‘debugging’ then writing code is ‘bugging’”
Mental fog translates to logical bugs that either the compiler or human reviewer never catches.
As a result, the program becomes buggy in more subtle ways than before.
To claim that overall, rust reduces the number of bugs during the lifetime of the program, we need experimental data, which is still missing.
> There are almost certainly many other large and small, obvious and subtle reasons for the difficulty of learning and using Rust’s Ownership type system, but these three (Rust’s different paradigm, unhelpful error messages, and the incompleteness of the borrow-checker) are the most apparent from the works surveyed here.
Is Ownership difficult to use? “Learning Rust Ownership is like navigating a maze where the walls are made of asbestos and frustration, and the maze has no exit, and every time you hit a dead end you get an aneurysm and die.” — Student participant from [Coblenz et al. 2021]
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
No direct Rust SBCL comparisons, but generally Rust and C++ perform similarly.
There's some discussion on the limitations of SBCL's GC in this thread:
https://old.reddit.com/r/Common_Lisp/comments/j8z1j7/how_doe...
I haven’t looked at the tests themselves so I can’t speak to their application.
But the range of performance for dynamic languages seems to be enormous. Much bigger than the one of static languages.
Well, no, it's SBCL. Common Lisp has support for types, but most compilers only use them for optimization, SBCL goes one step further and emits warnings when you mismatch types. And looking at the code, I can see lots of type declarations.
It's also interesting to note that the code does not seem to be using SBCL's new SIMD library*, so it could be sped up even more.
* <https://github.com/marcoheisig/sb-simd>, see the texinfo file for documentation.
Especially for values that cross the boundaries of compilation units, the compiler in another language cannot possible have enough information to know if aliasing could occur.
(1) mutable references can't be aliased, and
(2) referential transparency: data behind an immutable pointer never changing no matter how many aliases to it there are.
(The exception being UnsafeCell, of course, which can easily be detected.)
Exciting times ahead for Rust performance!
That is where garbage collected languages seem to be incurring penalties. Not sure what SBCL does specifically. There was this talk last October about efforts at Jane Street to make explicit stack allocations possible in OCaml:
byte_array1.iter().position(|&x| x == digit)Not by what it does. But by the fact that apparently every operation uses it, but most just throw away the result.
Keep in mind that in swift virtually everything is Arc-ed, so it puts heavy optimization on it like merging several counter updates within a scope into single atomic operation.
In my opinion such a performance cost is absolutely worth it in most applications given the much reduced complexity when writing code.