September 2022:
This has spread to a far wider audience than I had anticipated - probably my fault for using a title that is in hindsight catnip for link aggregators. I wrote this back in 2021 just as a bunch of personal thoughts of my experiences using Rust over the years (not always well thought through), and don't intend on trying to push them further, outside of personal experiments and projects.
Managing a living language is challenging and difficult work, and I am grateful for all the hard work that the Rust community and contributors put in given the difficult constraints they work within. Many of the things I listed below are not new, and there's been plenty of difficult discussions about many of them over the years, and some are being worked on or postponed, or rejected for various good reasons. For more thoughts, please see my comment below.
I love Rust and I have used it daily for many years as my go-to language, but I do have my own list of grievances that give me enough itches to contemplate playing with my own language ideas! Esteban Kuber's tweet inspired me to compile these into a list, even if I don't consider myself a ‘hater’!
Do note that Rust was a product of it's time, and was under many constraints in during its development, and I don't begrudge the choices that were made. Many of these things I only really recognise myself in hindsight!
- Many sub-languages to learn, many with different syntaxes and semantics.
For example:
- the expression language
- unsafe runtime language
- safe runtime language
- compile time language
- the type language
- the trait language
- the macro language
- the attribute language
- the expression language
- Language is designed in a monolithic fashion, as opposed to elaborating to a simple, verifiable core language with roots in type theory.
- NLL has access to tricks that are not expressible in type system, making it impossible to factor out code in some cases.
- Complicated, mutually recursive modules that make incremental compilation hard.
- A large menagerie of traits that could point towards a lack of polymorphism:
- function types:
Fn
,FnMut
,FnOnce
- conversions:
From
,TryFrom
,Into
,TryInto
,As
,AsRef
,AsMut
- function types:
- Traits are hard to extend after the fact, and you can't break them into smaller parts in a backwards-compatible way.
- Traits bias the
Self
parameter, which can make some multi-parameter traits rather odd. - No way of defining abstract types that conform to a given interface. You can
use traits, but these do not support abstract associated types, and require a
Self
type. Supporting this kind of feature is now difficult due to the complexity of traits (Haskell also struggles here). - Trait objects are a bit lackluster
- Complicated rules around object-safety.
- Associated types are incompatible with trait objects.
- Not allowed to combine trait objects
- Could this all have been achieved with better support for existential types?
- Marker traits (like
Send
andSync
) are kind of weird? I don't fully understand the complexities behind them, but IIRC have weird properties like leaking throughimpl
trait. I've also heard it's hard/impossible add more of them in the future. - Conflation of
mut
for unique references and for 'unfrozen' local variables. - Specialisation breaks parametricity. This is not necessarily a bad thing imo, but it's impossible to choose if type parameters are parametric or not.
- Silly gripes about syntax:
- Angle-brackets (
<
,>
) for generics is a bit of an eye-sore in complicated types. - Records use
:
in their literal form, which is overloaded with type ascription. This is also annoying in cases where you might want to move a field into a let binding. - No space before the colon in type annotations in the default formatting.
- Complexity in the grammar around:
- allowing semicolons to be omitted after some expressions, like
match
,if
/else
, andif
. - leaving off commas after blocks in match arms
- allowing semicolons to be omitted after some expressions, like
- match expressions syntax leads to lots of nested indentation
- Angle-brackets (
- Async/await splits the ecosystem: having access to effect polymorphism would have been nice (see the work on typed effect systems, like what Multicore OCaml is working towards).
- Type aliases are transparent (as opposed to abstract) by default, exposing their definitions publicly.
- Type aliases are leaky, and can expose implementation details via definitional equality. There are some lints to catch this kind of thing, but they often fail to fire.
- Hard to keep track of whether functions are panic-safe or not.
- While using allocators like arenas is possible through the use of crates, Rust's standard library lacks much in the way of support for this style of programming, and most libraries are not implemented with support for it, and the ones that do are often incompatible with each other.
- IO, file system, and other effect-based libraries are not capability-safe.
- Poor sandboxing/security for procedural macros (see point on capability-based security). Would be great if procedural macros would be only able to use safe Rust, with stricter requirements on what capabilities they have access to.
- Lack of support for typed, interactive programming through the use of
hole-driven development (As seen in Haskell, Purescript, Idris, Agda, etc).
todo!()
gets you some of the way there, but it's not possible to use it in places like types and patterns to express partially complete programs. Granted, this could be less useful in the presence of impurity (due to less precise types). - You can't run broken Rust programs (translating compile time errors to runtime errors).
- It might have been better to call
unsafe
blockstrusted
blocks. - Lack of visibility of the 'trusted' parts of code a library/code unit is taking on. Less in the sense of naming/shaming, and more making it easier to get visibility and double-check.
- No support for using something like separation logic within Rust itself to verify that unsafe code upholds the invariants that the safe language expects.
- Confusion about whether to use
kebab-case
orsnake_case
for crate names I now lean to the former, but it's impossible to import snake case crates using kebab case, leading to an ugly mix in myCargo.toml
file. Cargo.toml
is capitalised, unlike most other development files.
For some reason this got on the orange web site. I posted a comment there, but I‘ll repeat some of what I said here: These are my personal thoughts from last year... I think there are some things I would add or change now and it's ok to disagree with me!
Many of the things I listed are not new, and there's been plenty of difficult discussions about many of them over the years, and are being worked on or postponed, or rejected for various good reasons (I could have done a better job at citing stuff in this gist). Managing a living language is difficult and challenging task, and many compromises need to be made. I think the Rust community is doing a great job considering all the challenges.
That said, I'd love to see more language designers consider the possible space of memory-safe by default systems languages, learning from what Rust can teach us, and bringing on board ideas from other places, like the newer systems languages and developments in dependent types, sub-structural type systems, etc. There's still so much more to explore, and still lots that can be done to improve in Rust itself.