122 comments

[ 0.20 ms ] story [ 179 ms ] thread
I really enjoyed how this spanned from nitpicks/taste to things that have more depth.

I laughed at how the author noticed Cargo.toml was unusually capitalized - something you can likely notice consistently after burning it into your eye for years.

> As one of the most frequent interactions with Cargo, the question of why the configuration file is named Cargo.toml arises from time to time. The leading capital-C was chosen to ensure that the manifest was grouped with other similar configuration files in directory listings. Sorting files often puts capital letters before lowercase letters, ensuring files like Makefile and Cargo.toml are placed together. The trailing .toml was chosen to emphasize the fact that the file is in the TOML configuration format.

https://doc.rust-lang.org/cargo/faq.html

(That was actually my guess, but I figured I'd do a google search and then decided to quote the FAQ.)

It's wild they actually documented this reasoning! Seems like an easy detail to omit.
it's only unusual if you never write Dockerfiles or Makefiles...
This seems like a reasonable list of things a reasonable person might have grievances about.

With these types posts in general, keep in mind that they are "personal lists" -- meaning that other people might feel differently, and that's OK.

The context of the tweet that motivated this was "how can things be improved", and lists like this help (if not for rust, then for whatever comes after rust).

To pick one thing (mostly at random) to comment on:

> It might have been better to call `unsafe` blocks `trusted` blocks.

This is a thought I've often had myself. The name `unsafe` is not wrong, per-se, but it can sometimes have the wrong connotation

Thanks for reminding people!

> This is a thought I've often had myself. The name `unsafe` is not wrong, per-se, but it can sometimes have the wrong connotation

Yeah, this is one of the many things on this list that isn't a new idea, see this RFC from June 2014: https://github.com/rust-lang/rfcs/pull/117. I believe D has a `@trusted` attribute at the function level that serves a similar purpose.

The unsafe blocks should have been

    hold_my_beer {
       …
    }
This is a pretty reasonable list, one I resonate with (as an otherwise large fan of Rust).

I'll point out one that I think is a little less solid:

> Cargo.toml is capitalised, unlike most other development files.

This is consistent with Makefile, Gemfile, Pipfile, etc., meaning plenty of precedent. My understanding is that these files are capitalized to put them first in a directory listing.

VS Code at least does not order capitals first.
I meant in a terminal. When I run `ls`, I get capitals first. Maybe VS Code orders by modification time?
It orders alphabetically but, differently from basically every other tool I use, doesn't put capitals first. Never understood why.
It sorts lexicographically, case-insensitively. Microsoft tends to favor case-insensitive file names because humans perceive instances of the same word in different case to be equivalent.
It's a bit strange in a programming tool, though, given that programmers are used to case being important, and virtually all other similar tools order capitals first.
Isn't that just due to "lazyness", rather than an explicit design choice? I mean just doing char based comparisons, which is the easiest solution, will give the separation of uppercase and lowercase (at least for ascii and similar encodings).
Windows tends to have a case insensitive file system (although these days it's possible to deviate from this tradition). For files the case being important thing isn't really a thing there.
Sure. But I'm not using Windows.
Programmers using "C family" languages - ok, I guess that's ~99% nowadays, but the "Pascal family" languages (and probably others I'm not familiar with) are case insensitive.
You really, REALLY don't wan't to contextualize filesystem case sensitivity. Either it's case sensitive, either it's not. The rest should be the least surprising in all cases: either conform to long age practices (unix, windows, case sensitive fs or not), or kept behind flags, and then you have the UX for general users (eg. files browser), in which whatever the least surprising for a picked lower bar of computer literate will do.
I agree. But contextualising case sensitivity is sort of what VSCode is doing here. I'm running on a system where files are case-sensitive, yet it, because of Microsoft's preference for case-insensitivity, insists on sorting filenames without regard for case.
Not just humans; NTFS & Windows itself do as well :)
Capitals first, isn't that only in the posix/C locale? Natural language locales should not do that and don't think mine does.
> When I run `ls`, I get capitals first.

It depends on your locale:

  $ ls
  asdf  Cargo.toml
  $ locale
  LANG=pt_BR.UTF-8
  [...]
  $ LC_ALL=C.UTF-8 ls
  Cargo.toml  asdf
The two on here that stand out as things I agree with but don't see called out much are:

> allowing semicolons to be omitted after some expressions, like match, if/else, and if

This drives me up the wall. I know a lot of rust's aesthetic is there to make C++ programmers comfortable (I'm a recovering C++ programmer myself), but it would have been so much better if semicolons had been left out.

