A personal list of Rust grievances
gist.github.com
gist.github.com
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.
I happen to agree with a significant portion of that list and some are, as you point out, being worked on.
> 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.
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.
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.
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.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.
1 +
2
is a syntax error, you need to make it 1 + (
2)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 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.
foo.bar()
.1 + baz()
parses to foo.bar().1 + baz()
^^ = undefinedfoo.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.
Oops, you're right.
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]).
That would have stopped typo squatting much better than making names insensitive to all kinds of weird conventions.
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 ( ... ) { . . . }
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.
if
{
// ...
}
else
{
// ...
}
but the compiler doesn't let you.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.
(Obviously a joke. I've never written a program in one line, but I do respect the art of 5he craft.)
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~)
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.
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?
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.
It depends on your locale:
$ ls
asdf Cargo.toml
$ locale
LANG=pt_BR.UTF-8
[...]
$ LC_ALL=C.UTF-8 ls
Cargo.toml asdfWith 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
> 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.
hold_my_beer {
…
}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)
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.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 } (my-syntax-identifier-goes-here (we do not annotate)
(we transform syntax))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?
#[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.
- 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.
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).
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/
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.
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
> 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.
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? 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 one is getting fixed by removing type ascription - https://github.com/rust-lang/rfcs/pull/3307.
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.
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
Genawaiter[0] is one of them.
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 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.
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.)
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.
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.
It doesn't work like many other languages I'm used to.
Except Rust removed a couple of annoying edge cases that are present in Python.
or that exception is weirdly handled.
Jury is still out on Rust being incorporated into the Linux kernel because of the above mentions.
- People presenting it as something hard to learn.
simplest systems programming lang ive ever used
"There are only two kinds of language: the ones people complain about, and the ones nobody uses."
I.e., evidence positive for Rust use.
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.
> 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?
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...
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.
- `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)`)
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...
> 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?
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.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
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.