Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
My personal list of Rust grievances (September 2021)

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.

My personal list of Rust grievances (September 2021)

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
  • 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
  • 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 and Sync) are kind of weird? I don't fully understand the complexities behind them, but IIRC have weird properties like leaking through impl 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, and if.
      • leaving off commas after blocks in match arms
    • match expressions syntax leads to lots of nested indentation
  • 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 blocks trusted 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 or snake_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 my Cargo.toml file.
  • Cargo.toml is capitalised, unlike most other development files.
@brendanzab
Copy link
Author

brendanzab commented Sep 13, 2022

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.

@steakknife
Copy link

steakknife commented Sep 13, 2022

The verbosity of some expressions is like a ride with a coke-crazed used-car salesperson in their personal roadster: nauseating, unsettling, and wondering if it's going to wreck or not. Java just feels like listening to a TV lawyer reciting boilerplate disclaimers in a highly-predictable manner using slow, BBC Received Pronunciation. C++ is like listening to the fastest talker in the world auction off a 1967 Chevelle with a Tommy gun of operators, punctuation, and absurdly-long identifiers.

The obsession with idiosyncratically-specifying lifetimes (liveness) is like we're dropping back to Fortran placement of working set. There ought to be (NP-hard likely) way of calculating precise leader strong components and adjusting covers/subcovers to shrink them below 'static. If they cannot be reduced to below 'static either it has to be managed "unsafely" with linked-list-like transactional memory semantics or there's a possibility the choice of data structure and/or algorithm is attempting to create nonsense. In other realms, GC is a runtime heuristic of various strategies that attempts to side-step liveness static analysis without considering the with a "credit card" by paying with future work (e.g., deferred "technical" debt, in a sense) and using various fractions to multiples more resources than are strictly necessary. And sometimes it's faster to make copies of immutable things to get them into multiple cache lines so that there is reduced contention rather than zero-copy oneself into a global, single-core hot path.

@tsm-x
Copy link

tsm-x commented Sep 17, 2022

And sometimes it's faster to make copies of immutable things to get them into multiple cache lines so that there is reduced contention rather than zero-copy oneself into a global, single-core hot path.

I don't get this one. If it's immutable, then there are no updates, where's the contention coming from? Isn't read from cache basically free?
Are there any benchmarks that illustrate this claim? My 2 minutes of googling showed no results.

@rlabrecque
Copy link

rlabrecque commented Oct 2, 2022

@tsm-x If you're still interested, search for "Cacheline false sharing". That's likely what the OP is talking about in that case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment