I can actually see where this is coming from, as I found Rust hard to read when I started out. I do really like Rust for reference, but I do agree Rust is hard to read for someone that has not learned it.
For example:
return statements that are implicit just because the semicolon isn’t there. Even better if they occur inside a if block or something like that. Very hard to understanding when you don’t know the syntax rules.
Lambda functions, especially when using move semantics too. They are quite simple, but if you don’t know the meaning, it’s more arcane characters. Especially when this is used inside lots of chained methods, and maybe a multi-line function in the lambda.
A lot for the if let x =… type of stataments are tough the first time around. Same for match statements.
Defining types for use with function::<Type>() and such.
Lifetimes, especially when they are all named a, b, c etc. It quickly gets messy, especially when combined with generics or explicitly defined types.
Macros, though not entry level rust to begin with, they are really cumbersome to decode.
None of these are sins of Rust, but for new people they are a hill to climb, and often hard to just “get” based on previous programming experience and reading the code. Rust can be really hard to approach because of these things. This happens in other languages too, but I do feel Rust has a particularly large amount of new concepts or ways to do something. And this is on top of learning lifetimes and borrow semantics.
This is the most sober take in this thread. I was bothered by all these things you mentioned for the first two weeks of using the language. I begrudgingly accepted them for the following two months because I felt the benefits of the language were worth it. Now all of these things feel natural and I don’t give them a second thought.
They might not be strictly language issues, but if they are symptomatic of idiomatic rust then they are “sins of rust”. Something about the language promotes writing it using these kinds of idioms.
Just like French speakers don’t pronounce 80% of the written syllables because it’s impractical to speak fast with all of them…language features (or lack of them) drive how the language is used.
(BTW the implicit return behaviour on a missing semicolon sounds like Chekhov’s footgun)
I’m not sure, what you mean by “Chekhov’s footgun”, but well, it isn’t a footgun, so you won’t accidentally return a wrong value from the function, if that’s what you’re thinking.
It’s not a Rust invention, most functional programming languages have implicit returns, but basically think less of them as a function return and more just as the value that a scope evaluates to.
So, here’s a simple example:
letsum = {
letx = 5 + 9;
3 * x
};
Obviously, this is an extremely contrived example, but yeah, as you can see, it does not even have to involve a function. The implicit return makes it so that sum is set to the result from 3 * x.
And the scope-braces are nice here, because you can do intermediate steps without having x in scope for the rest of your function.
In practice, if you see scope-braces and the line at the end does not have a semicolon, then that’s the value that the whole scope evaluates to. Those scope-braces can also be the braces of a function, but then you need to annotate what the function is going to return, too, so it’s practically impossible to return a wrong value.
Well, and I would actually argue that explicit returns are a footgun in comparison.
Because someone might introduce clean-up code at the end of the function and not realize that an explicit return skips that clean-up code, somewhere further up in the function.
The implicit return always has to be at the end of the scope, so it’s not possible to accidentally skip code.
Something about the language promotes writing it using these kinds of idioms.
As someone who has used Rust professionally for 3 years, the idioms are good. I wouldn’t want the idioms to go away, and I don’t particularly want the style/aesthetics of the language to change unless it can be done without negatively affecting the idioms.
It’s not a situation where the aesthetics are actually bad, it’s just different than what most programmers are used to, but almost all of the differences are for pretty good reasons. With enough familiarity and practice you’ll start to recognize those differences as benefits of the language rather than detriments.
But I think a lot of people have so much inertia about tweaking their way of thinking that they don’t feel motivated to try long enough to make it over that hump, especially when their expectations get set by rabid Rust promoters like myself who insist that Rust is vastly superior to any other languages in almost all situations. The stark contrast between how good they’re told the language is and how they feel when first exposed to it might be too much whiplash for some people.
I recognise that different languages have different styles, strengths and idioms. One of my pain points is when people write every language as if it’s naughties java. Enough with the enterprise OoP crap.
I’ve also learnt languages like Haskell to expand and challenge the way I think about software problems. I learnt a lot doing it. That doesn’t stop a lot of Haskell code looking like line noise to me because it over-uses symbols and it being close to impenetrable in a lot of cases when you read somebody else’s code.
I think the aesthetics of Rust are the wrong side of the line. Not as bad as something like Haskell (or Perl), but still objectionable. Some things seem to be different even though there’s pre-existing notation. Things seem to be dense, magical, and for the compilers benefit over the readers (as an outsider).
I’ve been learning Zig recently and the only notational aspect I struggled with was the pointer/slice notation as there’s 5 or 6 similar forms that mean fairly different things. It has other new concepts and idioms to learn, but on the whole it’s notation is fairly traditional. That has made reading code a lot more approachable (…which is a good thing because the documentation for some aspects sucks).
The implicit return is perhaps the most dubious of them. I don’t mind it for simple functions, but they are not so good in anything large and with multiple return sites. Even more so when developers choose to implicitly return 4 chained method calls inside a closure with else cases.
But the rest aren’t really sins, they are mostly new or different concepts or ways to do something. And if there is a sin, it’s largely because the language itself has a complexity larger than others.
Taking my own examples here, lambdas are just fine, but the move semantics are cumbersome to deal with. But we need to do it some way, to indicate that a value is actually being moved into the closure. Maybe there are better ways and they make into the language in the future.
Conditional values and let statements and such is a good consequence of Rusts design choice with returning Results or Option types. Just because it’s different doesn’t make it a sin. Just takes time to learn the right way. I think most come from an exception based language, and that has a differnet code flow than what Rust has.
Lifetimes are hard though, and I feel a lot of the introduction is made excessively hard with the simple naming. It’s as of every programming tutorial used single letter variable names. Lifetimes isn’t something I’m that good with either, mostly because I haven’t had to use that complexity yet.
Entirely readable to someone who knows Common Lisp, and unreadable to someone who doesn’t know any kind of Lisp. Mostly readable to someone who knows Emacs Lisp, Clojure, or Scheme.
Being able to correctly guess what the syntax does without knowing the language is a function of similarity to familiar languages more often than it is a characteristic of the syntax itself.
I imagine the tricky part for someone unfamiliar with Lisp would be that there’s no syntactic clue that a particular thing is a macro or special form that’s going to treat its arguments differently from a function call. Someone who knows Scheme may have never seen anything like CLOS, but would see from context that defmethod must not be a function.
You don’t even need to define a class to define methods. I’m sure that’s surprising to people coming from today’s popular language, but the original comment was about syntax.
Whether Lisp syntax is ugly is a matter of taste, but it’s objectively not unreadable.
In most languages, I would agree with that. In Lisp, I think I might not. If Common Lisp didn’t come with CLOS, you could implement it as a library, and that is not true of the object systems of the vast majority of languages.
Maybe Emacs has fried my brain, but that is perfectly readable. Common Lisp has one of the most advanced object systems around, so yea you can write hard to read stuff if you want
You can’t imagine how often I just sweared today about js. What did go through the mind of their designers, when they created this growing disease, and why did web browsers accept this as the lingua franca for the web. So… much… pain…
Actually, my (not that small) Rust projects now take officially less time to cold compile than the “hot” reloading of our next.js monster in my job. Incremental compilation is at least an order of magnitude faster. And cherry on top, dumb code is often 100x faster than js.
well if you are recompiling thousands of crates with a single thread, for a simple webapp no less, then you are doing something wrong. multiple things, actually, I count 3.
Definitely not your average Rust code, more like a very ugly example of it.
Also, as the syntax first put me off as well, I gave it a chance years afterwards, and have now (or rather years ago) officially joined the church of Rust evangelism.
A lot of the syntax you define as ugly makes sense when you learn it, it’s just so much more explicit than a more dynamic language, but that exactly saves your ass a lot (it did for me at the very least) (I don’t mean macros, macros are ugly and should be avoided if possible)
Sorry, I love Rust but I can’t really agree with you here. They only showed a macro_rules!definition, which is definitely rust syntax. Lifetime annotations are relatively common.
I will concede that loop labels are incredibly rare though.
Loop labels are rare, but they lead to much simpler/clearer code when you need them. Consider how you would implement this kind of loop in a language without loop variables:
'outer: while (...) {
'inner: while (...) {
if (...) {
// this breaks out of the outer loop, not just the inner loopbreak'outer;
}
}
// some code here
}
In C/C++ you’d need to do something like
bool condition = false;
while (...) {
while (...) {
if (...) {
condition = true;
break;
}
}
if (condition) {
break;
}
// some code here
}
Personally, I wouldn’t call it ugly, either, but that’s mostly a matter of taste
Well, you’d typically put the loops into a function and then do an explicit return to jump out of there. I believe, there’s some use-cases where this isn’t possible, which is why I’m cool with loop labels existing, but I’ve been coding Rust for seven years and have not needed them once…
You used macro_rules, which is not common at all. Most rust files don’t contain any macro definition.
This code doesn’t even compile. There is a random function definition, and then there are loose statements not inside any code block.
The loop is also annotated, which is not common at all, and when loops are annotated it’s a blessing for readability. Additionally, the loop (+annotation) is indented for some reason.
And the loop doesn’t contain any codeblock. Just an opening bracket.
Also, the function definition contains a lifetime annotation. While they are not uncommon, I wouldn’t say the average rust function contains them. Of course their frequency changes a lot depending on context, but in my experience most functions I write/read don’t have lifetime annotations at all.
Yes, what you wrote somewhat resembles rust. But it is in no way average rust code.
Perl is ugly but great. It’s like shell scripting with a more solid programming language. I’d never use it outside of simple scripts in my os, but whenever I do use it it’s very fun. Anyway, yeah, rust looks fine to me. Maybe they are not very experienced with it? I know some of my programs used to have lines with just x.unwrap().unwrap().unwrap() or whatever, which is not pretty.
I know some of my programs used to have lines with just x.unwrap().unwrap().unwrap() or whatever, which is not pretty.
That goes away with experience, though. At least, I can’t think of a reason why you’d nest three Results or Options. Normally, you would collate them right away.
The most you see in the wild is something like Result<Option<_>> to express that a check can fail, but even if it doesn’t, then a valid result can still be that there is nothing there.
If you don’t care that your program crashes (like .unwrap() does), then anyhow is the error handling library of choice. With it, you can just write a ? in place of an .unwrap() for practically any error type. And well, it automatically combines the errors, so you won’t be writing ??? either.
Enums and nested blocks. I understand the importance of Option and Result, but it’s fucking infuriating when I have to check and destructure the result of every function call and either bubble the result up the stack from six levels of nested iflet blocks or risk Cloudflaring my program by using .unwrap(). And while I like being able to extract a return value from an if...else expression, the structure gets really convoluted when multiple if and match blocks are nested (of course each one returning a value), and it gets completely fucked once closures are introduced.
Enums are the best part of the Rust language IMO, so I’m not sure how you can view them as ugly. Having the choice to destructure something is fantastic. You generally aren’t required to destructure every return value. Make sure you’re using the ? operator as much as possible. If destructuring is getting in your way, it sounds like the code is not very idiomatic.
I can’t really comment on your issue with nested if and match. Too much nesting is bad in any language; try extracting more functions and let bindings to make it more readable.
You can enable a clippy lint to deny .unwrap() if you’re worried about it.
But really it’s the exact same as other languages, it just forces you to handle it better. C-based languages will return 0/null/-1 and you’ll have to check all 3 of those because they might not mean the same thing. How is that better?
Most of the times you can just let ... else (which is basically a custom ? if you neediflet ... else it’s because you actually need 2 branching code paths. In any other language you also do if ... else when you have 2 different code branches. I don’t see why this is a rust-specific issue.
This isn’t about some feature of the language being good or bad. It’s about Rust being ugly or not. The things I mentioned will always look ugly in the source code.
It’s hilarious to me that people talk about “ugly” as if their opinions are objective.
I found Rust unpleasant to look at for the first two weeks of learning it, and now that I’ve been using it professionally for three years I loathe when I need to read code in other languages.
No other language can rival Rust in showing the exact information needed to understand the code — never too much and never too little — while being concise, correct, and handling all edge cases.
You can be more concise in other languages, but it will come the loss of handling every little possible bug. You can be prettier in other languages, but it will come at the price of adding a lot of useless boilerplate.
Of course there are cases where Rust can be verbose or confusing, but that’s when you’re doing very esoteric things that would be just as confusing in other languages.
Like any opinion on aesthetics, how someone feels about the prettiness of a language will have far more to do with familiarity than with any objective metrics.
Sadly there’s still no truly good way to handle errors in rust. TBH I’m not sure if we as an industry have figured out an efficient, foolproof, composable, not overly verbose way to handle errors, so it’s not entirely on Rust.
Rust allows you to choose whatever method you want.
Early return propagating the error
Early return ignoring the error (maybe by returning a default value)
Explicit handling by if-else (or match) to distinguish between error and not error cases.
Early return and turn the error into another type that is easier to handle by the caller.
Assume there is no error, and just panic if there is. (.unwrap)
There are only 2 error handling methods that you cannot do:
Exceptions
Ignore the error and continue execution
And that is because both of them are bad because they allow you to do the second one, when .unwrap is just there and better.
If your concept of “not ugly” is “I just want to see the happy path” then you either write bad code that is “not ugly” or write good code that is “ugly”. Because there is no language that allows you to handle errors while not having error handling code near where the errors are produced.
I am well aware, I wrote quite a lot of Rust, including professionally. I’m not necessarily talking about the “clear happy path”, even though that is relatively nice to have (Rust sort-of allows that with the ? sugar and the pseudo-functor and pseudo-monad methods on wrappers); I’m talking more about the fine line between the overly verbose lists of all the errors that a function could produce (thiserror-style) or just a single messy error type that makes error handling difficult (anyhow-style). There surely must be something in between there that is concise, type-safe, and composable, but I haven’t seen it in any language yet.
In my case, I don’t usually encounter cases where I can’t just ?. But when I do, just make an error enum (kinda like thiserror) that encapsulates the possible errors + possibly adds more.
On the call site, just convert to string if I don’t care about specifics (anyhow-style).
I don’t find this much painful.
Concise: not much on the declaration side, since you have to create an entire enum for each function in worst-case scenario. But on code side, it’s just .map_err(MyError)?.
Type-safe: can’t beat errors as enum values wrapped in Result.
Composable: i don’t think you can beat rust enums in composability.
I don’t use anyhow/thiserror, so I’m not sure. But I believe thiserror fixes the conciseness issue for this.
thiserror helps a bit with conciseness. But it’s still the most annoying part of writing Rust for me, even more annoying than async closure capture semantics, and one that I happily offload to deepseek whenever I have to write it. I was even thinking of making some insane proc_macro that would traverse a function and extract the list of all ?, deduce the types, and come up with the final error type (maybe with some hints). But this turned out to be too difficult for a weekend project and ain’t nobody wants to pay me to write it.
Type-safe: can’t beat errors as enum values wrapped in Result.
I’m talking more about the anyhow-style here. It’s not very type-safe.
Composable: i don’t think you can beat rust enums in composability.
Well, thiserror-style enums are composable-ish, but they can be too structured for their own good. Sometimes you want only a particular property of an error (e.g. one of the HTTP requests returned 429 or something), and don’t care about the particular branch of the call tree it came from. Rust doesn’t really have the compile-time flexibility to write such a check without a ton of boilerplate that will also break once the call tree changes.
Yeah, I was gonna say, error handling easily makes up 80+% of the code paths, and depending on whether you’re building a library or service or script etc., different strategies are most suitable for how to deal with those code paths.
In a script, you often just want it to crash. In a library, you want to make these code paths matchable, in case the user cares why something failed. And then you have the awkward in-between, which is that 99% of your application codebase will be used by your main-function like a library, but you don’t want to spend as much effort on error handling for that as a proper library does, in particular also because you know what all consumers of your application-library need to know.
So, it’s kind of multiple different problems, with overlap, and people are hoping for one easy solution to cover all these problems.
It’s incredibly disingenuous to call this average Rust code and further erodes your credibility. I may as well point to hundreds of lines of preprocessor macros in a C++ header and call it average C++ code.
This is not what an average Rust developer is writing 99% of the time. If someone on my team submitted a PR with an implementation of sum that uses macro_rules! I would almost certainly reject it.
Key point being, similar to some random languages. JS and Python Syntax don’t fit a typed and compiled language at all.
Pretty syntax would probably be something like C, where not every single character already widely reserved for specific keywords (like !, ', =) is reused in completely random ways.
Ah yes I also found macro syntax like vec
I believe that it is useful in a few places. cppreference.com mentions templates as one case:
The syntax also matches that of lambdas, though I’m not sure that adding another way of specifying regular functions actually makes the language more consistent, since most code still uses the old style.
Additionally, the scope of the return type matches the function meaning that you can do
auto my_class::my_function() -> iterator { /* code */ }instead of
my_class::iterator my_class::my_function() { /* code */ }which is kinda nice
Very interesting, thanks! 🙂
Yeah, the most beautiful code is where all variables are just letters of the alphabet.
Keywords aren’t variables
Ooh yeah, overall coding culture is definitely not affected by the preferred nomenclature for identifiers. The person who’s habituated to
fnoverfunctionwill absolutely never name their functions in the vein ofchkdsk. The two are completely disconnected in the brain of the programmer who read too much K&R in their childhood and was irretrievably traumatized by it for life.I’d say it’s much more influential the names of the identifiers of the standard library.
A language with
functionkeyword that names it’s stdlib functionsstrstrandstrtokwill inspire way worse naming than on that hasfnkeyword with stdlib functionsstr::containsandstr::split.We could search for a random crate on crates.io and see what identifiers people actually use, or we could spread misinformation on Lemmy.
Tell me this is sarcasm
FORTRAN isn’t a beauty either.
And Python is strange as hell with its mandatory tabs.
You can use spaces in Python.
Two, three or four spaces? If you answer wrong I’ll never forgive you
Depends on the mood.
No one will ever know. That is my editor’s job. XD
Whatever your place defines as a standard. I’ve seen ugly code in C, JavaScript, Java, etc., that uses them all over the place because they’re not mandatory.
If you don’t have consistent indenting, your code looks like copy/paste from several sources; but if you do have consistent indenting, then the indenting of Python is a non-issue.
Per the Linux kernel coding style:
I’m rather partial to five myself but only when I’m feeling fancy.
Yes
Indentation-driven control flow is one of the most cursed things ever invented, excluding things explicitly designed to inflict pain or death.
Haskell has the choice of both indentation based and brackets for things like
doblocks, but most people use indentation based cause it’s the norm and looks cleanerWhite space sensitive languages are evil.
List comprehensions are much stranger than tabs vs spaces. There are very very very few languages that use them, and python’s is by far the worst out of the popular ones.
Skill issue. Once you learn them they are quite fun.
The concept of a list comprehenshion is sinple but syntax is awful, as if Yoda was writing a for loop. “x for x in y it is, hmm yes”.
Wasn’t the python convention to use spaces (4 iirc)? Which is just plain wrong imho.
most repos use 4 spaces
This is the one thing I hate about python, because the spacing would differ between editors. I used vim to create the files on one system, and geany to edit them on another. Via uses 8 spaces in a tab (at least for me), while geany uses 4. This makes python mad, and drives me crazy.
Also, the rules for whitespace separation between things like loops, methods, and the rest of the code is annoying/ wierd (at least to me).
You know that editors let you change their defaults, right?
and that indentation defaults in decent editors are usually language dependent. I’m not familiar with these editors, but… come on - if they use one default for all files, OP should use a better tool.
Yes, but I don’t normally program in python, so I never did. When I had to, I never thought of changing it (it wasn’t for long anyways and was less of a thought out decision to do programming in vim)
Get a code formatter. Ruff is popular. So is black. Never think about it again.