That is a super mature post whose content should be read by any language and/or eco-system component (libraries, frameworks and so on) creators and maintainers.
I love the intentionally limited scope and the amount of thought that went into writing this, and I wished I had the clarity required to be able to do this. Especially the bit about 'negative space' got my intention, that's one tool I'm going to put in my toolbox to re-use. And there already is one example from a company whose founder I knew that made serious bank on just knowing what they were not, so this advice has applicability far outside of computer programming language design.
> Especially the bit about 'negative space' got my intention, that's one tool I'm going to put in my toolbox to re-use.
One of the most profoundly useful things I've ever realized in my life is that the most important choices are often about deciding what not to do. Creativity is deeply wedded to the idea of constraints and pragmatically, there are only so many hours in the day. Deliberately deciding to not investing in one thing is the clearest way I know to free up mental capacity for the things I do want to put care into.
I see the value of deciding what not to do for version(s) X of something, but ruling something out for all eternity seems a little... short-sighted? Who's to say that what you're ruling out won't become the next big concern for everyone and your technology won't be the odd one out without it?
There are two possible ways to deal with that situation: create something new that is the old thing plus the feature that had been ruled out for it, or simply revoke that decision. Eternity can be surprisingly short when everybody agrees that it is time to move on.
Both ways are perfectly possible, and both would in many cases be much preferable to having an implicit "is it time yet for X?" on the agenda every time the version number is incremented. "Not in the foreseeable future" is effectively understood as a far out roadmap item, no matter how much you don't mean it that way.
I'd rather see a humble "we thought that we would never X, but we were wrong" repeated for many different X than a single misleading "we absolutely commit to not be adding GOTO before 2025".
> Eternity can be surprisingly short when everybody agrees that it is time to move on.
That relies on having a decision-making process and culture that accommodates that. In my current job I'm stuck dealing with inadequate tools because of tool choices made in 2002 that no-one has the political capital to revisit.
In all likelihood an entirely new product will be a better fit in that case, anyway. Especially if whichever direction you do pick completely precludes what you decide not to do.
In practice (and with some experience) most decisions do not prevent revisiting them later.
Came here to comment on C. IMHO it remains a gem in the space of languages, probably because it remains small and simple. It has many warts, but it does not suffer in the way C++ does.
People claim Rust is a better C++ and as from my outsider point of view it looks like it is. One of my fears is that it will follow C++ down the very road the author is concerned about.
I can't agree more with this. I am a long time c++ dev, been to c++ committee meetings. The language is too complicated and inconsistent, and that is now I believe unfixable. I believe no one understands it. New features keep arriving, but you still have to learn everything that came before, for older codebases.
For example, {} style initalisers were added to simplify and "unify" things. Except to make a vector of length 3 you still need to use the old style (3) notation. So now there is just one more thing to learn.
It is much easier to add "one more thing" to a language than it is to later take it out.
I've been dabbling in some Rust lately and as a long time mostly-C++-and-Python-and-some-Java dev I've found it relatively easy to get started with it. Sure, a lot of things are different (e.g. struct+impl vs. classes), many clearly better, some that may/may not be better just different, but overall it seems to me like Rust is relatively easy to pick up coming from C++.
And C++ is usually way uglier and far more complicated. I'm currently reviewing a bunch of code using bleeding edge language features and it is extremely difficult to understand what the code is supposed to do and what it might be doing.
The age of multi-vendored languages like C or C++ for systems programming might be coming to an end. I'd wager this is actually a good thing.
I was drawn to Rust because, behind the apparent complexity of the borrow checker, the language is/was actually very simple and easy to understand.
I have been using Rust extensively for over 2 years.
In those two years, the language complexity has increased substantially. It's already at a stage where I'm worried about losing track.
Also, there is quite a large amount of changes in the pipeline that were already accepted but are either not finished or not implemented yet.
All of those can/will have a significant impact on idiomatic code and API design and will increase the complexity burden:
* specialization
* generic associated types (a simple form of higher kinded types)
* async/await + generators
* existential types
* const generics
...
So I do believe Rust needs to slow down considerably and get much stricter with accepting new features. Both to not overwhelm the existing userbase and to not make the language another C++ in terms of complexity.
One thing that’s come out of this years’ posts by various people is that, after those features have landed, people generally do want things to be done. These features shore up major weaknesses in the language for various domains, but there’s not really things not in that list that many people still extremely desire.
History has proven that system programming language winners come with an OS/platform to cimment their advantage over others.
So I don't agree that the age of "The age of multi-vendored languages like C or C++ for systems programming might be coming to an end.", unless you mean we will managed to get rid of all OSes written in them, specially UNIX flavours or GPGPU libraries.
It would be nice, but it won't happen until the next big hardware revolution like Quantic computers being developed in Q# for example.
Used over in HPC here and their cluster version (which bundles Intel MPI) is several thousand dollars. Aren’t it’s tools also the core of Clear Linux? Or does that default to GCC/Clang?
Interesting, what do you think makes is encumbered by LLVM?
I thought that sqeezing MIR in the middle Rust could get both Rust-specific transformations and then benefits from the LLVM generic code optimizations...
LLVM is fantastic, but it's also a huge C++ component, and it's not necessarily optimized for speed. It would be nice to end up with something in Rust to simplify the toolchain needed, and to maybe have something that could be even faster for debug builds.
Maybe it makes sense to read the success of Golang as a smart piece of social engineering. Start with a simple and accessible language and gradually ratchet up the complexity to deal with more real-world problems. You may eventually wind up with a worse language but at least you'll have users. Rust tried to solve a lot of hard problems out of the gate and that has slowed its growth IMO.
Go is still a very simple language. It's not just social engineering. Compare it to Python, which is another "simple" language that you can be dangerous in on day one.
As you gain experience in Go, you learn idiomatic ways to do things and memorize the core library, but the fundamental mechanics don't change.
As you gain experience in Python, you develop a preference for virtualenvwrapper and experiment with 300 different ways to build a distributable package, then start extending __getitem__ and __setitem__ and adding decorators to everything and before you realize what you've done, you've torn apart the laws of physics and your code has nightmarish side effects lurking around every corner.
> As you gain experience in Python, you develop a preference for virtualenvwrapper and experiment with 300 different ways to build a distributable package, then start extending __getitem__ and __setitem__ and adding decorators to everything and before you realize what you've done, you've torn apart the laws of physics and your code has nightmarish side effects lurking around every corner.
Alternatively, you glimpse the chthonic horrors lurking at the end of that path, purge your code of hidden mutable state, and use Python to write straightforward procedural programs.
That social engineering strategy only helps in the short term (short on the language scale, which can still be many years). If you get a convoluted language in result, in the long term it will increasingly irritate programmers, who will anyway start looking for objectively better alternatives. So Rust took the right approach IMHO.
What tends to happen is that you bring out a simplified, cleaned-up version of the popular language of the day. People then flock to that new language and start making feature requests. Over time that language becomes as complex and inelegant as the language it replaced and you can start the whole cycle all over again.
The move from C++ -> Java -> Go is a perfect example of this cycle.
C was a language designed by two developers without background in language design that got lucky, because Bell Labs wasn't allowed to sell UNIX thus it ended up licensing it for a symbolic price to universities.
C was hardly efficient in the 80s micro-computers, and outside Bell Labs people were doing compiler optimization research in languages like PL.8 and similar.
Had Bell Labs been allowed to sell UNIX and history would certainly looked much different.
When talking about "efficient communication" between the computer and the programmer, it does not necessarily imply the characteristics of the runtime performance.
For instance, Fortran is very performant in numerical computing, even outperforming C, but it has difficulty in accessing I/O mapped registers or implementing an interrupt handler, a jump table, or just cleaning a particular chunk of memory addresses.
Another urban legend from C yearly days. There was plenty of research in system languages since the early 60's outside Bell Labs, with OSes being written in Algol, Pascal and PL/I variants.
"Oh, it was quite a while ago. I kind of stopped when C came out. That was a big blow. We were making so much good progress on optimizations and transformations. We were getting rid of just one nice problem after another. When C came out, at one of the SIGPLAN compiler conferences, there was a debate between Steve Johnson from Bell Labs, who was supporting C, and one of our people, Bill Harrison, who was working on a project that I had at that time supporting automatic optimization...The nubbin of the debate was Steve's defense of not having to build optimizers anymore because the programmer would take care of it. That it was really a programmer's issue....
Seibel: Do you think C is a reasonable language if they had restricted its use to operating-system kernels?
Allen: Oh, yeah. That would have been fine. And, in fact, you need to have something like that, something where experts can really fine-tune without big bottlenecks because those are key problems to solve. By 1960, we had a long list of amazing languages: Lisp, APL, Fortran, COBOL, Algol 60. These are higher-level than C. We have seriously regressed, since C developed. C has destroyed our ability to advance the state of the art in automatic optimization, automatic parallelization, automatic mapping of a high-level language to the machine. This is one of the reasons compilers are ... basically not taught much anymore in the colleges and universities."
-- Fran Allen interview, Excerpted from: Peter Seibel. Coders at Work: Reflections on the Craft of Programming
I don’t think Java and Go will ever, combined, reach the complexity of C++.
The reason why older languages are complex and inelegant is because our understanding of how to create simple yet useful languages has increased, not because older languages accumulate much complexity.
Wanting to do what the new languages can do without breaking backwards compatibility is a big reason why C++ got so complicated. C++98 isn't all that complex, but if you want to learn C++20 you still have to learn every language feature that was developed in the last thirty years and how they all interact.
I would disagree with the claim that “C++98 isn’t all that complex.” It was so complex that any project I found using it would restrict itself to some arbitrary subset, just to keep complexity under control. Java has gone through years of backwards-compatible changes, and is still wonderfully simple compared to early versions of C++. C++ started with a mess of different features and hasn’t really gotten worse. If anything, C++11 is simpler than C++98 from a user’s perspective, even though it’s not from an implementer’s perspective.
One of the big differences is that Java and Go have straightforward context-free grammars, mostly, and you can whip up a working parser in no time. C++ is a bit of a beast, by comparison (hence keywords like “typename”).
C++ also has the complex overloads and template system. I think people underestimate how complex these things are when they are learning C++, and how complex their interactions are. Then there’s the preprocessor.
You can kind of argue that these are just an accumulation of changes, but other languages contemporary with C and C++ do not suffer from these complexities, so the argument falls flat. By comparison, Go and Java rely on reflection or code generation more, and these are a bit simpler.
> I think people underestimate how complex these things are when they are learning C++, and how complex their interactions are. Then there’s the preprocessor.
Just like wanting to learn Python 3.7 means learning about 500 PEPs and how they interact across each Python release (major.minor, during the last 25 years and alternatives to CPython.
Well Python 3 broke compatibility so you don't have to start at Python 0.1 alpha, but yes, completely understanding Python is also no easy task. I write Python almost every day and have taught courses using it, but I know of features that I don't understand. There are probably more that I'm not even aware of.
Sunk cost or costly effort? There are a more than a handful of large of technologies, frameworks and libraries used in my industry which would have to be ported/rewritten and supported from C++ before anyone would think twice before using an alternate language. Particularly when speed is the name of the game (in terms of integration, not just compiled binaries). There are hundreds of C++ libraries employed daily that would be non-trivial to port.
This is what Guy Steele's Growing a Language talk was all about: "I should not design a small language, and I should not design a large one. I need to design a language that can grow. I need to plan ways in which it might grow—but I need, too, to leave some choices so that other persons can make those choices at a later time."
It's been a while since I watched this but my current thoughts on "Growing a language" is that I fear it is an unhelpful simplification of what makes language design hard. IMHO language design is a holistic design problem that needs global optimisation. You cannot incrementally hill-climb yourself anywhere useful when you can't backtrack over past decisions.
It is like designing a workshop. You only have so much space within arms reach. Here you place your most frequently used and valuable tools. You can "grow your language" by adding tools but they can't replace what you have in this limited and privileged position - instead, new tools have to go in cupboards or on another table. The new tools will have higher friction than the first priority tools you added.
Maybe you can have a workshop where you imagine a tool and it appears in your hand thereby removing the constraint of "tools within reach" but I think this then makes it too ephemeral and abstract. It is the constraints that makes a language tactile and ergonomic - remove the constraints and you have no structure at all.
In this case, the "tools within reach" are your core keywords and syntax. Growing features via libraries or syntactic extensions generally incur more ceremony and less elegance. Having totally flexible syntax extensions/keywords doesn't solve the problem, it just moves up a level abstraction/generality and means you have given your users the "design a language" problem instead of solving it for them.
> you have given your users the "design a language" problem instead of solving it for them
This is an interesting observation that reminds me of
> In Lisp, you don't just write your program down toward the language, you also build the language up toward your program.
from Paul Graham's "Programming Bottom-Up". Some people would consider it an advantage being allowed to fold the "design a language problem" into the overall problem.
More, I think this is just a matter of degree, not kind.
As soon as you have something as apparently simple as named procedures, you're really writing a DSL, albeit very coarsely, for your business problem. Add named record types. Add textual macros. Add operator overloading. Add Lisp-style macros. At every point where the language allows a word on the screen to stand in for something larger, you're giving the programmer the power to design a language for their problem domain.
Guy's point was that the language should be designed to be grown grown by users, not by language designers.
> We need to put tools for language growth in the hands of the users.
Guy's previos work with Common Lisp is epitome of this. There are three kind of macros in the language: reader macros, macros and compiler macros. They give the tools for the user to extend the language. There is just 25 or so core primitives in the 'core' language and the runtime. The rest 900+ functions and symbols are basically the standard library (the fact that they are slapped into the same package and they extend the core in a ways that other languages can't hides the simplicity of the language somewhat).
I think this is a really appealing idea but so far nobody’s managed to build a successful language this way. I’d love to see a mainstream language designed on these principles myself but I think it’s not going to be an s-expression based language.
Whether it’s mainstream is debatable, but I think Elixir is a really successful example of extensible language, and indeed it managed to do this without s-expressions. Various libraries extend the language for HTTP routing, parsing, DB queries, property testing, static analysis, etc. It makes possible a lot of experimentation by the wider community, and not just the group of core language developers.
The creator has repeated this philosophy a few times:
> There is very little reason for an Elixir v2.0 with breaking changes. The language was designed to be extensible and if we need to do major changes to the language to improve the language itself, then we failed at the foundation.
> A big language does not only fragment the community and makes it harder to learn but it is also harder to maintain. It is also why the language was designed to be extensible: so the community could build what is necessary without a push to make everything part of the language.
> We also understand there is a limited amount of features we can add to the language before making the journey too long or the language too big. Adding something now means not including something else later. As an exercise, let’s see a counter example of when we didn’t add something to the language: GenStage. [...]
I dont think that by looking only to features we can understand this very well. Go was very lucky to be launched when cloud computing fever was catching up, and people were being burned for using techs like Java for this end.
C++ were very performant, and unlike Java not a memory hog, but for this sort of tasks it was deemed to complex. Besides the ammount of people that were able to program in it was limited (and you can even see this reasoning being used as a reason why they created Go).
Then Go showed up with simplicity and a good story in performance and memory, deploying simple binaries in a world of complex server infrastructure paved out by Java was a very handy and right on time approach.
The problem with Rust now, is that it is target the system programming, in a era of system programming renaissance given the whole mobile app scenery.
The problem is; there is already a lot of C++, C, Java, Objective C and Swift code there, so it will be hard to have a good reason to rewrite complex stuff with a lot of man hours in it in Rust.
Every language needs a platform. C is here because of Unix, C++ because of Unix, games and early 2000 and 90's era startups. Java and C# because of bussiness software, Go because of the cloud, Ruby and PHP; because of webservers, Rails an blogs, Python for educational purposes, Data Science, ML and now AI.
Now what about Rust? Its trying to eat some lunch from C and C++. But a lot of code and value is already there in C and C++, where rewriting it in Rust just for some ocasional added value here and there is not reasonable.
Its really tough, and if Rust dont find a platform to grow it will be very hard to advance any further. But, there's always new tech waves and tools that will marry perfectly with them.
Lets see which languages are able to be a perfect fit for them in years to come. But right now Rust will depend a lot of the community it already formed to keep sharp and maybe be lucky to surf one of those waves.
> Now what about Rust? Its trying to eat some lunch from C and C++. But a lot of code and value is already there in C and C++, where rewriting it in Rust just for some ocasional added value here and there is not reasonable.
For existing software it certainly is less about rewriting it but extending it in Rust.
Thanks to not having a runtime and being quite easy to create a shared library with a C-ABI, pretty much any software written in any language could be extended with code written in Rust.
> Thanks to not having a runtime and being quite easy to create a shared library with a C-ABI, pretty much any software written in any language could be extended with code written in Rust.
Ok, but you kind of added complexity to your codebase. Now you will need not only C++ coders, but also Rust coders. If you are Mozilla or Google you can do that, you eat complexity for breakfast.. But they are probably not the companies you need right now to keep your steady growth, unless of course you are lucky that they created some killer app in Rust because of their in-house use of it. Like Google did recently with Kubernetes, only it is in Go of course, but i guess it checks the square of a killer app which helps into the language adoption as a programming trend.
Speaking of Go, it once was suffering from the same problem Rust is going through right now. It was sucessful in a first phase because of its community, but it needed more to start having more adoption and mind share. Then, Docker happened, and Go found a sweet spot to aim for and take it to the second base.
It's a good point that a language needs a platform to really shine. I think one place where Rust can find it is by targeting WASM and being the first systems language that can compile to code that runs in the browser. The rustc compiler supports WASM out of the box and I think this is one direction that the language adoption is going to grow.
Another platform is cryptocurrencies/blockchains- Rust lends itself uniquely as a modern C++ for writing performant blockchain protocols. This is evidenced by the Parity team using Rust exclusively for all their projects. As that world evolves Rust can find a foothold there as well - there is no real legacy code floating around, the Bitcoin protocol is only 10 years old.
These are definitely a couple of good bets to be aiming for.
The blockchain trend reminds me that Rust could also try to find some cosy home in banking software, in expert systems and in embedded software.
The key now is to be humble and not to try to pick up a fight with C and C++ just yet.
Go only now is in a position to pick a fight against Java in the server and in the cloud, and Rust will probably need to get into the same point of adoption, if it wants to be seen as a serious contender to those languages in the eyes of the people they need to convince.
Unfortunatelly Rust missed a good new trend now, with multi-platform cores for mobile phones that ended up being written mostly in C++.
Rust wasnt even invented when people were trying to solve the problem of multi-os phone apps.
And that is the current trend which is giving the system development trend a second chance. But is not that bad, because thats exactly what is making people to take a serious look at Rust.
Battery life, memory, storage. It all matters again thanks to the phone environments, and you cant afford the be wrapped in layers upon layers of software virtualization like it was common in the early PC era, where Java and C# were kings.
No offense, but that's been the tune we've been constantly hearing for years now. Always the "next big thing" killing off C++ or Java or something with its superiority. And yet, somehow, when choosing a very efficient language that has 1st party support, we always end up with pretty much one widely supported answer: the horror that is C++.
Why do you think that Rust will be a different story this time?
Most of us aren't concerned with whether anything would kill off C++. That certainly won't happen any time soon, given the inertia. But some of us would like to be able to use a language that lets you do low level, high performing goodness, without having to use C++. So it is nice that there are some more viable options now.
But it's share of total usage can and will decrease. Java certainly took a massive chunk of the "programming market" that would have otherwise been C++. And then later JavaScript. Of course, software usage in general has been going up and up, so in absolute terms C++ has been increasing as well.
I feel like C is having a renaissance because of that.
C is very conservative in adding new features, which makes it easy to get an understanding of the language - nothing is hidden, when necessary you can derive everything you need to know about a piece of code from first principles.
C compared to C++ feels like high school math compared to university math with all it's scary notations.
I don't think this is true: what C leaves out in terms of language features , programmers have to reinvent or keep in their heads perpetually while writing code, leading to tons of gotchas.
Yep, to reuse the math analogy. C is like solving 3d geometric problems by manually aggregating endless lines of cos, sin and sqrt, instead of using vector mathematics where you can do the same with just a single line of matrix multiplication or a simple dot-product.
In the other words, C standard library is not really small. It consists of a small mechanized portion and a much larger portion embedded in wetware, i.e. human mind. The second portion is very hard to update or fix, so the first portion keeps growing to cover all faults of the second.
It's not just the notations, either. I'm not a particularly strong C programmer (I don't write it professionally, I write Python) but I've read documentation for the entire C standard library[1], for example.
Certainly not in the C compilers that have been ported to C++, the OSes that now push for C++ instead of C, automative standards that replaced C by C++14 as certified language, IoT platforms based on C++,...
I don't feel like C was ever meant to be the permanent foundation on which all other computing was built. It was a language for its time. It took assemblies of the 70s made them much more palatable with an imperative syntax. C++ was also not meant to be a permanent foundation. It took shortcomings in C for scale and hacked on top functionality Bjarne wanted.
The complexity difference between a computer 46 years ago, almost half a century, when C was first developed and now is incomprehensible. It is reflected in how these languages, fundamentally, are willing to just say "since the computer only runs in 30 cycles a second and we are just doing increment operations if you break the rules it undefined" and the idea of having behavior native to the grammar that would impose kilobytes of size complexity or megacycles of computation would have been absurd.
At the time, of course. Modern C++ doesn't hesitate to generate multiple vtables for a complex class while juggling reference counting smart pointers with streamlined initialization grammars that can take up thousands of words of memory. But because the primitives that must remain stable for backwards compatiblity were built on the most constrained architectures the whole house of cards is thus constrained.
I think Rust has already made a lot of "just get it out there, don't think about the ramifications" decisions in its language design, especially around its grammar. On one hand there is still logic to what glyphs do what and how control flow is laid out - particularly concerning minimizing the number of retraced parses the source needs to go through - but it doesn't change the syntax of lifetimes ('), the use of double colon namespacing (::), or the turbofish but being an unintitive disgusting hack.
In my experience the RFC process though has helped temper that. All those blemishes are predominantly from the origin of the language up through ~2014. Once the ecosystem developed and decisions required months or years of hanging around on the issue tracker to see stabilization Rust mostly stopped grafting on arcane behavior and started reusing its syntax in intuitive ways (like impl trait).
I don't think any of the current crop of outstanding wants really makes the language more complex. A lot of it is just increasing the compliers complexity to handle more generic code, be it in terms of kindedness or by type delegation or inference. Those kinds of complexities aren't generally something for users to explicitly learn in rote memorization to use the language - if Rust introduced overloading and default arguments all those who don't know it exists won't have the language made any harder to use, but those that want it can take advantage of it.
As long as Rust is never tempted into the "a substantial portion of this behavior is undefined if not done explicitly as intended" hole that C and C++ were predestined to fall upon I don't think features make the language harder. The core syntax is there, shouldn't change, and is being made easier and more intuitive, not harder and more complex (NLLs save the day, and the number of explicit lifetimes needed have been culled dramatically in the last two years of development). In just a few glyphs of C++: void* = 0 - there are already all manner of edge case, undefined behavior, syntactic complexity beyound comprehension.
Nobody working on Rust today wants arcane, impossible to process grammar or undefined behavior complexity. The language already mostly lacks it. Nobody in their right mind will add it. Thats never a feature, its the bug Rust fixes that is inherent to the C lineage in ways nothing but a rewrite can fix. It took a decade of pain for Python to change its string type from bytes to unicode - there is absolutely no way to salvage C or any of its descendants. By design. From its fundamentals. All the complexity on top has just been repeated attempts to temper the fundamental incongruities.
> if Rust introduced overloading and default arguments all those who don't know it exists won't have the language made any harder to use, but those that want it can take advantage of it.
The following program might no longer compile if `f` could be overloaded, additional type annotations would be required.
fn f(n: u8) -> u8 {
n + 5
}
let o = Some(Default::default());
println!("{:?}", o.map(f));
In general I would expect overloading to make type inference less reliable (not so much with the compiler getting it wrong, but needing more annotations), which users of Rust that do not want overloading would be right to object to.
I don't want to really derail the thread into a discussion on overloading - I was citing it more as an example of a common desire that also introduces a lot of compiler level complexity without really modifying the language syntax - but I imagine any first pass conservative overload implementation would be restrictive enough to let the compile know if theres only one definition of a function. Default would only break if there were 2+ impls of the same signature and would just require a type annotation then.
I'd argue you could not implement overloading without enough support being in place to let the current single signature inference work as it does, which is also probably a good chunk of why its never been really proposed in earnest. Its a hard problem to approach.
I think the fear of unrefined behaviour is starkly exaggerated in everyday C programming.
If you follow the established best practices (and the compiler will warn you if you don't) you will not experience undefined behaviour.
> Those kinds of complexities aren't generally something for users to explicitly learn in rote memorization to use the language - if Rust introduced overloading and default arguments all those who don't know it exists won't have the language made any harder to use, but those that want it can take advantage of it.
This however is only true if you never have to work with someone else's codebase,
When you start creating dialects your usual 'style' will feel alien in another project or you may have trouble understanding how something works due to an arcane language feature you've never had to use.
> If you follow the established best practices (and the compiler will warn you if you don't)
There are a lot of best practices that can be quite hard to follow without language support, and would generate too many false positives to warn about. For instance, use after free. There is no lifetime information available in the type system that the C compiler could use to tell you whether you are doing something that could result in a use-after-free. How do you propose the compiler could catch these, except in the most trivial cases?
Common practice would be to set the pointer to NULL after free and if you have multiple pointers to the same object - well, try to avoid doing that. If you must, you can use reference counting.
I think that's pretty terrible advice for something that affects memory safety, and it invalidates your entire "the compiler will warn you" argument. There's a reason Rust has ownership and lifetime annotations: there are many things that a compiler simply cannot infer.
I haven't looked into Rust very much yet, but doesn't the borrow checker effectively prohibit you from having multiple references to the same object?
I wouldn't say it's terrible advice though. If I see that something I do is potentially dangerous and would require special care to get it right, I'd first look if there is another way to do it.
The borrow checker allows for one mutable reference, or multiple immutable references. There are also primitives for allowing interior mutability through shared references, which enforce single access at a time by other means (mutexes, copying data in and out, etc).
References are always guaranteed to point to something that lives longer than them. What they refer to could either be on the stack, or it can be allocated and deallocated on the heap via smart pointers. Box<T> is similar to C++'s unique_ptr; there is a single owner, and when that owner is dropped, the referenced value is dropped. Arc<T> is similar to C++'s shared_ptr; it's an atomically reference counted pointer, so the value will be deallocated when the last clone is deleted.
These combine in ways that allow you to use most of those "best practice" patterns from C and C++, but with actual language guarantees that you will follow them and the compiler will catch it if you don't. In the internals of data structure implementation, you may need to go beyond what this allows with use of unsafe, but you can limit that just to a few small critical pieces of code that can be more thoroughly audited and tested.
In a large project "try to avoid doing that" doesn't really fly; in a large codebase, with dozens of developers, developed over multiple years, people are going to make mistakes. They will need to do some kind of refactoring, and one of the guarantees will get missed.
If you've done a decent amount of work in a large C or C++ codebase, you might appreciate this description of the process of storing a reference to a slice of a Vec in Rust, without having to manually reason about all of the places in which that slice could be invalidated: https://manishearth.github.io/blog/2015/05/03/where-rust-rea....
For vanilla references (&), Rust will prevent you from having multiple mutable references. Beyond that, there are several types in the stdlib[0] that progressively increase flexibility, but always safely.
I think it's bad advice because you need to know what is potentially dangerous. That alienates newcomers to systems programming (which is why one of Rust's goals is to be able to "hack without fear"). There are also some designs that I'd never try to build without safety checks, because they're notoriously hard to get right. Even knowing the fundamentals of memory safety, without a way of enforcing lifetimes and mutability, writing zero-allocation implementations that share immutably can be daunting.
The neverending stream of exploits based on undefined behavior proves you wrong. There is not a single widely used codebase that is completely free of UB and doesn't come with a formal proof.
I agree. I see comments like that time and time again. As someone who writes it professionally, it just doesn't occur as often as the internet would make you think.
It's not a simple matter of how frequently it occurs, the fundamental question is one of rigor. The principal difference between craftsmanship and engineering is also rigor. Historically, the ability to prove your code was free of classes of error required sacrifices that few practicing software developers could or would accept (e.g., formal proofs, use of "niche" languages, uncomfortable toolchains, etc.).
What's got people excited is the evolution of these systems which have a rich intersection between rigor and usability (to include ecosystem, tooling, etc.). The ability to statically eliminate classes of error with good (or maybe good enough) ergonomics means these questions of "how frequently does this occur" are not relevant - you can drive such occurrences to zero and doing so should become table stakes. This is a proper engineering mindset.
None of this is to dismiss craftsmanship, which definitely has its place. And before the ability to practically apply rigor, that's all there was. After all, people have been building complex systems for centuries without the benefit of modern mathematical foundations. But none of those are good reasons to eschew rigor when it is within reach and when the systems you develop are big/complex/widely-used enough.
You think this until you're on the front line exposed as attack surface directly or indirectly via linkage. This is a knowledge category where ignorance really does give people unfounded confidence.
I'm not sure that's an important comparison. They're not saying that there's no good reason for the current state of C++, or that they can't understand why it's like that. They're just saying that it'd be nice if Rust didn't disintegrate into the same morass of new syntax, primitives, idioms and pitfalls that C++ has become.
True, but many of the "modern C++" stuff like RAII, type safe strings, arrays, collections were already possible in C++ compilers for MS-DOS for example, it was a matter of actually using them.
Not sure which managed languages you mean, those that I use also provide the option to compile to native code just like Object Pascal, if I wish to do so.
There’s a few hundred thousand lines of Rust in Fuchsia. (Garnet’s repo claims 12% Rust as of today.) They’ve been hiring Rust programmers aggressively to add more. They’ve been helping drive the design and implementation of async/await, since they’re big users. Rust is one of the four or five languages that they maintain a sort-of-libc for and use it as an example for writing applications for the OS. (It’s a microkernel, so it’s not really a libc so much as it is their IPC layer, which is how you make the equivalent of syscalls in other languages. “FIDL”)
I don’t think there’s any plans to move Zircon. It’s also less pressing, IMHO, since it is a microkernel. Maybe someday, but if I were in charge, it wouldn’t be super high in my list.
I have written non-trivial code in Go and Rust. My situation is C++ dev -> Go -> Rust -> Go (current day job). The shifts have been due to various circumstances on my day job.
The thing is when I moved back from Rust to Go, I had forgotten how productive Go is and I felt something like this:
Personally, in my heart, I'm kinda keeping fingers crossed for a future where if one needs to/has to, one uses Rust (mostly where one would use C/C++ today, including for "squeezing the last drop of performance", and especially high risk everpresent libraries like libpng; plus as an extra, probably for highly ambitious parallelism like in Servo); otherwise, one just goes with Go (for the amazing general productivity and readability).
Though then there's also Luna-lang, which I'm hoping even stronger to become long-term a new breakthrough paradigm/tech in programming & scripting...
On a mostly unrelated note, but continuing with off the sofa predictions, I'm curious on a few more developments:
- whether WebAsm will become the new de-facto universal architecture — at least for VMs, but I wonder if we won't eventually end up living on purely WebAsm CPUs at some point in future? (Unless WebAsm has some inherently hardware-hostile design decisions; I'm not a CPU/ISA guy to know that.)
- then RISC-V; will it become the ultimate personal computer/device CPU in the meanwhile? Also, what may happen if both RISC-V and WebAsm start naturally competing in this domain eventually?
- Fuchsia off-handedly dethroning the Linux kernel & ecosystem (and maybe even Windows) as the sudden standard mainstream OS (and thus also, amusingly, suddenly swinging the Tannenbaum's argument up through a semi-random caprice of Google's deep pockets)?
Especially per the last point, I wonder if we'll eventually end up in a world with much more... "secure"... personal devices. And if yes, will it end up being net good or bad (or just neutral/hard to say/nuanced, a.k.a. both, as usually) for humanity. Given that I'm recently realizing that "secure" unfortunately seems to be a very close sibling, or even maybe just an other face of, "closed" — think DRM, walled-garden ecosystems, no more rooted Android or XBOX... or even no more control at all over your PC, a.k.a. no general computing for the masses, potentially. Though I personally stay mostly hopeful on this front; in part because I think I believe people are as a sum too chaotic for this to be able to happen completely.
As a newcomer to Rust and perhaps having been spoiled by Go's very readable specification, I was surprised that Rust doesn't have a definitive language spec. How do the people working on Rust ensure everyone has a common understanding of the language without one?
Some stuff is quite nailed down, some stuff is not. In the end, not breaking existing code is the most important thing.
These things are always on a spectrum. Just because a spec exists doesn’t mean that it has holes; Go’s spec isn’t formally proven, for example, so you could make a similar claim: how can people know that it all works without a proof?
The answer is that it’s all a spectrum. Many languages don’t have anything resembling a spec at all!
(We are interested and actively working on such a thing for Rust, but we’re shooting high. It’s gonna take a while.)
I have many times run into issues where a written down language spec would have been a big help. The are very useful features in the language that aren't mentioned anywhere in The Book, discovering them requires hoping someone on twitter mentions it, which is crazy.
I would love to see a feature freeze and focus on writing down a spec and speeding up the compiler.
Some things that are covered are a bit hard to find. For example, I didn't know if operator overloading was covered; it turns out, it's mentioned in https://doc.rust-lang.org/book/ch19-03-advanced-traits.html, but it's not in the outline and there's no table of contents on that page, and it's only actually mentioned as an example of how default generic type parameters work. The topic it is covered well by the `std::ops` docs (https://doc.rust-lang.org/std/ops/index.html), but since it's a language feature as well as a library feature, I'd expect it to be covered by the book as well.
#[path = "path/to/file.rs"] doesn't seem to be mentioned; there are probably a bunch of other obscure attributes like this that aren't mentioned.
repr? I didn't find any mention of #[repr(C)], #[repr(packed)], #[repr(align)] etc. I also didn't find union.
simd? Seems to only really be covered by the 2018 edition guide. Googling "rust simd" turns up references to the old, unstable std::simd.
The first two examples are a couple of things I've recently looked for and couldn't find; the others are ones that I noticed browsing around just now looking for other things that might have been missed.
Now, some of these are documented in the reference, in the nomicon, in the 2018 edition guide, or in the standard library docs. But that's one of the problems; that there's no one, or even two or three, places you can go to find everything you need.
The reference has large warnings at the top about being incomplete, although it's actually not as incomplete as I remember, it might be a good idea to remove the scary warning from it, or to do an audit of it and only put the warning on the sections that are found to be incomplete (like the memory model, which is clearly incomplete as it hasn't actually been formalized yet), but take it off of the sections that actually are up to date.
And Google doesn't always help. It still sometimes gives me first edition book links at the top. When I was trying to look up all of the `#[cfg(target_...)]` forms I could use, I tried to search for "rust cfg target", https://www.google.com/search?q=rust+cfg+target, but that gives me Rust By Example, a first edition link, and a link to a placeholder that refers to both the first edition and the reference. It took a lot of clicking through to actually find the reference page on it.
Thanks! So I think that some of this difference is that you're mostly describing library features, not language features. It's also true that some of these examples blur the lines. I'm going to make some notes.
> it might be a good idea to remove the scary warning from it, or to do an audit of it and only put the warning on the sections that are found to be incomplete
Completeness is hard, as you've seen with the book! This is what we'd like to do, but it's non-trivial.
I'd say that most of these are language features. The only that are mostly library features are operator overloading and simd, but they both have associated language features supporting them.
I agree that completeness is hard, but I feel like it could be a bit easier if there were some consolidation of the material.
At the very least, make sure that everything covered in the 2018 edition guide is also covered in the Book, in at least a similar level of detail. I'd also make sure that everything covered in the Reference is covered in the Book, or the Book provides at least an introduction to the topic and then a reference to the more detailed information in the Reference.
> I'd also make sure that everything covered in the Reference is covered in the Book,
They have very different goals, so this will never happen. The Book's job is to teach you how to program in Rust. The referece's goal is to (eventually) be a full specification. The former must be a sub-set of the latter.
I do agree that everything should be documented, but the book cannot cover every last possible thing.
Yes, that's why I included the second part, "or the Book provides at least an introduction to the topic and then a reference to the more detailed information in the Reference."
For example, I don't think that every last detail of #[repr(...)] needs to be covered in the Book; but as the entry point for most people, it's how they discover features, so it should at least provide an introduction, and link to the reference for more information.
Likewise, while I think that the std::ops documentation does a good job of covering all of the details of operator overloading, I think the Book could provide a slightly more discoverable introduction to operator overloading which isn't just being used as an example of another feature, default generic type parameters, and provide some links to std::ops which might help with the Googleability problem.
One thing that I think really needs to be focused on is some SEO for the docs. For some reason, the way the Rust docs have been organized over the years, with old versions of docs and the old book being replaced by the new one, means that a lot of times, when I search for something I find old, out of date docs.
For instance, with operator overloading, if I search for "rust operator overloading", I find Rust by Example, which is OK but has a lot of text in comments which is harder to read, followed by a first edition link which warns about being out of date and then provides a link to the new book but not where in it to look, as well as a link to the 1.30.0 version of the first edition book, then a link to std::ops (which if I were a newcomer, I might not realize was relevant), then a 1.5.0 edition of the book.
The first edition operator overloading chapter seems pretty reasonable, actually, and it's present in the table of contents so its more discoverable, and it contains a link to `std::ops`. But because of the new structure of the second edition book, the first page I see if I try to check the book tells me that it's outdated, and directs me to the new book in which this material is harder to find.
And in the new book, even once I find it, there's not much to direct me to where to learn more about the topic. There's no link to `std::ops` documentation; I have to separately look that up.
Part of the problem was that the first edition book was a bit more like a more accessible reference, while the second edition has a more narrative or tutorial structure. So there's no longer a place to go for an accessible reference to all of the language features. There's the Reference, but that's written for detail and precision and not accessibility, and as discussed, is incomplete. There's Rust by Example, but that's mostly just code examples with commentary in comments. The second edition book covers a lot of the same material as the first edition, but because of the different structure, some of the "reference information" on more advanced features is just buried in a couple of "Advanced X" chapters.
I'm not sure of the best way to address this; I admit that getting this right isn't easy.
One option might be to have the new book be in two halves; one of which was the narrative/tutorial portion; chapters 1 through 18, and 20, perhaps, though they might be able to be slimmed down a bit and some material moved to the second portion. The other half which is more of an "accessible reference" for advanced features; the material in chapter 19, but expanded a bit and with more discoverable per-topic chapters like the "Syntax and Semantics" section of the first book, plus the Nomicon material, and any of the topics which are missing.
Or, alternatively, just expanding on and integrating the "advanced X" material, Nomicon, and missing topics into the overall structure of the book.
Or another option would be to have the Book just be the Book focusing on the main narrative/tutorial introduction to the most common features, and have a Reference in two portions; one longer one which covers every major feature but in an accessible style with examples, followed by a more formal appendix covering each last little detail.
I guess ultimately what I'm saying (sorry for taking so long and so many diversions to say it, but as you point out, it is hard to get this right, and I've been thinking about what I'd be looking for as I write), is that there should be some place that has an "accessible reference" to all major features which is organized somewhat like the "syntax and semantics" section of the first edition book. The second edition book, chapters 1 through 18, is definitely a better organization as a book than the first edition, but that structure doesn't lend itself well to covering every feature, and as you point out, you wouldn't want it to.
But the Reference also isn't a friendly place to point people, both because of the "incomplete" warnings but also because it's written in a more formal style.
And the fragmentation into the Book, Nomicon, Rust by Example, stdlib docs, 2018 Edition Guide, Cookbook, Reference, Cargo Guide, Unstable Book, along with some of the reorgs along the way, can sometimes make it a bit hard to figure out the right place to look. I feel like there should be one document somewhere that has a reasonable accessible introduction, with examples, to every feature, in addition to the more formal and succinct Reference.
Maybe I should put my money where my mouth is and try to compile something like what I mean. It would help to have a little bit of buy in from docs team leadership, though, so that there would be a hope of it getting merged and maintained. Perhaps a docs RFC would be appropriate?
Compare with Go's language spec, which is written in a formal (but readable) style with little math. I wouldn't want it to have examples because it's good that it's concise.
One way to start might be to have an official extended cheat sheet. A cheat sheet needn't explain everything, but it should list every language feature available. The idea is to give you a definitive "inventory" of everything available so you can search for answers elsewhere if you see something you don't understand. It should also be brief enough for the language maintainers to commit to updating it when they enable new features.
It is actually fairly comparable to the Rust Reference (https://doc.rust-lang.org/reference/introduction.html), though the way the Rust Reference is divided into chapters makes it a bit hard to see everything at once or search.
They both do contain some examples, as well as things like the syntax in a BNF like format.
Perhaps what I'm looking for is just to have the Rust Reference made a bit more complete and a little more accessible in format (the division into chapters doesn't really seem to help much), as well as improving the SEO since it doesn't show up in Google searches often.
> Go’s spec isn’t formally proven, for example, so you could make a similar claim: how can people know that it all works without a proof?
that's moving the goalposts. let's see Rust's language spec finalized first, then we can talk about it being proven. without the two you shouldn't throw stones.
There are no stones being thrown and no goalposts moving. Go's specification is clearly in a much more advanced and useful state than Rust's. The point is to respond to "how can people know." That is, it is a measure of degree, not binary.
I actually think a mathematical-flavored spec where a proof would be meaningful would be a bad idea to the extent that it makes the spec less readable by ordinary users. For example, Dart has a more mathematically flavored spec and it's not readable by most people.
Of course, formalisms can still be useful, but as a matter of writing style, maybe it's better to put that sort of thing in an appendix? (As sometimes done for grammars.)
I would rather have a formal spec as the definitive one. Deriving a formal spec from a readable one seems to hit ambiguous cases all the time. Just look at publications that do that for C or Java.
With a formal spec you could also derive an implementation automatically which is a great useable reference. Look up the K framework for one possibility.
One can generate an informal spec from a formal spec.
One can NOT generate a formal spec from an informal spec (or else that “informal” spec would actually be formal, after all).
So, a formal spec is strictly better than an informal one — it enables all the benefits of an informal spec, via the ability to generate any number of informal specs from it in many human languages, cultures, levels of detail, etc., and of course enables things like compiler reproducibility (which you cannot do at all without a formal spec).
That being said, any spec is probably better than no spec.
Since many programmers are not mathematicians, a formal spec will always have a smaller audience than a well-written spec written in English. You cannot automatically derive good writing from pure mathematics.
As a result, neither is strictly better than the other. They have different audiences and serve different purposes. The audience for pure mathematics is quite small.
I don't use Go, but I thought of it while reading this post too. I respect their well-documented restraint [1] and also their documentation. I can't think of another language that has both users and restraint, to that degree :)
In order to do this, there also needs to be an unambiguous formal semantics of Rust, so one might even hope that this will turn into some form of official language specification.
I think a full, accepted specification that is adjusted in line with design decisions is still a bit out. Because as soon as you have a blessed specification, people and tools and such are going to depend on it, which limits the ability to change and break the spec down the line.
I don't think Rust is stable enough yet for a real specification.
> Reputation for overcomplexity, loss of users. Becoming the next C++ or Haskell.
I think Rust is already there. It's a concern, but I'm not sure you can or should try too hard to avoid this.
What you can do is look to make simplifications as you add complexity.
And sometimes innovations can have simplifying effects. For example, the various "effects" libraries for Haskell simplify I/O code compared to not having them.
It's quite difficult to simplify an existing language, even when adding a feature could theoretically simplify a language. For instance, higher kinded types are simple in principle: it's just removing an arbitrary restriction about what you're allowed to pass to a type constructor. However, when adding them to an existing language it doesn't always work that way. In the limit of removing type system restrictions you end up with dependent types, which are very simple in terms of the number of type system rules that they require, but try adding them to Java and you'll get a monstrously complicated beast.
There is at least one language that evolves in the opposite direction. The philosophy of Oberon is that "Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away."
And sadly lost its opportunity to Java in the late 90's.
So whatever Wirth has done was quite of interesting in terms of language research for the language geeks among us, but hardly with any market relevance.
It's interesting to see this critique of the Rust language's development process. There's a lot of process. Most of it aimed at adding new features. That may be part of the problem. The Go crowd seemed to know when to stop.
I've criticized Rust's growth here before. The big breakthrough in Rust was the borrow checker. Finally, one could have memory safety without garbage collection or reference counting. Huge improvement.
"Unsafe" opened too big a hole. I was looking forward to seeing more work on eliminating almost all need for "unsafe". Backpointer support. A way to talk about partially initialized objects. More static analysis. But that didn't happen. Instead, feature after feature was piled on, like C++. Individually, the features aren't bad, but cumulatively, Rust is now a very big language.
Meanwhile, C++ has been trying to retrofit Rust ownership semantics using templates. There's too much "be very careful" associated with that.
All of those unsafe "hole fixes" are features, meaning your desires are for adding even more features.
Additionally, useful back pointer support (i.e. for anything more than a toy doubly linked list) is probably a bigger feature than anything else added since Rust 1.0, and probably harder to work with.
I think that, in not only languages but anything related to code (frameworks, packages, etc.), there is a tendency for things to overshoot because that is likely to make those who know the most, more powerful (they know how to use all the features), and its negative impact is on new learners, i.e. those who have little or no voice in these decisions (for obvious and often good reasons). Thus, it becomes a form of pulling up the ladder after you. Not that this phenomenon is in any way limited to coding, of course.
As a longtime JS dev, I sometimes cringe for this reason when I see new features in ES. Even though I'm excited about them, when I think about experiencing them as a learner I feel intimidated. One good point the article makes that I wish more language designers accounted for is the fact that in a shared language you can't just pick and choose - to reach a level of expertise you basically have to know all of it.
JS especially has been a rocket-ship of new language features lately. It used to be my go-to language for new people since it was so simple (when used with a few boundaries (like every language needs)) that it could be picked up and the basics understood fairly quickly, and you can set people free to look at other people's code, and gradually learn DOM APIs and whatnot as needed.
Not any more. Not even close. The amount of stuff you have to deal with immediately is far larger now.
I actually miss the days when people added this kind of stuff to their transpiler of choice (e.g. coffeescript) and just used that - it doesn't need to be in the language, because it can never be removed from the language. Let fads appear and die outside that barrier, not inside.
Case in point the "how to deal with asynchronous stuff" problem. Callback pyramids weren't great, but the pastiche of approaches that followed (promises, generators, async functions, etc.) Is starting to feel worse.
Yes, I think you could know "all of" of JavaScript in 2009, in the ES5 era. Does anyone even remember that anymore? I'm shocked at how much the language has changed.
I was experimenting with server-side JS in 2009, before node.js was released, and I hacked on Brendan Eich's Narcissus interpreter, which gave me a good sense of the language.
Now when I look at example JavaScript code on the web, I invariably find something foreign. I'm not saying it's bad -- just foreign for somebody who uses the language only occasionally.
I don't believe JS should be a language only for "full-time JS programmers". I use 3 or 4 other languages regularly, so it's not nice when one language wants to hog my mental bandwidth (unfortunately C++ is one of those languages, so I know this problem well).
It seems like everybody forgot what Crockford was saying back then? He talked a lot about the failed ES4 project (which ironically Graydon Hoare was a part of (?)). And ES5 was very restrained as a result, and I think mostly successful.
It seemed this idea of restraint went completely out the door soon afterward, and nobody even remembers what was discussed 9 years ago. Crockford seems to have moved on (as well as Ryan Dahl). I haven't followed the JS language changes that closely, so I could be wrong, but I have the impression that it's completely changed.
-----
I don't think I'm alone, as I remember that Bryan Cantrill recently said that "Brendan Eich never wrote a book about JS". In other words, the language was left without a founding philosophy. It's been an accretion of convenient features, some of which may be regretted in a couple years' time.
There was a Strange Loop talk about this too regarding the lack of history/culture in JS.
It seems like the same thing may have happened with Rust, since Graydon isn't an active contributor by his own admission.
Yeah, it definitely feels like ES6/7/Next is packed with features that theoretically make it a little faster to write code while making it monstrously slower and harder to read for at least 80% of practitioners... (destructuring comes to mind, with a syntax that is bafflingly similar to literal decs, that used to be the "good part") Seems like they made that choice every time.
I was reserving my thoughts about ES6 until I worked with it for a while, but now that I have, I have to say for those reasons you cited I don't like it as much as ES5.
"Confusing" is perhaps too harsh (although destructuring over several levels can be terribly confounding). By "hard to read" I mean primarily the fact that when you are "scanning" code, destructuring statements are syntactically way too similar to literal declarations. It severely degrades, IMHO, the speed with which you can effectively "scan" code in JS. It's not really that you _can't_ understand it when you slow down and analyze - it's that you _have to_ slow down to see the difference between "let a = [0,1]" and "let [,a] = [0,1]". Whereas in ES5, whenever you saw the brackets you could know without reading into the code there was an object or array being declared.
That's not to say that I think destructuring is all bad - it certainly saves lines of code - but aftet working with it for a few years, I would gladly trade it back for more scannable code, easier syntax for beginners, etc.
I'm not GP, but I believe they were referring to the fact that the syntax for destructuring is visually very similar to the syntax for object literals, which makes it easier to make mistakes while reading code in a hurry or as a newcomer to the language.
I ran into the Failure crate today. It was hard for me to figure out if it was something official that was going to make its way into the core language or just a third party crate. If the former, should I avoid learning failure patterns that are not using this crate? It sometimes feel like the language is moving too fast for me to learn it.
Failure is "semi-official" (It's not official, but it's created by a prominent member of the Rust community) and experimental.
I believe that the general consensus is that it is to be avoided for libraries. For application development it is convenient, but not perfect and it will probably be replaced by something better at some point.
> It sometimes feel like the language is moving too fast for me to learn it.
It has been moving fast, but expect that to change over the next year! Most of the "Rust 2019" posts have been advocating slowing down.
`failure` captures common error patterns in Rust and provided a test bed for experimenting on them while working to improve `trait Error`.
There is an RFC for pulling some of the trait improvements into the language. After that, I believe they plan to continue to iterate on the design of the failure crate.
The general recommendation I make and see from others is that `failure` is far from stable. Feel free to use it in applications but avoid it for libraries.
Anything that would break failure in libraries written now would break all libraries because it would mean the fundamental Error trait has changed. Since failure is Error compatible you don't lose anything using it. Its only ~2500 lines of Rust.
Coinciding with the 2018 edition release I ported one of my libraries to it that was using vanilla std::Error impls for two years and dropped about ~400 LOCs out of 600 lines of error handling. It still does all the exact same stuff, and if you use its generic Error impl it behaves the exact same way as the std trait.
If Failure stops getting updated.. fine? It doesn't need changing if it works right now, on the 0.1.3 release, for you. If Rust's error trait changes it breaks all libraries anyway and would only happen in the next edition. Thats probably three years out!
> Anything that would break failure in libraries written now would break all libraries because it would mean the fundamental Error trait has changed. Since failure is Error compatible you don't lose anything using it. Its only ~2500 lines of Rust.
If you expose failure in your public API and failure makes a breaking change, it is a breaking change for your API because your clients need to be on a compatible version of failure.
Your clients don't though, you can have multiple versions of a crate in your build tree. Your failures aren't then compositable with newer failures as failures but they do impl Error and can be used as standard errors.
The alternative is to just use... standard errors anyway? With the aforementioned sizable boilerplate? There's no downside to failure unless there's a less volatile error replacement you'd use instead.
That you can have multiple versions of your crate is exactly what enables the issues caused by public dependencies. It's not about multiple versions of your crate, but rather, multiple versions of the failure crate. If failure 0.2 is released (and doesn't use the semver trick), then you'll have two versions of the Fail trait, and thus, two versions of the failure::Error trait object. Madness then ensues for anyone using failure::Error (or dyn Fail in general).
failure is a victim of its own success. I think most everyone believes it's pretty close to what we want Rust's error handling story to look like. But it's a public dependdncy and everyone started using it. Now it's more difficult to evolve.
> Your clients don't though, you can have multiple versions of a crate in your build tree
Yes, but you can only directly depend on one version of failure. What happens if you directly depend on two libraries with different versions of failure? How do you interop with them? In a lot of cases, you'll be lucky and not need to reference the types in any way and it will "just work". This breaks down if you do need to reference the types.
The thing is that the description method in the Error trait in the stdlib is broken. I can't find the post on the internals.rust-lang.org atm, but the author of Failure has a plan for backwards compatibly fixing it and eventually moving (parts of) failure into the stdlib.
The other thing about Rust error handling is the amount of boilerplate to convert between errors. Error-chain and failure are two iterations on how to deal with it. Failure seems to be the current best practices.
the 1st result is "A 30-minute Introduction to Rust". Going through it requires 4 navigations from the user to get to a valid page, everything in between is deprecated
Interesting! That page has been deprecated for something like four years... thanks for pointing this out. I've seen people mention other pages before, but never this one.
I'd also like to mention that getting to the documentation for anything with Rust is fairly annoying, mostly because a ton of the stuff in the book links to the first edition and getting from the first edition to the current one is really cumbersome. It'd be great if you would be redirected from the old book to the latest version instead of getting to it and only get a warning that it isn't the latest version. Especially since there's no easy way to go from the first edition chapter to the corresponding one in the latest, it's at least three-four clicks and annoys me every single time.
Can you please file bugs when you see this? I'd be happy to update them to point in the right place.
We have some constraints that make plain redirects really hard; we cannot run a web server or use a ton of JavaScript, for example. Maybe this situation is painful enough for community consensus to change...
The documentation is distributed locally, as static files, as well as being available on doc.rust-lang.org. This is considered a big feature.
I personally would love to tell people "sorry, you have to run a basic web server, even locally" for a few reasons, but it doesn't feel socially viable at the moment, sadly :/
Here's one way you might be able to make the case: If you require a web server even locally, then the docs can be installed as a single file (zip archive, SQLite database, etc.). This would eliminate the overhead of lots of small files on at least one popular desktop OS, speeding up installation and updates.
1. A first edition that is marked as outdated [1]
2. A second editions that seems to be not outdated [2] but
3. also the book without any edition marker that differs from the second edition [3]
4. As well as the second edition for various rust versions (eg [4] or [5])
And all of them are only distinguishable via the url. I guess the book without any version or edition marker [3] is the nightly version? But I am not sure. I guess the second-edition does not differ much between the rust versions as the chapters look the same, except for bug fixes? I am not sure.
As a beginner learning rust that is really confusing because I am never sure if I am reading the correct book or if there is some other version with more information.
Eg why differs [6] so much from [7] when both are the "second edition"?
Ok, while writing this comment I now see that [2] are only empty pages linking to [8] and that all that books and documentation are generated for each rust version. But my point still stands: When coming from the google search result page to the rust documentation there is some confusion of what I am looking at.
As of Rust 1.31, there's only one version of the book, doc.rust-lang.org/book
Here's a short history: I wrote the first edition myself, before Rust 1.0. Then, Carol and I started a re-write. When it was ready to be published, we cut the final draft as the "second edition", and forked off new work in what was called the "2018 edition". That's why they're so similar.
However, for various reasons, this situation isn't ideal. So we've decided to only ship the "edge" version of the book, even if it may be a bit ahead of what's currently in print.
Thanks for chiming in here and giving more context. I agree with other commenters about the potential confusion for beginners.
The tour of rust in 30 min should probably do an automatic redirect to the doc.rust-lang.org/book
I'd love it if there was something interactive like the golang tour but the book is already a great resource. Thanks for writing it
To be fair, C++ did a lot of research that the Rust designers then could learn from. I hope that C++ will get replaced by something simpler but it's a phenomenal success story.
The comparison between C++ and Haskell is laughable, and shows that Graydon is missing the most important facet of this:
GHC Haskell and Rust with have an intermediate language (Core and MIR + Chalk, respectively), which keep the rest of the language in tight check. No other mainstream language has this (Java bytecode doesn't really speak to high level safety properties). This ensures the vast majority of the languages are forgettable sugar, and making consistencies of various sorts tractable.
I would argue that even outside of languages as we conventionally think of them, the lack of separating functionality into core languages/interfaces and sugar is the central failure in letting complexity spiral out of control. And indeed, just about everything has.
That is not an important facet at all.
It's not a thing your users see. The "syntactic sugar", as you call it, is the interface between you and your users.
No amount of pretending otherwise will change that.
No amount of separation will change this. It does not give you any more ability to change the syntax over time, or get it righter.
Your users care only about this syntactic interface.
In a good world, they do not care how the rest happens in practice.
Worse than this, the idea that a mid level IR should be the "thing that keeps the syntax in check" seems beyond broken.
The lower levels do not drive the higher level in roughly any case.
Instead, they exist to serve the higher levels.
First you understand what users want at a high level, then you try to understand how to make it fast/well.
If you need to change the mid level IR to do so, you do.
There are nothing but tradeoffs in mid level IRs, and those tradeoffs change over time based on the needs of languages, not the other way around.
Driving a language based on what you can accomplish in a mid level IR would be incredibly silly - "Welp, we better use this form of parallelism at a high level because we chose coroutines in the mid level IR" should never occur.
Instead, the answer is "we change the mid level IR to best support the form of high level parallelism we want in the language"
While I generally agree with your point, D/Walter Bright slightly disagrees with the sentiment. One constraint for D is "must not require data flow analysis" because that incurs a cost on compilation time. Afaik this was mostly inspired by Javas definite assignment rule [0]. If fast compilation is a goal, then a language is constrained by implementation and IR aspects.
This is a perhaps over-fine semantic distinction, but I would argue that there's a qualitative difference between IR/language implementation and compilation speed. The former should not drive the language's features; the latter is a language feature.
IMO most discussion of the compilation speed is incredibly narrow-minded. We incremental compilation more than anything else. Programs barely change each rebuild, so that should solve all the performance problems without having to maim ourselves.
OK yours is the second comment to talk about "Users". IMO this first-impression-based design methodology, like we're writing some consumer CRUD app for the startup [proverbial] you're gonna quit in <= 3 years, is a terrible skeumorphic philosophy for designing languages. Languages are for more terrible; programs last (too?) long; long-future readers may be expert or knowledge.
Let me response point-by-point
> It's not a thing your users see. The "syntactic sugar", as you call it, is the interface between you and your users.
Users can and should learn the core language too. It's can allow compressing the information in the brain just as it allows reducing the code in the implementation.
> In a good world, they do not care how the rest happens in practice.
Again, I believe the core language is a meaningful thing to study and learn, not just some shove-under-the-rug implementation detail. You clearly don't, and your conclusion is indeed implied by that presupposition. It's hard to argue either from other principles so let me say it is widely held by those that study programming languages and work with cutting-edge programming languages: My axiom has more buy-in by the relevant in-groups. Maybe out-groups with yours are right and we are bad designers, or maybe those out-groups should accept a non-trivial learning curve vs permanent complexity.
> The lower levels do not drive the higher level in roughly any case.
This is wrong. As someone active with RFC discussion for both Rust and Haskell, this empirically not the case. Ideas are constantly proposed and vetted based on whether they fundamentally extend expressiveness or are just sugar.
> First you understand what users want at a high level
Yes, languages changes should and are be driven by end goals, but end goals != surface syntax!!
> There are nothing but tradeoffs in mid level IRs
What does this even mean? Surface syntax is full of tradeoffs too.
> those tradeoffs change over time based on the needs of languages, not the other way around.
Again, once the MIR/core exists, RFCs heavily reference them so this is false.
> Driving a language based on what you can accomplish in a mid level IR would be incredibly silly - "Welp, we better use this form of parallelism at a high level because we chose coroutines in the mid level IR" should never occur. Instead, the answer is "we change the mid level IR to best support the form of high level parallelism we want in the language".
This leads me to think you don't know much about IRs in these langauges in practice. Neither Rust's or Haskell's IR has any notion of concurrency precisely because nothing good enough has presented itself. Both language use syntactic / "encapsulation tricks" (See Haskell using the IO monad to avoid the value restriction of OCaml, Rust's Send and Sync) to chew off some safe subset that works with many modules. An IR with a deep/"semantic" understanding of concurrency doesn't exist for these "production" languages, though I hope http://plv.mpi-sws.org/rustbelt/ could get Rust there someday.
"User" refers to someone writing code in the programming language---someone "using" the compiler. It's not some project-management-y jargon.
-----
Extending expressiveness vs sugar is fundamentally a property of the high level language, not the mid-level IR. It happens that talking about a mid-level IR is sometimes a nice way to "prove" that something is new or sugar.
As the parent said, the programmer using the compiler only truly cares about the high-level language when writing code. They may dip into the mid-level for optimising their code etc., but they won't write it directly: to a non-compiler author, IRs are implementation details of the programming languages that happen give insight into how some detail of the language works or how a particular piece of code is optimised.
To hammer home this point: if there's some major change required/desired in the high-level language, the current details of the mid-level language shouldn't affect the overall design. The mid-level IR can change to accommodate the desired surface behaviour.
You leave out the 3rd and conflate the second. Others have conceive of PLs as some sort of bargain between human and machine, or human and pure math. Either way, there's multiple interests at stake.
Or, even simpler, what if Rust becomes way harder to learn, but mastery gives one correspondingly way more productivity? Is that a fair trade in your eyes? (I do not believe in practice it will become way harder to learn, but I accept the situation I describe as fair and not a failure of Rust).
-----
You may not write it directly, but you will still think about it. Again the IR of these good languages is like some platonic ideal that sacrifices concision of programs for concision of the language. It is not just an inplementation detail.
Hell, if you insist on thinking that is just some crufty implementation detail, sure. But then let me show you how it's an abstraction that leaks badly, hahaha.
- "Non lexical lifetimes" are probably the most anticipated Rust change lately (see rest of thread). But crucially, they are lexical at the level of MIR. The easiest way to precisely define them is desugar to an explicit control flow language. Otherwise you are forced to look at the a combinatorial explosion of surface-level constructs, or just handwave based in the intuition of control flow.
- https://github.com/thepowersgang/mrustc/tree/master/src/mir this is the alternative Rust implementation's MIR. It's pretty close to the same thing! (Not so with LLVM IR and GCC's RTL.) If mrustc gets to implementing NLLs, it will probably get even closer.
- mrustc does not implement old borrow checking probably because it's sort of an arbitrary spec against the surface syntax. Surface syntax is much more boilerplate to go through all the cases, and while old lifetimes may have a simple enough rough intuition, they are very arbitrary when considered precisely.
And yes, the core language can change underneath the hood, but this is highly unlikely in practice. GHC's core has certainly changed, but this is evolution more than revolution.
I think the heart of what I'm saying is two things:
1. Formal reasoning is the only way to constrain complexity. Humans alone inevitably don't fully graps the entire thing (it's just too big!), and even when they do don't have identical mental models. The results is always contradictory design if any change at all is permitted.
2. Human PLs have lots of conveniences and other fluff (yes, even Scheme, Nix, or your other favorite minimalist language), so the only efficient way to reason about them formally is by desugaring to a core language.
Language implementors are a very special case. Their effort is O(1) while the other two are O(amount of code in the language). In any case, I don't see what your point is.
---
I think you're conflating things too: NLL is defined in terms of a CFG, which happens to be MIR at the moment, but if the current MIR is insufficient for some task, it can still be fundamentally changed, with the NLL semantics handled specially, e.g. with a multi-step lowering, like HIR in Rustc.
The mid-level IRs chosen for a compiler are in service of the high-level language and its tools (like formal/static analysis), not the other way around.
In fact, a static analysis or formal reasoning tool likely needs more information than fits into a compiler/optimisation-focused IR. (You see this even with LLVM IR vs MIR and SIL etc: LLVM IR can't hold enough info for the front-end to do everything.)
Also the Dependent Haskell work of Richard Eisenberg I'd argue is more important as an attempt to shrink the language than add expressive power. The main thing it does is combine existing language features, which only yields more constructs insofar as the old features had friction. You can think of it roughly as taking the convex hull (or probably better bounding parallelepiped) of the existing feature space.
Seen in this light, I've never such a thing attempted at this scale (GHC's size and GHC/Haskell's age), and very much commend it. Hope other things can do the same thing someday.
I agree with you, but complexity is perceived by newcomers through the surface syntax of the language, not through an internal, mostly invisible IR. It's great that as advanced users, most of the perceived complexity can simply be thought of as sugar, but to a newcomer without an understanding of the core of the language yet, the complexity is often intimidating.
It's easy for a language designer to say things are just "forgettable sugar" whereas in the eyes of the users, they are anything but forgettable.
For starters, I think newcomers should look at the core languages more. It's a great way to develop a (shared) mental model, rather than do so independently, reinventing the wheel and then maybe finding that yours disagrees with others'.
But even discounting that, I think it's still meaningful. Sugar is easier to deprecate; The desugaring can be used to remove deprecated sugar in the wild too. Core language complexity is a lot harder to eradicate.
No (safe) Rust is safe, while arbitrary assembly is unsafe. "Sugar" where the difference between the range (of elaboration) and codomain is significant is not sugar.
If we had an assembly+proofs language and a compiler from Rust to it, then I would agree. See CakeML for an example of research towards correctness-proof-preserving compilation.
Actually this is for any definitional of "safe". The important part is codomain vs range. As long as the target language allows source language invariants to be preserved (by explicit proof or by construction (proof in meta theory)), I'd call it sugar.
This doesn't obviously match with Felleisen's notion of expressitivity (which might be used to draw the line between desugaring and general compilation), but I think I like it better. I value invariants over expressiveness.
"Reputation for overcomplexity", especially there being little on w technically level to stop the language from growing and devolving into inconsistent design.
Since when did you need to read, comprehend, and digest every language feature in Rust to ever use the language?
The absence of HKT / GATs / overloading / default arguments from the language doesn't make it easier for a newbie to learn. These aren't topics someone new, or even intermediate, should ever be touching until they need them. And in the absence of having this functionality (example - we JUST got const fn this month in stable) you have to work around the weaknesses of the language in often obtuse and cumbersome ways... such as hacking the macro system with lazy_static.
I think a lot of this sentiment comes back to what makes a language hard. I recently started learning CSS properly for once, now that Grid is in and I can just do all my styling by hand again in the same way I can use ES6 instead of JS frameworks. I needed Bootstrap and JQuery back in the day because despite the simplicity of the fundamental languages there were things I wanted to do that I could not do in an intuitive way. So other people wrote thousands of lines of code to make what should have been simple and accessible but the languages weren't expressive enough.
On the inverse, you need some degree of intuitiveness to your design. If I didn't read about the syntax of [<name>] col [<name>] col in CSS Grid I would have never intuitively thought you could name Grid regions. The syntax is completely arcane and arbitrary, isn't based on anything else common to CSS, and just happens to be. That is the source of real complexity in a programming language, not how long the book is or how many pages long the standard library is. Nothing worth learning or doing is so simple as to be digested in a weekend and trying to make a programming language like that, especially a systems one - one that has to expose all the jank complexities of how computers actually work - would have you make a really pretty paper weight nobody could use to get real work done.
The complexity in C++ is not in how big the template library is, or how many glyphs the syntax uses, but in how much mental stress it is to see void* = 0 or new Foo and have to juggle all the edge cases and eccentricities of the grammar and model on every single line. Its a massive weight on ones shoulders and that is what will drive a newbie away, at least a newbie with the mindset to take advantage of what they learn. Not the length of your book, so long as all those chapters are about things people want, need, and that are beneficial to the user, rather than warning them about all the ways they can break everything with a single character or how arbitrary decisions were made incongruent with the rest of the language because thats just the way it is.
C++ and C are not complex for their expressiveness, they are complex for their arbitrariness and lack of consistency. Same way Javascript and CSS are complex. Same way an actual, legitimate newbie to Rust is going to have much more difficulty processing why :: is the namespace delimiter or why you need curly braces to deliniate scope than how having the option to declare your function const would. Until they need const functions they don't need to touch them and can write all the non-const Rust they want. But now they have const when they need it and can often never touch something crazy like lazy_static again.
> Same way an actual, legitimate newbie to Rust is going to have much more difficulty processing why :: is the namespace delimiter or why you need curly braces to deliniate scope than how having the option to declare your function const would.
I disagree. :: and curly braces are just a shallow bit of syntax: you see it, learn it and it doesn't interact with anything. It is the easy part of learning the language. For example Go has curly braces too, and it is one of the fastest languages to get started with.
Maybe const functions are not visible immediately, but then as a beginner you would start looking at some documentation, and you'll see const there in API documentation and not understand it, so that makes the language look like it has these opaque features you do not understand. I'm not saying const functions are not worth the cost, I'm just saying that they do have a cost and add complexity in a way :: and curly braces do not.
I completely agree that complexity of edge cases, undefined behavior, and high combinatoric complexity between language features is a FAR more damaging kind of complexity (due to its nonlinear cost) than simple language feature count.
However, I disagree that simple language feature count has no cost: Assuming “linear cognitive complexity”, even such linear features do have a cost: Sure, you may not need to learn what a “const fn” is until you need it if you’re working only on your own project, but if you’re working on a team and/or with external libraries, the chances that you can get by without knowing the ENTIRE LANGUAGE diminish rather quickly with project size.
This is why even the least costly kinds of language complexity can still end up costing a lot in a large project containing a lot of code, dependencies, and developers.
Yes this hole thread makes me think we have a bunch of people who think complexity = initial learning curve. No wonder most of our industry is an inscrutable inconsistent mess.
Allow me to summarise the pitfalls of any sweeping 'keeping features out of the languge spec will prevent users from having to deal with the complexity burden of those features' assumption with a story:
Once, long ago, there was a new programming language. It was lean and mean. Partly to keep codebases understandable to all, and to make the language easy to learn and straightforward, it eschewed both having too many features, and having any feature that was too abstruse. People marvelled that the whole language, standard library and all, could fit in a nutshell. The name of the language was Java. The end.
Rust can force a programmer to be disciplined and that's good as far as it goes, but sadly no current language can force (or even encourage) a programmer to think critically at an architectural level. Indisciplined coding leads to security issues (at least immediately), but a poor architecture can mean a complete failure of the project to meet its goals, never mind exceeding them.
Async/await isn't stable yet, that seems to be making many commenters here wait to adopt, so at least that should make it in before they lock it down. I'd like the platform independent SIMD but that I can admit is niche and could live in nightly for a long time without harming adoption. They have polls to get a feel for the difference between those two categories and which features fall into them.
Async/await appears to require runtime support, possibly even a modern OS. Wasn't rust supposed to be suitable for writing bare-metal code?
SIMD at least has hope of running on bare metal. I suppose the ideal is to have it adjustable: compiled to non-SIMD, using typical SIMD hardware, using implicit threads (and thus needing OS support), or using SIMD hardware with threads.
I fear the high-level web developers are in the driver's seat, which may doom rust as a systems language. I'm hesitating on rust because I would miss bitfields and various unsafe performance features. I like the opt-out safety of the "unsafe" keyword, but I want more things that I can opt out of. I want goto, computed goto, no-default switches, something like Microsoft's __assume keyword, and all the other low-level stuff you'd want for making boot loaders and high-performance kernels.
> Async/await appears to require runtime support, possibly even a modern OS. Wasn't rust supposed to be suitable for writing bare-metal code?
You're probably thinking of Tokio, the library for async network IO, which obviously depends on an OS. The async/await language feature compiles down to basically a state machine and has minimal runtime requirements. I'm looking forward to using it on bare-metal microcontrollers.
async/await in Rust produces a single value, a Future. Executing that future requires calling its poll method repeatedly. That can be extremely simple, or it can be more complex. We call "a thing that calls poll repeatedly" an executor, and they can be written without an OS or even the standard library, just fine.
The largest, most well known executor is Tokio, which is built on top of OS primitives. If you're writing your own OS, you'd use those primitives.
> I fear the high-level web developers are in the driver's seat
First, I pretty fundamentally reject this as an idea, but since you don't, please do a Google Scholar search for many of the core team members. They're very much not web developers.
How is adding async await will make Rust into C++ ? Would people actually name the features that should not be in? Outside of ownership and borrowing have Rust added any features that are not mainstream and well proven in other languages?
How did adding exceptions to the language make C++ into C++? RTTI? Multiple virtual inheritance? Default member function generation?
They didn't, not individually. They were popular and uncontroversial (rather less controversial than promise apis, even). And yet...
The point is that Rust seems to be charging blindly down the same road, not that any one feature is going to blow it all up. Frankly IMHO Rust is already harder to learn for a median programmer than C++ is, even if it makes more aesthetic sense to an expert.
I think there is fairly large difference between learning and being able to write production ready code. It's hard to imagine a person for whom Rust is hard to learn and who can write safe threaded code in C++ for example.
Hundreds of thousands of engineers are writing "production ready" C++ code every day. Your point seems trivially falsifiable, unless you want to turn it into a no-true-scotsman situation and explain how what they're "really" writing isn't "C++" or whatever.
I'm not saying Rust is bad. I'm saying it's... becoming senselessly complicated. And that in 20 years when 60% of its amazing new features turn out to be just fashion (because they happens to everything), Rust will be "ugly" then in basically the same way that C++ is now, and we'll all be chasing some other new hotness.
Go never banned generics, it just set a very high bar of integration with the rest of the language.
The idea is that Go is one language with a very specific personality: if it changes that much it just becomes another language and can as well be called something else.
any shop that is considering Rust would need to consider the question of how to hire or train a sufficient number of programmers. With any mainstream language you have a big pool of programmers up for hire, my guess is that this is more of a problem with a new languages like Rust.
Maybe that's the reason why they did hype Java to such an extent when it was a new platform, i think SUN understood that it is quite difficult to gain acceptance as a mainstream player in this game.
>Magnifying inequality: only the most-privileged, available, energetic, well-paid or otherwise well-situated participants can keep up.
>privilege this privilege that
Shit like this is why I will never touch this language.
I love the intentionally limited scope and the amount of thought that went into writing this, and I wished I had the clarity required to be able to do this. Especially the bit about 'negative space' got my intention, that's one tool I'm going to put in my toolbox to re-use. And there already is one example from a company whose founder I knew that made serious bank on just knowing what they were not, so this advice has applicability far outside of computer programming language design.
Thanks for posting this.