It seems like we put a semicolon on every line (except the ones mentioned above! But only if they're not subexpressions!) just to make the whole "you don't need to say return in the last statement" look clever.

> 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.

I'm so tired of guessing which one I need every time I import a crate in Cargo.toml. Never mind that this is a blatant opportunity to make supply-chain attacks. I don't know why these are treated as distinct names in crate names, and if there are any conflicting crates out there in the wild right now they should probably all be forced to rename.

My personal pet peeve that doesn't come up much is the way magic impls around From/TryFrom/Into/TryInto conspire to make incredibly obtuse error messages about implementing TryFrom<T, Error=Infallible> or the like. I'm not entirely convinced having these 'helpful' impls is really worth it, at least not the way they were implemented.

Someone correct me if I'm wrong, but I think the semi-colons main purpose is to make error messages better. It makes it easy to say "well, the previous malformed statement definitely finishes by here".
And to avoid all the horrible edge-cases that can arise when the parser has to work out whether the next line is a continuation of the same statement or not.

For example in JS:

  var x = y.z
  [a, b, c].forEach(doIt)
parses to:

  var x = y.z[a, b, c].forEach(doIt)
rather than the two statements that were clearly intended.
There are a bunch of reasons why this is more of an issue in JavaScript than it is or would be in most languages. The early incarnations of the language practically leaned into bad parsing logic and it's permanently stuck with them now.

But in particular any strongly typed language is gonna need some really convoluted code to produce similar ambiguities that actually make it to linking.

I think the most likely "bad" outcome of semicolon-less rust would be losing the hanging dot function chaining people do so much in rust, but even that's not a guarantee.

This was almost prevented from the get go. Brendan Eich tried to implement scheme in the browser. But the business people asked him to make it look like Java. :(
> But in particular any strongly typed language is gonna need some really convoluted code to produce similar ambiguities that actually make it to linking.

Even if it's a compiler error, what are you supposed to do when the compiler returns the error from the snippet above?

I think you'll need to come up with an example that exploits a weakness in rust's grammar for me to answer that? The above js example I don't think it would make sense to have rust erase the newline to begin with..

I think the main way that people hang syntax like this in rust is with chained .methods, and since . at the start of a statement isn't valid rust to begin with it could probably be unambiguously be allowed anyways.

But I wouldn't expect:

    a
    [1]
To be considered valid specifically because of the potential ambiguity of a single term array and a suffix index. It's also not really common right now anyways, afaik.

JavaScript tries to be way too damn clever here and no one is ever going to seriously suggest anything try to replicate "automatic semicolon insertion". Just don't allow line joins when it makes the next line ambiguous.

Interestingly it would be possible to come up with a straw man parser that makes semicolons optional and then run it through the whole "recompile every crate" thing the rust project has to see if it would cause new failures. It would be an interesting project tbh.

It's a problem in Python too, but in the opposite direction in that Python is too eager to parse multiple lines as multiple expressions instead of a single expression, forcing you to add parentheses everywhere.

  1 +
  2
is a syntax error, you need to make it

  1 + (
  2)
also :

    foo.bar()
    .1 + baz()
parses to

    foo.bar().1 + baz()
             ^^ = undefined
Nope. 1 is not a property name that can be used with the dotted notation because it begins with a figure.

foo.bar().1 + baz() gives:

    Uncaught SyntaxError: unexpected token: numeric literal

The following:

    foo.bar()
    .1 + baz()
is equivalent to:

    foo.bar();
    .1 + baz();
The parser will find this syntax error, backtrack and insert a semicolon (Automatic Semicolon Insertion applies).

Interestingly, I tried to run the following in the browser console:

    {}.1
And it runs. It gives 0.1. This is probably because {} is interpreted as an empty block (of statements), I guess. The following gives the same result:

    {};.1 
The following is a parse error:

    ({}).1

I tried the following to convince myself that {} is evaluated as an empty block, so I went further.

The following works:

    {a:2}.1 
... because a: is a label. you really need to try with "two properties" to see a syntax error:

    {a:2, b:2}.1 
Gives: Uncaught SyntaxError: unexpected token: ':' (character 7). A label cannot be introduced after the comma operator... (yes, because it is a comma operator! {a:2, console.log("hello")}.1 prints "hello" and its result is 0.1)

(WAT?)

edit: {;}.1 and {;;}.1 also work of course.

> Nope. 1 is not a property name that can be used with the dotted notation because it begins with a figure.

Oops, you're right.

JavaScript's algorithm for this is dumb. There are better ways to do it, as seen in Python, Ruby, Nim, …
Because not all statements in a block have to end in a semicolon, and most things at module scope don't, rust fails at this anyways imo. Without an editor's help I'm pretty frequently lost finding the missing balancer for an open brace or parenthesis.
I feel like cargo should have gone further and made it borderline impossible to write out the package name from a guess and require you to look it up and copy/paste the name so its always right. They should have required all packages to be scoped under an org name like NPM is moving towards.

That would have stopped typo squatting much better than making names insensitive to all kinds of weird conventions.

I think it would've been sufficient to namespace package names, something like `dtolnay::syn = "1"` (this is terrible syntax, just an example). Perhaps also allow Cargo to be configured to fail if any of the packages were outside of an allowlist of trusted namespaces. I think this is a good compromise between allowing people to experiment quickly and allowing established projects to lock themselves down.
> It seems like we put a semicolon on every line just to make the whole "you don't need to say return in the last statement" look clever.

This has been my own finding when designing a language. Freestanding expressions can in most cases be turned into statements. Even for things like Rust's if...else expressions, a 'be' keyword could work.

> Never mind that this is a blatant opportunity to make supply-chain attacks. I don't know why these are treated as distinct names in crate names, and if there are any conflicting crates out there in the wild right now they should probably all be forced to rename.

crates.io already disallows crates differing only in underscore/hyphen choice. I can't easily find documentation to this effect, but users have reported it returning an error (e.g., [0]).

[0] https://github.com/rust-lang/crates.io/issues/166

I suppose it must be a thing that while crates.io does disallow this, there's a possibility that there's a private registry out there that does allow it. That's the only reason I can think of for why they haven't just made it treat them as interchangeable in Cargo.toml files.
Is it Stockholm syndrome if I prefer semicolons over no semicolons?

It gives me warm fuzzies in the belly knowing that as long as I've placed the semicolon it doesn't matter if I indented correctly or used the new line correctly.

I'm even one those weirdos that put the first squiggly bracket right after the statement(

if ( ... ) { . . . }

There are languages like Lua that do not require semicolons and are also whitespace (and newline) insensitive.

That comes with other tradeoffs, the main one being that Lua is forced to maintain a pretty restrictive grammar to remain unambiguous. There are only very limited expression statements, for example. But anyway, it exists and it addresses the points you specifically made in your comment.

Also Swift and Go, IIRC.
golang is not semicolon insensitive, otherwise you'd be able to write

    if
    {
        // ...
    }
    else
    {
        // ...
    }
but the compiler doesn't let you.
Maybe. Is it Stockholm syndrome that I prefer indentation?

It gives me warm fuzzies in the belly knowing that as long as I’ve indented correctly it doesn’t matter If I placed the semicolon.

Nice, but ... Can you write an entire program in one line?

(Obviously a joke. I've never written a program in one line, but I do respect the art of 5he craft.)

I know you are joking, but one-liners in Perl are a long-standing tradition, and also the way many people learn some of the language's intricacies. Once you have your prototypical one-liner down pat, you grab it and make it into a properly formatted sub in your source code.

For me, this is probably the main reason why I cannot abide space-sensitive languages such as Python.

>For me, this is probably the main reason why I cannot abide space-sensitive languages such as Python.

I completely understand you, I mean why would you ever want to use a _space_ when a perfectly functional and normal Tab already exists.

(This is a joke ... ait' I'mma Tab out~)

Except that you can't see easily whether you intended correctly or not, especially during editing the existing block.
Why couldn't you?
> just to make the whole "you don't need to say return in the last statement" look clever.

I sure prefer `let x = if foo { 0 } else { 1 };` over `let x = if foo { return 0; } else { return 1; };`. Which raises another potential dilemma: do you break consistency and allow returning values without `return` in if-blocks, or do you make it impossible to return from a function early within them, instead making the returns exit the if-expression early for ultimate confusion?

No, please don't take away my semicolons unless you provide an actual alternative, even if it's just a Lisp-like flood of parentheses.

I think the alternative style of semicolons are as separators which makes more sense from the grammar and is what many languages that are C-like do. Rust tries to be both and unsurprisingly ends up with complicated and IMO fragile rules.
It's not clear to me why it even needs to be treated as a special case. In particular, the biggest other example of a language that has this "return or hanging last expr" I'm aware of is ruby, where the problem with it is that accidentally returning a value from a block might affect escape analysis for GC, but rust doesn't have that problem.

In your example, the if is in expression position, so it's fine if it treats its bodies as expressions. I'm definitely not saying I want to pepper my code with returns, I'm just unconvinced semicolons help with avoiding that.

There are other options out there. Ada, for example, has extended return statements and - more directly - expression functions.
Why not do it like Ruby, where blocks always return the result of the last statement (and the return keyword returns from the nearest enclosing function)?
> ...would have been so much better if semicolons had been left out.

FWIW, I created a DSL where comma and semicolon are treated as whitespace.

> ...kebab-case or snake_case...

Crate names are case insensitive, right?

Maybe treat underscore and hyphen as different cases of the same delimiter?

> No support for using something like separation logic within Rust itself to verify that unsafe code upholds the invariants that the safe language expects.

I think this is something we might see in the future. There are a lot of formal methods people who are interested in rust. Creusot in particular is pretty close to doing this - at least for simpler invariants

https://github.com/xldenis/creusot

> Traits are hard to extend after the fact, and you can't break them into smaller parts in a backwards-compatible way. > 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.

This is definitely one of the main grievances I have with Rust. Overall, Rust can be a productive language but it's frustratingly limited when it comes to advanced types or really anything compile time. I read something a while back that made sense to me, to paraphrase "Rust solves problems by brute force".

The syntax for annotations on structs are also really distracting to me as well. (edited spelling)

> They syntax for annotations on structs are also really distracting to me as well.

I really like annotations on structs/struct fields, but I've never seen a syntax for it that felt quite right.

The ones that I've seen and come to mind are rust's:

    #[annotation goes here, always tokenized and usually strictly parsed according to some custom-ish dsl]
go's:

    "let's just use a string, parsing is often questionable"
and kotlin/scala/...'s

    @SingleIdentHere(Maybe, With, Arguments)

I'm curious if people have other examples, and/or a preference for one.
Scheme:

    (my-syntax-identifier-goes-here (we do not annotate)
                                    (we transform syntax))
Annotations are very useful, but they're pretty much ugly regardless of the language.

Though I like the sibling comment's Scheme version where it's postfixed. I've also come to appreciate Nim's sorta post-name annotation syntax since I can quickly read the name and type at the ends:

    type MyComponent = object
      position {.editable, animatable.}: Vector3
      alpha {.editRange: [0.0..1.0], animatable.}: float32
I was pondering if languages could provide CSS like annotation. Like

    #annotate: MyComponent.position { editable }
Hey, funny to see this old thing pop up here!

I don't really use this site any more, but thought I'd just pop in to remind people that these are my personal thoughts from last year... I think there are some things I would add or change now and as others have noted, 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.

It’s interesting someone found your random list and posted it lol
Please no more languages
I think there's actually a serious point here. As is so often the case, we should be unifying our efforts to solve common, pressing problems, not fragmenting our efforts over things that matter far less. Specifically, I think those of us that care about reliability and efficiency should unite behind Rust to provide a compelling replacement for C and C++ on the one hand, and (for some applications) higher-level but less efficient languages on the other. Rust is certainly not perfect, but if we succeed at making it a popular language with great libraries covering many application domains, it'll be better than what we have now.
I’d be inclined to agree with you but the same sentiment was expressed in the pre-Rust world, and the world is much better with Rust in it now.
Ah! Now I understand why that old tweet was being interacted with :)

I happen to agree with a significant portion of that list and some are, as you point out, being worked on.

- Raw-pointer equivalent to lifetime parameters (`GenericStruct<*>`, converts `&'a T` into `*const T` and `&'a mut T` into `*mut T`)

- `impl Trait` for associated types. This lets you workaround async trait functions without boxing the returned futures and also return anonymous closures from traits

- And with that, async trait functions

    trait Foo {
        async fn hello() -> Bar;
    }

    impl Foo for Baz {
        async fn hello() -> Bar { ... }
    }

    // desugars to

    trait Foo {
        type __Hello: Future<Output=Bar>;
        fn hello() -> Self::__Hello;
    }
    
    impl Foo for Baz {
        type __Hello = impl Future<Output=Bar>;
        fn hello() -> Self::__Hello { ... }
    }
- Limited support for `impl Trait` with different return types, by returning an enum on traits where all methods can be trivially implemented (see https://crates.io/crates/auto_enums)

- Explicitly assert trait variance, and unsafely override trait variance.

- Improved compile-time support. In particular, enough support so that you can write functions that generate other functions at compile-time, e.g. `const fn pull_factory(repo_url: &str) -> impl Fn(&mut Repository)`, `const pull_my_repo = pull_factory("https://github.com/my-org/my-repo.git");`

- Remove restriction on `Self` parameters in trait objects, or at least allow `self: &Rc<Self>` and `self: &Arc<Self>`, as `self: Rc<Self>` and `self: Arc<Self>` are allowed

- Allow generics in trait objects if the generic parameter is only accessed / returned behind references and raw pointers, as then the compiler should be able to implement by generating 2 specialized variants: one for thin pointers and one for fat pointers.

- More declarative macro support, e.g. more types of ASTs as metavars

- Partial borrows: https://github.com/rust-lang/rfcs/issues/1215

- Mutability variance: e.g. write `fn index<&a>(&a self, idx: Idx) -> &a T`, instead of needing to write `fn index(&self, idx: Idx) -> &T` and `fn index_mut(&mut self, idx: Idx) -> &mut T`)

- Built-in unit types (e.g. write `#[unit(ms)] pub struct Milliseconds(usize)`, and then you can write `128ms` => `Milliseconds(128)`)

> - Raw-pointer equivalent to lifetime parameters (`GenericStruct<*>`, converts `&'a T` into `*const T` and `&'a mut T` into `*mut T`)

How would that work? References and pointers are fundamentally different types, and the primary operation on references (`Deref`) isn't available for pointers. Any methods implemented on the struct wouldn't be able to do anything with the "reference" field, since it might actually be a pointer instead - it would be like having a `struct Foo<T>(Vec<T>);`, and then specifying `Foo<boxed T>` (imaginary syntax) if you actually want a `Box<T>` instead of a `Vec<T>`. You can't just treat a `Box` like a `Vec` (or a pointer like a reference).

> - `impl Trait` for associated types. This lets you workaround async trait functions without boxing the returned futures and also return anonymous closures from traits

You're probably already aware, but for the bystanders, that seems to be close to happening: https://github.com/rust-lang/rust/issues/63063

> - Mutability variance: e.g. write `fn index<&a>(&a self, idx: Idx) -> &a T`, instead of needing to write `fn index(&self, idx: Idx) -> &T` and `fn index_mut(&mut self, idx: Idx) -> &mut T`)

I think that's something the Keyword Generics (aka "totally not effects") Initiative[1] is looking into.

> - Built-in unit types (e.g. write `#[unit(ms)] pub struct Milliseconds(usize)`, and then you can write `128ms` => `Milliseconds(128)`)

Making sure that doesn't turn into a name collision nightmare would probably needs some work. It might also make it very hard to figure out what type you're actually dealing with just by looking at such a number. The current solution to this problem, defining an extension trait on integers, produces only moderately more verbose code, with the benefit of not requiring any additional syntax: `128.ms()`.

[1]: https://blog.rust-lang.org/inside-rust/2022/07/27/keyword-ge...

As a rust noob, I gotta say the biggest thing for me for entering the language is how strange the module system works. I still don't fully understand how importing a module works nor defining one.

It doesn't work like many other languages I'm used to.

I'm also just getting into Rust and I've loved the module system so far. I'm coming mostly from Java, Kotlin, Python, and TypeScript, which are all rather weak when it comes to modules. In comparison, I like that Rust treats modules as an organizational unit rather than just a side effect of file/folder structure, and I think they manage to do it in a way that isn't overly hard to navigate.
IMO the module system is explained very well in the book. Read it carefully once or twice, experiment a little, and you're golden.
A mod.rs is exactly like a __init__.py.

Except Rust removed a couple of annoying edge cases that are present in Python.

The best mental model is that `mod` works exactly like `fn`, `struct`, and `enum` — creates a new named item in its location.
what is stranger still is the compiler-generated symbols that mimic C++.

or that exception is weirdly handled.

Jury is still out on Rust being incorporated into the Linux kernel because of the above mentions.

Most of those things don't bother me much. Most are annoyances. The big problems I run into involve back references. I frequently want single owner objects with weak back references, and constructing those with Rc is excessively complex.

Error handling seems to have finally settled down. Mostly. It would be nice if everything had the same base type for errors. I use "anyhow" for everything, and often have to convert error types.

I have misgivings about trying to do everything through ever more complicated type abstractions. People get carried away with type theory.

I would love to see a simplified Rust with simplified types, a structural type system and algebraic types - like TypeScript but stricter
People often ask for "simplified x", but it's often not clear how they envision that. What would you remove from Rust to make it simpler?
The complicated stuff.
That's the issue. Different people see different stuff as complicated.
If we wanted to retain its use as a systems language I would probably mostly focus on improving the module system. Coming from higher languages, Rust's module system is very restrictive and forces applications to be written in monolithic files. Tests are often in the same files that the working code is in. I know the Rust community likes this, but it's something I have found has a negative impact on maintainability and organisation. I think TypeScript does an excellent job at managing modules.

If we look at the language with the goal of applying it to application development (web servers, GUIs) then I'd start with the simplification of the type system. There's no need for &str and String. There's no need for the various ways of describing accepting a function as a parameter or the need for types as structs - just have them as language built-ins. I would include algebraic types, structural typing and simplify the borrow checker using keywords like "read" "write" rather than & and &mut.

In fact, I actually wrote an entire language specification but am not smart enough to write a compiler. I'm still figuring out how to translate lifetimes

https://github.com/alshdavid/BorrowScript

>> 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

I've been wanting to ask Rust experts about this..... to what extent is this really necessary?

Is it possible to create a language with all the memory and thread safety of Rust, based on a single language with no sub languages?

I am not a expert, but I wrote things I am happy with and I think it is a little over the top. What is called the type language, the trait language and the attribute language here are just one thing for example. Those are not necessarily sub languages, they are different parts of the same thing. The hard thing is not the syntax or the way you write it, it is the concepts behind it.

The hard things about Rust are hard, because they are novel concepts that solve existing, hard problems in often very intelligent ways, while still upholding the guarantuees the language gives you. Granted, in C there would be less language to learn (although getting the details about value conversion right can be hard) — but using the language at a level where you'd be able to utilize it safely at a level like that would take a few decades as well. Because now you have to uphold the guarantees. C has no concept of ownership and lifetimes for example, but if you use your pointers without having a mental concept of ownership and lifetimes you are going to introduce the next exploitable zero day.

That being said: You can also happily program without ever touching half of that list and you would still end up with programs that are fast, maintainable and compile fine. In fact I'd argue unless you are building some hardcore library where you have to do things in optimal ways while still getting the ergonomics you'd be totally fine with e.g. avoiding unsafe, macros etc. The language is flexible in that way, you can go extremely deep or stay on the surface (or even do both in different parts of your program at the same time).

The type system is something that you need to get used to if you come from dynamic languages, but once you grasp it, the guarantuees it provides really simplify a lot of things in your head. E.g. typically when a compile fails I would know right away where to look and how to fix it, even in big bespoke programs. It prevents a whole class of bugs that every programmer makes once in a while. But type systems are old programming concepts. Sure there is a certain way it is written and used in Rust, but the hard thing again is the concepts underneath (e.g. how a type system makes your variables map to memory and what cool tricks you can do with that).

The macro language is legitimately ugly and hard to read. I can spit out a macro definition, but later I will have difficulty understanding it
Maybe it is. But each of these perform a role that can’t be easily removed.

- unsafe. You could make a language that didn’t allow third parties to write unsafe code. Except there are a few cases where it’s necessary. Me personally, I’ve never written any unsafe but I have relied on well written, vetted implementations written by others (like the standard library Vec).

- compile time language. A language either needs run time reflection or it needs compile time meta programming. Rust chooses the bare minimum for a runtime so there are no reflection APIs. That said, there probably is space for more ergonomic meta programming APIs. It still wouldn’t look like regular Rust, but it could be easier to read.

I don’t know what they mean by expression language. And I thought the type and trait language is the same? Calling these “languages” seems excessive to me, but sure.

I think it’s almost certainly possible to improve on Rust, either in place or by creating a new language that makes different trade-offs. Maybe that one would provide run time reflection, for instance.

I guess that by “expressions language” the author means the language used for expressions and statements (the latter also being expressions in Rust).
Yeah, I would just call that Rust I guess.
I think it's more a reflection of how Rust evolved, and the techniques and approaches known and understood at the time and the strangeness budget they were (understandably) willing to take on at the time as opposed to something inherent. And also sometimes having separate, complicated features for similar things (as opposed to simple, generalised features that compose powerfully) can be useful pedagogically as well.

At any rate, this is something I'm personally interested in based on my experience working with Rust over the last decade, and so that's why it appears so high up on my list. Often you really do want sub-languages for different purposes, but managing how they interact and work together, what is the same and what is different, and how that impacts usability is interesting (and difficult) part. I feel like it should be possible to do this, but it's going to take some work and there's still lots of unknowns.

In technical terms, I'm interested in dependently typed module systems, multistage programming[1], graded modal type theory[2], elaborator reflection, and two level type theory[3]. These all sound pretty intimidating, but you can actually see glimmers of some of this stuff in how Zig handles type parameters and modules, for example, something that most programmers really like the first time they see it!

I do feel like there is the core of a simple, flexible, powerful systems language out there... but finding it, and making it approachable while maintaining a solid footing in the theory and being sensitive to the practical demands of systems programming is a nontrivial task, and many people will be understandably skeptical that this is even a good direction to pursue. Thankfully the barrier to entry for programming language designers to implementing languages in this style has reduced significantly in just the last number of years[4], so I have hope that we might see some interesting stuff in the coming decade or so. In the meantime we have Rust as well, which is still an excellent language. I'm just one of those people who's never content with the status quo, always wishing we can push the state of the art further. This is why I got excited by Rust in the first place! :)

