Rust's Ugly Syntax
matklad.github.io
matklad.github.io
Rust spent enough of its weirdness budget on new semantics. I may not be a fan of C++-style syntax, but if your goal is to specifically tempt C++ developers with the promise of safer/saner language semantics, then making syntactic concessions towards familiarity with C++ is the right call. Rust isn't trying to be The One Language To Rule Them All, it's trying to target a very specific domain.
I think the strong counter is that you only really learn a language once, but Swift is a bridge from those coming from Objective-C not C++ and tries to hit other language goals so it’s hard to compare. Certainly this is the first real language giving c++ a run for its money in a way that nothing else has before, that’s not nothing, and it’s hard to argue whether the syntax chosen is part of that success or in spite of.
That's why the semantics are so different. It's more of the lineage that began with ML in 1973 than of the ALGOL 68 lineage like C is.
The Rust compiler was even originally written in Ocaml before it became self-hosting.
That's also where syntax elements like these come from:
- let for variable declaration and postfix type annotations separated with colons
- 'a (Ocaml's equivalent to <T> for generics, because, on an abstract/theoretical level, Rust's lifetimes are a special type of generic parameter, right down to "a" apparently being the habitual default type variable name, just like "T" is in C++)
- Option (option in Ocaml), Some, and None
- match
- fn (fun or function in Ocaml)
- -> and => (just -> in Ocaml) for function returns and match arms
- ()
I wonder how well that's worked out for them. My impression is that there's been a lot more people moving to Rust from Python/Ruby/Javascript than from C++.
Is there any reason in particular you think a lot of high-level language people are moving to Rust? That is not my impression or experience at all and I struggle to think of any reason to do so besides a large, individual change in career focus. As a higher-level language user myself, I can't think of a single reason I would ever use Rust for anything. Sure, I think it's cool, and it's always good to learn new languages, but in practise it's just not suited to my domain - and I think that goes for the vast majority of us.
I would say golang has scooped off more of the dynamic language types myself, although I also consider it a productivity downgrade and/or cargo cult to mostly be ignored without compelling reason. Which is even more the case with Rust!
I used to write primarily in highlevel languages, but if you want to write realy interesting high performance stuff in javascript, WASM+Rust is a really neat tech to use. Similarly, with WebGL/WebGPU you need some familiarity with bare metal programming.
I still write a lot of JS, but I regularly drop down to Rust.
Golang specifically has three things that turned me off as a replacement for Python in the early days before I got hooked on what Rust offers:
1. A disgust-inducing level of boilerplate use for things like error-handling and poor support for metaprogramming. 2. A design that not only lacks things like PyO3, but is implicitly hostile to FFI. 3. Git repository URLs as dependency references and "you figure it out" for ensuring they don't go away.
There's a lot more Python/JS/etc developers than C++ developers, and Rust meaningfully offers something those languages don't, so I suspect you see a rush of folks from that world going to learn Rust and posting about it online.
However, I generally don't see or hear about Rust being used professionally except in a context where C or C++ would have been seriously considered as well.
In the meantime, chasing C++ programmers introduced problems. Template heavy code means that IDEs can't follow it well and there's problems with compilation time. C++ programmers never were keen on GC so D tried to introduce GC-free semantics for the language which aren't complete.
I don't think C++ programmers in general are worth chasing. If they stuck with C++ for such a long time, it means they are used to it and accept C++ with all its flaws. It's also my impression that Rust is attracting more JS programmers than C++.
This is also partly because there was never a language that fit the niche where GC isn't a good fit that brought enough to the table in the eyes of that space. Even today some improvements like being able to avoid UB or seemingly high level features like pattern matching are looked upon with suspicion.
> It's also my impression that Rust is attracting more JS programmers than C++.
I think this is a testament to how successful the efforts to bring high level constructs to the systems space have been, as well as the ongoing efforts to make the system level features be ergonomic and usable by as many people as possible.
If you have a theoretically perfect language that only three high priests in an ivory tower can use, then you don't have what Rust is trying to be.
People don’t use C++ because it’s convenient and easy, they use it because it’s one of the best widely used options/ecosystems if performance really matters.
Any other language targeting this space has to chase C++ users because C++ is so prevalent. It would be like coming up with a new general purpose dynamic language and not appealing to Python users, or a new stats modeling language and not appealing to R users.
let foo.iter().map(|x| x*2).collect::<Vec<_>>()
If they'd used something like [T] for generics, then there'd be no collision with the < and > operators needing disambiguation in some contexts.
They even have a Book of Mozilla-esque entry in their test suite named "Bastion of the Turbofish" to pin down an example of the problem case, complete with
https://github.com/rust-lang/rust/blob/43192ca3d72ed0ca42ba9...
https://play.rust-lang.org/?version=stable&mode=debug&editio...
Welcome to Lisp! 'x means (quote x)
'(+ 1 2) evaluates to (+ 1 2)
Personally, while I hear you that :: is ugly, I do really like being able to distinguish module paths from value paths at a glance. And when looking for an alternative, we once again run into other characters already having far more familiar conflicting uses, or just being too weird. (E.g., # or @ could have been used, but that's probably worse than ::.)
Vec⟪u8⟫
Vec「u8」
Vec⟦u8⟧
Vec༼u8༽
I'm sure this could open up a whole new frontier of exciting, untypeable language designs. If it works for APL, it should work for any language![1] https://homes.luddy.indiana.edu/samth/fortress-spec.pdf#page...
[2] https://nim-lang.org/docs/manual.html#lexical-analysis-other...
If you broaden your viewpoint, [] is the same thing for arrays and generics. A generic function is a family of functions parameterized by types; an array is a family of values parameterized by 0..n; [] selects one member of the family. In fact in math subscripting is already a common notation for the parameter of a generic.
Same for paths using dot, it's just member access on a module object. In languages like JS or Python it's literally just normal runtime member access. In Zig it's also just normal member access on a comptime object.
Seeing it in code all over (as a beginner…I’m sure after a while you get used to it), it screams danger to me every time I encounter it in code.
However, after over a year of coding Rust every day, I now really appreciate the distinction. It's an effective way to give a meaningful context point at a very low cost.
I was wrong again! Which has been a consistent pattern with my initial Rust appreciations.
The only piece of surface-level syntax that I'm really truly bothered by is the closure syntax:
fn_with_a_callback_1(|param| param + 2);
fn_with_a_callback_2(|param| {
println!("{}", param);
});
- The fact that the | | is the same character on each side- The lack of directionality from the args -> to the body
- The lack of any kind of overlap with the normal function syntax fn foo()
Especially compared with something like JavaScript which has a really lovely lambda syntax, this one's hard to look at. I'm sure there were good reasons around ambiguity with other syntaxes or something, but... eesh.
I think most of what people call C's ugliness is actually just C's power. Some of it is C's age: we can't blame C for being born in the very early 1970s (when sensible ISAs were just born), but when you update C, don't try to make it be something that it's not. You see the beauty of UTF-8? People who could not have invented UTF-8 can at least see the beauty of UTF-8. Unfortunately, people who could not have invented C, are updating C.
When you try to update C, not only don't wreck it, don't try to replace it with something emo, weak, and overwrought, which is what Rust is.
C is powerful, natural, and concise. It doesn't wipe your ass for you. Wiping your ass neatly is not something you want to entrust to a bulletproof syntax, not when you can always just wash your hands. Rust? Rust has the color of not washing your hands because Rust imagines that it made it unnecessary.
squares := numbers.
filter(func(x int) bool { return x >= 0 }).
map(func(x int) int { return x * x })
With what I understand to be your suggested Rust syntax, this would still me: let squares: Vec<_> = numbers.iter()
.filter(fn(x: i64) -> bool { x >= 0 })
.map(fn(x: i64) -> i64 { x * x })
.collect();
which is a lot of noise compared to let squares: Vec<_> = numbers.iter().filter(x => x >= 0).map(x => x * x).collect(); let squares: Vec<_> = numbers.iter().filter(fn(x) { x >= 0 }).map(fn(x) { x * x }).collect(); squares := numbers.
filter(func(x) { x >= 0 }).
map(func(x) { x * x })
let squares: Vec<_> = numbers.iter()
.filter(fn(x) -> { x >= 0 })
.map(fn(x) -> { x * x })
.collect();
You don't necessarily need new syntax for this, although it may help with clarity.The Rust => code is still a bit shorter, but not much and I don't think it matters much.
let squares: Vec<_> = numbers.iter().filter(x => x >= 0).map(x => x * x).collect();
is not valid Rust, but this is: let squares: Vec<_> = numbers.iter().filter(|&&x| x >= 0).map(|x| x * x).collect();`|arg| arg*2` and also `|arg| { arg * 2 }` I'm not really a fan.
Well yes. Turns out beauty is not just skin deep. Semantics actually matter. You can't make a beautiful language by slapping some magical surface syntax on top of any language you like.
Once the syntax sinks in, it's significantly easier to read than most other mainstream languages because there are significantly fewer rules/concepts/themes to comprehend.
As an extremely simple example, after learning what this does:
let a = loop { break 1; };
you can bet the programmer can figure out what's going on here: let a = if true { 1 } else { 0 };
A language serves three purposes: it must be easy to write, it must be easy for a human to read, and it must be as unambiguous as possible (so that a computer can understand it). From a purely syntactical standpoint, Rust does great on all three. Most modern languages barely manage two.* Form follows function. *
(0|1)*
There: now you know everything from SVR4 UNIX executable format to H.264 video wrapped in MP4 container! Go forth and decode ...
You might as well say what the second line of code does - will save me time replying again ;)
So (I guess) both examples set the variable to 1.
That logic of the “break” keyword given a parameter is unusual compared to to other languages.
Maybe it was a bad example to use.
A nitpick. Unambiguity (and other language features) is extremely important not that a computer can understand the language (it can), but rather that the understanding (or interpretation) of a program by programmer and computer are the same.
Bugs like off by one, use after free and similar issues quite often stem from this misunderstanding.
"Haskell flavored Rust" anyone?
read
:: forall p
. AsRef Path p
=> p
-> Result (Vec u8)
read path = inner (as_ref path)
where
inner :: &Path -> Result (Vec u8)
inner path = do
mut file <- File.open path
let mut bytes = Vec.new
read_to_end file (&mut bytes)
pure bytesPersonally I think many styles, like the Haskell-y you show here, would be far more pleasing on the eye; luckily we live in a time where it is not too hard to create either a different compiler frontend, or, easier, a translator (transpiler, but I don’t like the word as it only translates, hence it takes too much credit being called transpiler).
inner path = do
Generally speaking, improved syntax would have been great, but similar projects that existed 10-15 years ago failed to be adopted widely (CoffeeScript, HAML).CoffeeScript can actually be considered very successful, since it influenced the design of a lot of other language features we take for granted. Same can be said for HAML.
I'd love to hear what features CoffeeScript made appear in other languages. JS did not take the syntax at all. I guess it's hard to tell whether generators and var-args came from CS or Python.
Also you don't need to take my word for it, Brendan Eich, the creator of JS himself has said that CoffeeScript had a significant impact on his thoughts about the future of JavaScript (https://brendaneich.com/2011/05/my-jsconf-us-presentation/).
Early Rust looked much more like a ML language. They intentionally moved to a C-like syntax because it is more familiar to their target audience.
I completely understand the reasons for Rust devs targeting a C like syntax. Rust had already blown its novelty budget on the borrow checker semantics.
So while I agree with some of the points of Rust having a tendency for hiding the what in all the how (and token salad), I think looking at the stdlib isn’t always a good example of idiomatic Rust.
But I do belive that rust is somewhat ugly and more complicated than C++ - for no reason other than a bad syntax design.
public func read(path: AsRef<Path>) -> io.Result<Vector<u8>> {
func inner(path: &Path) -> io.Result<Vector<u8>> {
var file = File.open(path)?;
var bytes = Vector.new();
file.readToEnd(&mut bytes)?;
return Ok(bytes);
}
return inner(path.asRef());
}
I have replaced pub with public, replaced fn with func, removed separate declaration for P, replaced :: with ., replaced Vec with Vector, replaced let mut with var, replaced snake_case with camelCase, added return keywords. Note that all these are syntax changes. Semantics hasn't been changed at all. One more thing would be to remove the semicolons..Compare that to the original. Which one is more readable?
pub fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
in Rust without needing to declare P separately.Here's a demonstration of the difference: https://play.rust-lang.org/?version=stable&mode=debug&editio...
fn poly(a: impl Display) {
println!("{a}");
}
instead of just fn poly(a: Display) {
println!("{a}");
}
Can I use same identifier for type and trait at the same time?* Is this function monomorphised for each type passed in or is the argument passed in as a trait object? If the latter, does it implicitly fall back to monomorpisation if the trait is not Object-safe(e.g. it takes or return `Self` by ownership)? How do we specify which of the two to use when we need to for performance reasons?
* Let's say we have a function with two inputs: `func foo(a: SomeTrait, b: SomeTrait)` How do I specify that both inputs must be the same type?
> Which one is more readable?
Not seeing any difference.
fn foo(x: u8) -> u8 {
let y = {
let z = something();
x + z
};
y
}
TL;DR: It's not a property of the function, it's a property of the curly braces. let (a, mut b) = //some tuple
You can't just replace `let mut` with `var` because it changes the semantics of the code. public func read(path: URL) throws -> Array<UInt8> {
let file = try FileHandle(forReadingFrom: path)
let bytes = try file.readToEnd() ?? Data()
return Array(bytes)
}
Yes, the example above is not generic and not even idiomatic Swift, but it's a good glimpse into how Rust could look like.Hopefully someday Swift would become more safe, less tied to ObjC legacy and actually cross-platform. It's a shame that such a nice language is basically Apple-only.
But sure, there are a lot of interesting languages out there. I would love to use something other than Rust, but there are simply no choice.
use std;
use std`fs`File;
pub fn read<P: AsRef<Path>>(path: P) -> io`Result<Vec<u8>> {
fn inner(path: &Path) -> io`Result<Vec<u8>> {
let mut file = File`open(path)?;
let mut bytes = Vec`new();
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
inner(path.as_ref())
}TFA highlights it well:
> It is needed because Rust loves exposing physical layout of bytes in memory as an interface, specifically for cases where that brings performance.
Rust (and its syntax) is designed for precision and performance and explicitness. That's great for the parts of your code that are extremely performance sensitive.
However, for most of our code, we need flexibility and high-level readability a bit more... a bit of a mismatch with some of Rust's decisions.
I like C#'s approach here. By default it decouples away all of these unnecessary low-level decisions, and lets you focus on what the code is actually trying to do. Then, if you want to do performance-sensitive work, you can use things like `struct`. I wish it went a bit further, but it's pretty nice.
But if I read correctly what you wrote, that's not just syntactic, right?
And no, I don't mean it as having java-like semantics. Semantically such functions should stay generic over the types. There's no reason not to sugar it over, though
Since Rust 1.26 you can write this:
fn foo(x: impl Into<String>, y: impl Into<String>) {
...
}
Which is almost exactly the same as this: fn foo<T: Into<String>, U: Into<String>(x: T, y: U) {
...
}In general I'm with you: just stick the .into() in the caller.
But this is pub std code, expected to be called from many many places, so they probably made the right choice to make it more convenient to call.
* horrible syntax
* crates.io is a graveyard of typo-squatting and lacks quality control, and no vendors / namespaces
* slow compilation
* standard library doesn't have HTTP, requires third-party libraries that are still immature / suck.
* widespread & prevalent abuse of .unwrap()
* error handling sucks because rust lacks set-theoretic types
* rust's async doesn't mesh with structured concurrency
* rust's async requires third-party libraries like tokio
highlights of rust
* pattern matching + ADTs
* move by default
* affine types
* concurrency & parallelism are easy
* serde crate
* kube crate
* rayon crate
> * widespread & prevalent abuse of .unwrap()
[citation needed]
> * rust's async requires third-party libraries like tokio
As opposed to baking Tokio in and having people lament that they can't replace the executor for stuff like embedded use-cases?
I remember hearing that there's interest in using async/await inside the Linux kernel with a custom executor.
Basically they're criticizing, in a roundabout way, people who criticize Rust for it's syntax by showing that such people don't actually understand Rust.
This rust code reads like there is a lot to learn about rust. This is a big barrier.
Rust is high level value semantics running on bare memory kind of thing. It's meaningfully different and useful.
Code aesthetics is directly related to its readability and to programmers quality of life.
Code should be made as simple to read as possible, the writing part is less important.
But the reverse seems more difficult to argue: that there is some ideal syntax that is objectively more reasonable than all other syntaxes. I have preferences (do/end blocks feel like unnecessary noise, I like my types to be written inline in my functions, angle brackets feel right for generics), and some of them even have a veneer of logic behind them (significant indentation is a nuisance when I want to paste a block of code that has the wrong indentation). But clearly other people have different preferences, because they keep on designing languages that don't fit my ideal aesthetics.
The other side of this is that the more I write different languages, the less important syntax becomes. The places where I appreciate the syntax of a particular language are usually because the syntax fits particularly well with the language's semantics, rather than because the syntax itself is good or bad. Partial application in ML-family languages is syntactically very neat, but I don't feel the need to remove parentheses from Javascript because of it. What I really like is partial application, and using space as the function application operator works well with those semantics.
Words are less ambiguous and cleaner.
In my experience, when I actually start getting to grips with a new language, the syntax very quickly becomes one of the least interesting things about it.
I submit for your consideration Brainfuck and Whitespace. There are others but these two came to mind first.
https://en.wikipedia.org/wiki/Brainfuck
https://en.wikipedia.org/wiki/Whitespace_(programming_langua...
Rust is a systems programming language concerned with several low-level details. Programmers aren't reading Rust code for entertainment, but need to be aware of things like ownership, mutability, lifetimes, trait bounds, dynamic dispatch, and so on. Beyond being eye-pleasing, Rust also needs to be locally explicit to communicate costs, limitations, and unsafety of the code.
As the article shows, beyond just bikeshedding which ASCII sequence you use for what, the aren't many ways to simplify the syntax that don't involve hiding information or changing language semantics that could worsen performance or create unpredictability.
And there is always a subjective part about any design decisions, but overall there are better designs and worse designs.
And concerning programming language syntax, I tend to prefer a bit of verbosity over any terseness / compactness.
I prefer Java/C# style over Perl/Sed. And I think that modern system programming languages tend to drift a bit too much towards the Perl style.
Writing powerful but unreadable on-liners should be considered harmful.
The syntax design of Rust is adding to the situations as well. For example, Rust is probably the only language that makes me cautious about the usage of semicolon, as you might have loads of unsound problems if you misused one.
My experience with Rust has been generally great, but I think these small details really deserves some thoughts from the Rust team.
BTW: I also wouldn't really trust those people who have to navigate through all the quirks of something, and then calling their experience "Great". There are people who memorized the entire `:help` book of `vim`, just so they can use the editor at hyperspeed instead of lightspeed. Those people are monsters, they can eat RAM chips and then spit out the bits cache line by cache line. You give them a grab of sand, they'll make a computer out of it and play DOOM on the computer. So of course those people well have great experience on almost everything (Or they just been polite, like me)
But the ending "much better" isn't really addressing the actual question of whether there is a way to improve syntax while keeping all things that need to be addressed instead of dropping them one by one like the post did.
In general, I don't love Rust's choice of sigils (::, <>, etc), but the language reads well enough. That inner/outer function thing is legit the compiler model leaking into user code though.
Note that relying purely on whitespace significance is particularly problematic for expression oriented grammars like Rust's, as opposed to statement oriented like Python's.
If you want to make a good faith argument against semicolon inference, that's not the way of doing it.
It's slowly becoming apparent to more people (e. g. compare the screeching a few years ago about Rust's wrong choice of <>/::<> generics to today), which I guess is why we see more of these huffy articles from Rust defenders.
A Rust function has to be generic and fast, it should absolutely use references and also Result types, some functional idiom and generally encode as much as possible in the type system. When other languages would settle for another if or an exception, Rust always has one more weird trick up its sleeve to use instead.
This is the curse of the bigbrain programmer wanting to use bigbrain concepts everywhere. Crate and library developers have internalized these lessons and it has become a self-fulfilling prophecy.
pub fn read(path: PathRef) -> io::Result<Vec<u8>> { fn inner(path: &Path) -> io::Result<Vec<u8>> { let mut file = File::open(path)?; let mut bytes = Vec::new(); file.read_to_end(&mut bytes)?; Ok(bytes) } inner(path.as_ref()) }
I don't see noise in the remaining code, like the author said, they all have meaningful semantic (besides the repeated return type).
But somehow, on HN, the article seems to have been taken literally with long threads about "::" v.s. "." and how < > are bad for generics.
Physics over optics, not everything is about cosmetics.
I dislike this, as even in C++, I like expected or StatusOr better than exceptions. I don't really think the Rust semantics for Result is that bad.
That said, if I were to fantasize about potential improvements, one thing I'd really like is an overhaul of error types in results. I wish there was some magical way that the error type could be inferred to be a union of all of the errors that can be returned. Most of the time, needing to explicitly do this manually doesn't add much value, and worse, it leads to much less precise error types as people reuse the same union type for functions that return disjoint errors. Unfortunately, there's no obvious way to make this work out, especially since it breaks Rust's rule about non-local inference, since the return type would be inferred by the function itself. Also, it would make life a lot worse for not breaking your API. But it would make the code a lot simpler and lower effort, that's for sure.
Perhaps another thing would be simply not needing to write out Ok(value), and just have an implicit conversion there, but while that could actually be done, it is also ugly for other reasons.
I don't see a problem at all with the ? syntax itself.
I know there's https://doc.rust-lang.org/beta/unstable-book/language-featur... so you can do e.g.
let foo: Result<T, Error> = try {
x()?;
y()?;
Ok(z)
};
but it hasn't been stabilized yet. The other workaround is to make an anonymous function and invoke it immediately, but sometimes you run into janky lifetime issues with that.You could use `Result<(), Box<dyn std::error::Error>` to use dynamic dispatch at runtime and allow the function to return any type that implements `std::error::Error`.
Otherwise people often use the anyhow[1] or the thiserror[2] crate to easily work with errors. They allow concise error propagation and quickly creating custom error types.
[1]: https://docs.rs/anyhow/latest/anyhow/ [2]: https://docs.rs/thiserror/latest/thiserror/
dyn Error would be fine by me performance-wise, but I do want to exhaustively know the exact possible kinds of errors a function can return statically at compile-time, and dyn Error thwarts that. It looks like anyhow Result is basically in the same boat, just with better ergonomics.
I know it took me a long time for that to stick.
FnMut: a function that can be called many times but can have side effects, because of this it must be exclusively held (&mut F)
FnOnce: a function that can be called only once, making use of by-ownership function calls
So I remember it like Fn: just a function FnMut: a function that needs &mut access FnOnce: only used once, moving ownership
Let's just go with a really contrived and stupid example... "pub fn". Would it really have killed them to spell it out? Why do we need 'fn'? Functions with a return type (i.e. `fn add(a: i32, b:i32) -> i32`) is just not easy to read. Yes, I'm aware that someone here will find this 'beautifully terse and concise', as well as easy to read. But without syntax highlighting, it's just a jumble of characters to me.
We read code more than we write, so clarity of code should be the goal of any 'modern' languages these days.
I do, in general, think keywords should not be abbreviated. We have "return" rather than "ret", "continue" rather than "cont", and so on. This wasn't always the case; ancient (pre-1.0) versions of Rust had a lot more abbreviation in the language and library than current versions.
We know that when people read lines of text, they pay a lot of attention to the beginning and the end and comparatively less attention to the middle. One result is that lines with common prefixes tend to blur together. To get around this, you want the reader to encounter the unique portion of the line ASAP, which makes it desirable that any kind of necessary prefix be brief.
Upshot is you want the function signature to be what you see as you scan down the left side of the page, so it does make sense to keep the function keyword particularly concise.
i32 means I'm doing FFI with C libraries.
If you check the Rust documentation, you see it has no integer types that are not a fixed size.
"All the world is x86/ARM (formerly DEC VAX / Sun SparcStation )"
C started on 18 bit machines. A program that correctly used "int argc" for the main argument count runs on an 18 bit machine or a 16 bit machine without modification.
I'm guessing you're too young to remember the 32 -> 64 bit transition, because this is backwards in practice. Non-fixed integer sizes are a nightmare for portability and the reason why some C code still hasn't been ported even today, whereas programs in e.g. Java or OCaml ran without modification on the new machines.
A modern language should make it easy and first-class to use arbitrary-precision integers and decimals though.
Executor, Valgrind ...
(This one: https://en.wikipedia.org/wiki/Executor_(software) )
MC68K MacOS software running on x86 via executor has not been "ported", even though it is transpiled to x86 code.
That's approximately the same as Java portability. There is a Java virtual machine (whether interpreted or compiled). Programs are written to that, and the virtual machine semantics is what is ported.
The programs are running on the Java machine, just like the programs running under Executor are running on a 68K Mac (as far as they know).
The portable C program is a kind polyglot among more than one of these (possibly all).
I also like how Rust makes both signed and unsigned types equally easy to type, because I feel that a lot of people use signed integers where they should be using unsigned simply because it's easier to type "int" compared to "unsigned (int)". And if you absolutely need a machine word size dependent type in Rust, you do have usize, which is the equivalent of size_t.
I16 and I64 is more concise than short and long and it is immediately obvious what the number range is for a beginner. The only complaint regarding portability is the unstated assumption that every computer can only store two values per bit. A hypothetical QLC based computer might store a I64 number in 32bits.
unsigned char buf[1024];
and the index being used to walk over the buffer is uint16_t rather than just plain old int.When I look at Rust code and see all the 32 and 64, it's triggering my bozo switch.
It's clear who they are trying to attract, in any case.
Were you aware that, because people use `int` so liberally, the LLP64 model 64-bit Windows uses and the LP64 model Linux, macOS, and the BSDs use keep `int` 32-bit on 64-bit platforms? Hello, risk of integer overflow.
LLP64 even keeps `long` at 32-bit.
https://en.wikipedia.org/wiki/64-bit_computing#64-bit_data_m...
It turned out that the C approach was so bad for portability in practice that it de-facto standardized `int` as 32 bits wide.
Any programmer worth their salt knows this. If writing maximally portable code, they either stick to that assumption or else make some use of INT_MIN and INT_MAX to respect the limits.
The minimum range is adequate for all sorts of common situations.
Some 32 bit targets had 16 bit int! E.g. this was a configuration for the Motorola 68K family, which has 32 bit address and 32 bit data pointers.
See the "-mshort" option of GCC, documented here:
https://gcc.gnu.org/onlinedocs/gcc/M680x0-Options.html
> It turned out that the C approach was so bad for portability in practice that it de-facto standardized `int` as 32 bits wide.
And, so, Rust builds on that by renaming that to i32, to further entrench the poor practices.
Of course, you will not win any popularity contest by appealing to people who know what they are doing, who are the minority.
Rust is one big "C and C++ have proven that 'hire better programmers' isn't a viable solution". You might as well use the same logic to argue against C and C++ restricting and complicating assembly language with a type system and the removal of unconstrained GOTO.
> And, so, Rust builds on that by renaming that to i32, to further entrench the poor practices.
The rationale is that a program's logic should function the same on different targets, or fail with a compile-time error if that's not feasible.
THAT is why the default is to use fixed-size variables except for the `size_t` equivalent which is intended to fit any valid memory offset, and to encourage use of correct types for various applications by making it more bothersome to convert types. (eg. Does a file format use a 16-bit integer for an image dimension? Expose that in the parser's API.)
> Of course, you will not win any popularity contest by appealing to people who know what they are doing, who are the minority.
I dunno. I know what I'm doing, but I choose Rust over every other language because it's refreshing and de-stressing to teach the compiler to watch my back as much as possible.
Everyone has bad days, and distracted days, and tired days, and, unless everything you write is closed-source hobby code, you have to deal with code other people have touched.
> At the mention of ugly source code, people will of course think of
> Perl. But the superficial ugliness of Perl is not the sort I mean. Real
> ugliness is not harsh-looking syntax, but having to build programs out
> of the wrong concepts. Perl may look like a cartoon character swearing,
> but there are cases where it surpasses Python conceptually.
I do think there's something to be said for the notion of "conceptually ugly" rather than "syntactically ugly", though the latter is what people tend to focus on. Perhaps we need a better word than "ugly," with its superficial connotations.Interestingly, Python’s aspiration to have only one (obvious) way to do things could be understood as an aspiration to achieve conceptual beauty.
Harping on Perl again, one could see its roots in natural language with sigils representing different kinds of "nouns", and ability to rearrange conditions to be more like spoken language ("do X if Y vs. if Y do X") as an aspiration towards "beautiful to work with."
And of course the various notions of beauty can get in each others' way.
But they're all a lot more fruitful to consider than a mere "yuck, too much punctuation."
I just don't get why this even gives someone pause.
I don't understand this complaint on a statically typed language.
However rust has made the stance that the full signature is required for the reason of local static analysis. If you have a function A that calls another function B, you shouldn't need to analyse B in order to analyse A (both as a human and as a compiler)
The semantics are what matter (at least in most programming languages)!
To me, syntax wise, I just can’t stand inline type definitions. CrabML becomes close to what I can grok syntax-wise. I spent many years with C++ and I can read TyoeScript… but to me they’re examples of bad syntax, not role models.
However if you read A Logical Approach to Discrete Math or Programming in the 1990s you’ll know just how syntax matters… most programming languages can’t even reach this level of which is why I think semantics are more important.
Rust has to be learned, true, but once the initial learning phase is done, those syntaxes start making sense and become part of the value rather than the cost.
I would recommend putting personal aesthetic preferences aside while learning and reassessing them later.
Also, I hope the article's point was not lost. It took me too long to get it, but I am glad I did before I wrote my first comment on Reddit a few days ago.
It does make sense if you think of lifetimes as being part of a type system. Almost all lifetimes are generic (aside from 'static), so it kind of makes sense to put them in the section for declaring generic parameters.
It also works for how you constrain lifetimes: `'foo: 'bar` means that `'foo` outlives `'bar`, but could also be read as `'foo` inherits `'bar` because, like in type inheritance, a `'foo` can be used anywhere that wants a `'bar`.
Your interface with code is your eyes, so it can matter a lot how it appears. We read code more than we write, so writing for legibility is important. People talk about line noise. And we use syntax highlighters and monospaced fonts. All of this is for appearances.
Also, that was a nonsense metaphor for you to use. The grip being slippery is so far removed from “beauty” that not even trying to change the subject to be about “interfacing” can redeem it.
C++ is a slippery hammer, not because it’s ugly, but because it’s easy to hit yourself in the foot using it.
But that doesn't mean that you should be writing exceptionally opaque syntax. For example, in the given example, I would demand that <T: AsRef<Path>> be written with a where clause instead. rustc will accept either, but humans should demand `where` at least whenever the traits contain `<>`.
It's a bit of a straw-man, of course, but it's an interesting idea that made a couple things a bit clearer for myself.
And yes, these proposed syntaxes for Rust are not practical because Rust's support for macro definitions relies on the existing syntax. But if you're willing to reimplement any macros, it becomes viable.
X!(T)
X!T
are the same. This was Andrei's idea, and it worked out nicely.I suppose that dates back to my aviation experience, where if an airplane is beautiful it very likely flies well, too.
https://github.com/rust-lang/rfcs/pull/3267
I was against because I felt that there should always be parens/brackets/braces (macros can use any of them) for better scannability when you're invoking an unclear amount of extra stuff behind the scenes.
(An appeal to the "make costs explicit" side of Rust's philosophy that also requires explicit .clone() rather than copy constructors.)
Here's the summary of why the consensus was "no":
https://github.com/rust-lang/rfcs/pull/3267#issuecomment-128...
I don't find Rust's syntax ugly. But then I don't find Modern Art ugly as opposed to what many people think so well...
I jump into an unknown C++, or TypeScript / JavaScript or C# codebase and it's just readable and easy to understand. I read a bit of complex Rust and it's making me queasy.
> The next noisy element is the <P: AsRef<Path>> constraint.
Should be replaced with `fn read(path: impl AsRef<Path>)`
> We’d have to use some opaque Bytes type provided by the runtime.
A less joke example might be to have `pub type Bytes = Vec<u8>` as a secondary declaration.
I guess the rest of the post is just hyperbole. Next time, take an example that has a struct with a lifetime parameter implementing traits that have lifetime parameters. That's some ugly syntax!
> This is lifted straight from the standard library, so it is very much not a strawman example. And, at least to me, it’s definitely not a pretty one!
Reddit is atrocious for this and I suspect people don't want to see it get in the way of real discourse.
Just like building an app that functions well is different from designing an app that looks great.
We don’t expect every backend developer to be the best front end designer and at some point we shouldn’t expect every language developer to be the best language designer.
It might be worth treating these two tasks as separate disciplines that can be studied and practiced independently.
Otherwise, we keep getting languages that feel akin to 1990’s websites. They function great, but look as if the aesthetic is an afterthought.
pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
fn inner(path: &Path) -> io::Result<Vec<u8>> {
let mut file = File::open(path)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
inner(path.as_ref())
}
In D (I'm not a Rust expert, so apologies for inevitable mistakse): public io.Result!(ubyte[]) read(P : Path)(ref P path) {
io.Result!(ubyte[]) inner(ref Path path) {
auto file = File.open(path);
auto bytes = file.read_to_end();
return Ok(bytes);
}
inner(path);
}'read' taking 'ref P path' isn't entirely accurate. In Rust, 'path' is moved into the 'read' function, which destroys it after calling 'inner'. (If the caller doesn't want it to be destroyed, it provides a reference, i.e., a safe non-null immutable pointer; when 'read' destroys the reference, nothing happens.) As I understand it, 'ref P path' in D still gives the caller the ability to access 'path' after calling 'read' and the responsibility for destroying it.
Also, the '?'s are important for error handling: the 'File::open()?' line basically expands to
let mut file;
match File::open(path) {
Ok(value) => { file = value; }
Err(error) => { return Err(error); }
}
and similar for the 'read_to_end()?', except the result is discarded. And one of the reasons (apart from flexibility) that 'read_to_end()' takes an external vector as input is so that the caller can retain the result of a partial read; otherwise, the function would have to return a vector on error as well as on success.For example, D could would likely throw an exception, rather than use pattern matching, for error handling.
> and let some top-level
> try { } catch (...) { /* intentionally empty */ }
> Much better now!
NO
For example, in
pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
for me it's really unpleasant that there is so much stuff between the function name and the names of the parameters. It's a bit nicer with already-a-Rust-feature where-clauses like pub fn read<P>(path: P) -> io::Result<Vec<u8>> where P: AsRef<Path> {
but if nothing else I think that makes a mess of the indentation if you linebreak anywhere in that line.C++ avoids this by just yanking all the extra bits at the very start of the declaration, so we could copy that (without going all the way into C++ caricature):
for<P: AsRef<Path>>
pub fn read(path: P) -> io::Result<Vec<u8>> {
This addresses my specific complaint, but is probably worse for other people. Who knows.As another example, the whole business with explicit Result types and the early-return question mark is basically isomorphic to checked exceptions, which may or may not be syntactically lighter/more pleasant. Would Rust enthusiasts prefer that? Probably not. Would it make the language more approachable to people coming from some Java ecosystem? Maybe? Would it be worth it? No idea.
Marginally more advanced compilers will try to recover, get to the end with as much context as possible, and then complain, including maybe some extra info that came after that might be relevant to the error seen.