[1]: https://github.com/metaocaml/metaocaml-bibliography

[2]: https://granule-project.github.io/

[3]: https://github.com/AndrasKovacs/staged

[4]: https://github.com/AndrasKovacs/elaboration-zoo/

I think you share a lot of my interests on the theory side of things. I also share your belief that those areas probably can allow for the development of the core of systems level programming language. Of course, I tend to find that graded, or the more general contextual modal type theory subsumes multi-staged, two level and grade modal theories. I also think a broad spectrum dependently typed language (with modal enforcement of computational irrelevance) can take care of dependent modules.

I think you are also spot on about Rust having a strangeness budget and that could be responsible for the syntactic state of the language as it exists today. I have a much higher tolerance for non-conventional syntax so almost all of my type theory implementation and PL work has been outside the normal syntactic bounds for the last 2 years. I doubt I ever produce a language that is public, let alone a language that is used by any portion of the software engineering field. But my belief is that this arena is fertile ground for a more fundamental core, like you mentioned.

As an aside, I happened to stumble upon Andras’ video presentations on YouTube on Saturday and flagged them to watch and bookmarked the repo for them earlier today. So bravo on linking what look to be really nice resources for this area of work.

Of all those the only one that bugs me is macros. They are legitimately alien to the Rust language and remind me of Perl (a.k.a. Herl). I do understand why macros are a sub-language because they're doing something totally unlike what the main language does, but could the syntax have been less fugly?
The micro-syntax in attributes could have been avoided. There isn't any technical reason why:

     #[cfg(all(feature = "a", target = "b"))]
couldn't look more like the rest of Rust:

     #{ has_feature("a") && is_target("b") }

I'm not sure if declarative macros would have been better without a custom syntax. Procedural macros are just regular Rust, and they're verbose and hard to follow. In this case languages that aren't LISP just have to accept some trade-offs.

In case of safe Rust vs unsafe Rust vs compile-time Rust, I think they're fine. They're still using the same language with the same syntax. The specifics of what is allowed in safe/unsafe/const are different, but that's the point of these features.

Let's add (2021) in the title.

It is not super clear from the article that it is from last year, but if you click "revisions", you can see it's 1 year old. More over, author has written in comments that it is old.

This seems like a good time to remind ourselves...

"There are only two kinds of language: the ones people complain about, and the ones nobody uses."

I.e., evidence positive for Rust use.

I don't like this saying, because it doesn't distinguish between people merely disliking a language and the language having real warts. It can be used to justify bad design.
And knives can be used to cut yourself. Yet, we all use knives.

Every language becomes a legacy language before it achieves maturity. The next immature language is promoted as lacking all its flaws. But new flaws are always invented.

- People think it is hard to learn.

- People presenting it as something hard to learn.

simplest systems programming lang ive ever used

I feel the same! I've done very high level languages all my career, tried to understand C, but just felt super complex and over my head. Tried Rust, managed to get it enough to be able to build stuff with it in just a weekend.
What's the deal with people calling it the "Orange Site"? Some sort of lame injoke?
It's a website with a very marked orange header and some orange styling. Since there's very little else in the way of appearance (coming from me, that's a compliment!), the orange becomes quite prominent. Calling it "the orange site" seems very reasonable.
It's a "nickname" that people who don't like HN use.
You know how people call Voldemort "you-know-who" or "he who must not be named"? It's the same energy, but for HN.
Big one for me that stops me from using Rust like I would a scripting language for some tasks is the lack of an easy iterator/generator syntax.

Python and JavaScript make it dead simple to create iterators using generator functions, and in Python's case, generator expressions, as well.

For example, there are a lot of problems that are solved simply and efficiency by chaining generators together in pipelines. That's a task that's easy to tackle in Python and even JavaScript, but it can sometimes be awkward with Rust when defining iterators in the first place.

It's frustrating because Rust has a lot of features that make working with iterators nice. It's just a shame that they're awkward to create from scratch.

Are you familiar with `core::other::from_fn`[1]. This allows converting from `impl FnMut() -> Option<T>` to `impl Iterator<Item = T>`.

I recognise that these aren't strictly generators - you need to do your own state tracking. However, this does make it easier to do that ad-hoc.

I believe there is some work in the ecosystem abusing `async` to make fake generators, although I haven't used any of them.

[1]: https://doc.rust-lang.org/stable/core/iter/fn.from_fn.html

> `async` to make fake generators.

Genawaiter[0] is one of them.

[0]: https://github.com/whatisaphone/genawaiter

There's also an effort to get generators into the stable language, but it is currently gated on multiple open questions on the async iteration story, including what traits are to be involved (will it be `trait AsyncIteratorItem`? will it wait until GATs and/or async fn in traits and "keyword generics"/"maybe async" functions?), but you can see some explorations of the syntax aspect at https://github.com/estebank/iterator_item/

The proc-macro is written in such a way that it tries to parse as much as possible, so that you can try things out mixing and matching from the different alternatives that I've thought of: https://github.com/estebank/iterator_item/tree/master/tests

Like for example:

    fn foo() => i32 {
        for n in 0..10 {
            yield n;
        }
    }
    fn* foo() -> i32 {
        for n in 0..10 {
            yield n;
        }
    }
    gen foo() -> i32 {
        for n in 0..10 {
            yield n;
        }
    }
    fn* foo() yield i32 {
        for n in 0..10 {
            yield n;
        }
    }
One thing I love about these is how trivially easy it is to turn an Iterator<Item = Future<Output = T>> into an AsyncIterator<Item = T>: https://github.com/estebank/iterator_item/blob/b79d7b496687b...

    async fn* into_stream(input: impl Iterator<Item = Interval>)
        yields Interval
    {
        for i in input {
            yield i;
        }
    }
I'm aware of it, I just find it awkward for some tasks that would be easily expressed using generators in Python or JavaScript.
Disclaimer: I have been (re-)learning Rust recently through some toy projects.

> Type aliases are transparent (as opposed to abstract) by default, exposing their definitions publicly.

IMHO this hurts a lot because the main alternative, which is the "newtype" pattern (`struct NewType(OldType);`), introduces a lot of friction. Yes, you can `derive`, but it usually takes a lot more to reach "feature parity" with the original type, with only the undesirable parts swapped out. There are libraries to help: `derive_more`, `derivative`, etc. but the qualities vary, you need to find the correct library in the first place, and even then there's no guarantee they play nice with each other...

> conversions: From, TryFrom, Into, TryInto, As, AsRef, AsMut

This makes the 1st point worse TBH. With a myriad of newtypes and typedefs, it soon becomes confusing what can convert to what, through which trait or direct methods.

I never quite understood why this is the way it is - why is the paradigm

    impl TryFrom<&T> for U 
instead of

    impl Into<Result<U, E>> for &T
Why make a separate trait? And why make the blanket impl from-based rather than into-based?
Because Rust has restrictions on what can be impl'd to containing at least one local type/trait.

    impl Foo<X> for Y {}
         ^^^---     ^ one of these two has to belong to your current crate
            |
            this part doesn't matter
If you tried

    impl ForeignTrait for ForeignType {}
You get a compile error, hence why you can sometimes hear people suggesting using a new-type

    struct LocalType(ForeignType);
    impl ForeignTrait for LocalType {}
                          ^^^^^^^^^ this is a local type, so it compiles
This restriction is to avoid allowing semver footguns, where changes in a dependency of a dependency can cause your current crate to stop compiling after a seemingly innocuous upstream change.

With that context

    impl Into<Result<U, E>> for &T
         ^^^^                    ^ could be foreign?
         |
         foreign (from std::)
If you want to provide your users with a way to call .into() on a type you provide, that can be converted to a type they wrote, you need to be able to express the opposite construct, hence `From`

    impl From<Foreign> for Local
         ^^^^ -------      ^^^^^ local type, so the impl is allowed
         |    |
         |    foreign, but it doesn't matter
         foreign (from std::)
This is a fantastic explanation.
Good list. Hard to argue with most of it.

But please don't use kebab-case for crate names. It causes a stupid paper cut where you have to magically know that the crate name is auto-converted to snake_case in the code.

Really you shouldn't use kebab-case anywhere. There's too much risk of it ending up as a variable name and (unless you use a niche language) having to be converted. CSS also made this mistake.

As a Rustecean. We made it boys, people are complaining about Rust (and not just the borrow checker).
I don't understand this one :

> No space before the colon in type annotations in the default formatting.

Why do people like that kind of syntax? I know it's more symmetrical, but what personally bothers me is that it's grammatically incorrect. We're already used to placing colons right after words, so why change it?

Ah! that's a really good question. No whitespace before the colon in type annotations is my own personal pet peeve – the hill I'd die on – so it seems like my duty to answer.

I think of it this way: there are two symbols that by coincidence are written the same way (two dots aligned vertically). I mean that genuinely (I think of them as separate things), but it also helps as an explanative device.

The first I'll call the prefatory colon. Its usage matches usage in English [1], where the text preceding the colon introduces the text after the colon. For example, it is used to start a block (as in Python), or to create a key-value pair (as in Python), or define a member of a struct with a name-value pair (as in C or Rust).

The second is the type annotation. This syntax is borrowed from mathematics (notably, type theory). It is a binary relation, and binary relations are always written with equal space on the left and right. Just as you'd never write `x= 1` (another relation), `x> y` (relation), or `x+ z` (operator), you'd never write `x: X`.

Whenever I see "a: b", I immediately think prefatory colon. Despite having seen it hundreds or thousands of times in other people's code, it always trips me up when it turns out to be a type annotation, which has to be deduced from context or the right-hand side.

Truthfully, I'm somewhat baffled how it would ever even cross anyone's mind when designing a language to write type annotations like `x: X` given the long-established, pre-existing precedent in mathematics and the way its semantics seem backwards (if you had to use a prefatory colon, `X: x` would make much more sense to me).

[1] https://en.wikipedia.org/wiki/Colon_(punctuation)#Usage_in_E...

I'm having a bit of trouble understanding a few points, maybe someone can help me out.

> Traits bias the Self parameter, which can make some multi-parameter traits rather odd.

Can someone show me an example? What does bias mean here?

> Type aliases are leaky, and can expose implementation details via definitional equality.

What is definitional equality? What does an abstract type alias look like?

Traits are designed in such a way that there always is some privileged “receiver” type. In abstract datatypes, ML modules, and type classes there isn't this bias. In an ADT you can have a number of associated abstract datatypes, and in type classes all the 'parameters' are treated equivalently (other than order of the arguments which is a little annoying). To compare:

    module type Add = sig
        type lhs
        type rhs
        type out
        val add : lhs -> rhs -> out
    end

    class Add lhs rhs where
        type Out
        add : lhs -> rhs -> Out lhs rhs

    trait Add<Other = Self> {
        type Out;
        fn add(self, other: Other) -> Self::Output
    }

The way Rust does it is nice for more OOP-style usages of traits, but is annoying if you go outside of that (like with the operator traits). This is a personal annoyance, and would have other knock-on effects if it were changed, but does annoy me sometimes, hah.

> What is definitional equality? What does an abstract type alias look like?

Sorry, type theory lingo. Definitional equality means (in the way I was using it - there's some subtlety) that when comparing two things, you look at the definitions, potentially performing some computations to simplify things down. For example, if I had:

    pub type Id<T> = T;
    pub type Pair<T, U> = (T, U);


    pub fn foo((x, _): Id<(i32, String)>) -> i32 { x }
Then I can supply a `y: Pair<String, i32>` to `foo` from another module and everything will be ok, because Rust computes the underlying type when comparing the type of the argument with the type of the parameter, based on the definition of the type alias:

    use stuff::Pair;

    fn weird(pair: Pair<i32, String>) {
        dbg!(stuff::foo(pair))  // Rust checks: Pair<i32, String> == Id<(i32, String)>
    }
If however, I wanted to hide the contents of the type alias, but only expose its signature, I'd be out of luck. In other languages you can have "opaque" or "abstract" type aliases that don't reduce definitionally outside the module where they were defined. Granted, in writing this now, you could use a "newtype" struct for this, and that probably works better with traits, but yeah... I think I recall running into other weirdness related to this stuff but can't remember off the top of my head.
> 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.

This one is getting fixed by removing type ascription - https://github.com/rust-lang/rfcs/pull/3307.

I wouldn't call this "fixed". It's more like the worse-is-better people won another round.
I think the Rust language is amazing (I mostly love the predictibility that it gives me and that the compiler requires me to think about how I access data that needs to be used in multiple places). I wish some features, like macros wouldn't be overused in the code base I'm working on (the danger of advanced features is overuse), but I accept it.

Where it really lacks compared to C++ is tools: the debugger on my M1 Mac just doesn't show some variables, and the only refactoring in VS code breaks the code (extract function). Even renaming a variable doesn't work always well.

I wish there would be advancement in tools compared to older languages (edit and continue and reverse debugging should be a requirement in any modern language in 2022), but it feels like we're going back in time.

I like Rust, but this guy is not wrong. The one thing that always irks me is that closures seem like they are kind of a side-hack, almost an afterthought. Why not just have them use regular function syntax?