You might this blog post interesting, which argues that it's Rust semantics and not syntax that results in the noisiness, i.e.: it's intrinsic complexity:
I found it reasonably convincing. For what it's worth, I found Rust's syntax quite daunting at first (coming from Python as well), but it only took a few months of continuous use to get used to it. I think "Perl-esque" is an overstatement.
It has some upsides over Python as well, notably that the lack of significant whitespace means inserting a small change and letting the autoformatter deal with syntax changes is quite easy, whereas in Python I occasionally have to faff with indentation before Black/Ruff will let me autoformat.
I appreciate that for teaching, the trade-offs go in the other direction.
I'm not sure which of the dozen Rust-syntax supporters I should reply to, but consider something like these three (probably equivalent) syntaxes:
let mut a = Vec::<u32>::new();
let mut b = <Vec::<u32>>::new();
let mut c = <Vec<u32>>::new();
let mut d: Vec<u32> = Vec::new();
Which one will your coworker choose? What will your other corworkers choose?
This is day one stuff for declaring a dynamic array. What you really want is something like:
let mut z = Vec<u32>::new();
However, the grammar is problematic here because of using less-than and greater-than as brackets in a type "context". You can explain that as either not learning from C++'s mistakes or trying to appeal to a C++ audience I guess.
Yes, I know there is a `vec!` macro. Will you require your coworkers to declare a similar macro when they start to implement their own generic types?
There are lots of other examples when you get to what traits are required to satisfy generics ("where clauses" vs "bounds"), or the lifetime signature stuff and so on...
You can argue that strong typing has some intrinsic complexity, but it's tougher to defend the multiple ways to do things, and that WAS one of Perl's mantras.
Being able to use disambiguated syntaxes, and being able to add extra brackets, isn't an issue.
PS. The formatting tooling normalizes your second and third example to the same syntax. Personally I think it ought to normalize both of them to the first syntax as well, but it's not particularly surprising that it doesn't because they aren't things anyone ever writes.
It's really not. Only one of my examples has the equivalent of superfluous parens, and none are dereferencing anything. And I'm not defending C or C++ anyways.
When I was trying to learn Rust (the second time), I wanted to know how to make my own types. As such, the macro `vec!` mentioned elsewhere isn't really relevant. I was using `Vec` to figure things out so I could make a `FingerTree`:
let v: Vec<u32> = Vec::new(); // Awfully Java-like in repeating myself
let v = Vec::new(); // Crap, I want to specify the type of Vec
let v = Vec<u32>::new(); // Crap, that doesn't compile.
> let v = Vec::new(); // Crap, I want to specify the type of Vec
This kinda implies you've gone wrong somewhere. That doesn't mean there aren't cases where you need type annotations (they certainly exist!) but that if `Vec::new()` doesn't compile because the compiler couldn't deduce the type, it implies something is off with your code.
It's impossible to tell you exactly what the problem was, just that `<Vec<T>>::new()` is not code that you would ever see in a Rust codebase.
exactly. you specify types for function parameters and structs and let the language do it's thing. it's a bit of a niche to specify a type within a function...
There is a reason the multiple methods detailed above exist. Mostly for random iterator syntax. Such as summing an array or calling collect on an iterator. Most Rust devs probably don't use all of these syntax in a single year or maybe even their careers.
> There are definitely times you want to specify a type.
So I'm coming from basically obly TypeScript type system experience but that seems completely ok to me. There are times I make my TS uglier to make it less ambiguous and times I make it more ambiguous to make it more readable. It's unreasonable imo that such a system could universally land on the most readable format even if we could all agree what's most readable. Instead, some cases are going to be tradeoffs so that the more common cases can flow unimpeded.
I can't believe that a flexible powerful syntax is considered limiting or confusing by some people. There is way more confusing edge-case syntax keywords in C++ that are huge foot-guns.
I've only ever seen `a` and `d`. Personally I prefer `a`. The only time I've seen `c` is for trait methods like `<Self as Trait<Generic>>::func`. Noisy? I guess. Not sure how else this could really be written.
Fwiw, I didn't go looking for obscure examples to make HN posts. I've had three rounds of sincerely trying to really learn and understand Rust. The first was back when pointer types had sigils, but this exact declaration was my first stumbling block on my second time around.
The first version I got working was `d`, and my first thought was, "you're kidding me - the right hand side is inferring it's type from the left?!?" I didn't learn about "turbo fish" until some time later.
Rust’s inference is generally a strength. If there's a type-shaped hole to fill, and only one way to fill it, Rust will just do it. So for instance `takes_a_vec(some_iter.collect())` works even though `collect` has a generic return type — being passed to `takes_a_vec` implies it must be a Vec, and so that's what Rust infers.
> The first version I got working was `d`, and my first thought was, "you're kidding me - the right hand side is inferring it's type from the left?!?" I didn't learn about "turbo fish" until some time later.
Tbh d strikes me as the most normal - right hand sides inferring the type from the left exists in basically every typed language. Consider for instance the C code
Doing this inference at a distance is more of a feature of the sml languages (though I think it now exists even in C with `auto`) - but just going from left to right is... normal.
I see your point, and it's a nice example, but not completely parallel to the Rust/StandardML thing. Here, your RHS is an initializer, not a value.
// I don't think this flies in C or C++,
// even with "designated initializers":
f({ .flag = true, .value = 123, .stuff=0.456});
// Both of these "probably" do work:
f((some_struct){ .flag = true, ... });
f(some_struct{ .flag = true, ... });
// So this should work too:
auto a = (some_struct){ .flag = true, ... };
Take all that with a grain of salt. I didn't try to compile any of it for this reply.
Anyways, I only touched SML briefly 30 some years ago, and my reaction to this level of type inference sophistication in Rust went through phases of initial astonishment, quickly embracing it, and eventually being annoyed at it. Just like data flows from expressions calculating values, I like it when the type inference flows in similarly obvious ways.
I mean, the fact that you mention "probably equivalent" is part of the reality here: Nobody writes the majority of these forms in real code. They are equivalent, by the way.
In real code, the only form I've ever seen out of these in the wild is your d form.
This is some True Scotsman style counter argument, and it's hard for me to make a polite reply to it.
There are people who program with a "fake it till you make it" approach, cutting and pasting from Stack Overflow, and hoping the compiler errors are enough to fix their mess. Historically, these are the ones your pages/books cater to, and the ones who think the borrow checker is the hard part. It doesn't surprise me that you only see code from that kind of beginner and experts on some rust-dev forum and nothing in between.
The issue though is that this isn't a solvable "problem". This is how programming languages' syntax work. It's like saying that C's if syntax is bad because these are equivalent:
if (x > y) {
if ((x > y)) {
if (((x) > (y))) {
Yes, one of your co-workers may write the third form. But it's just not possible for a programming language to stop this from existing, or at least, maybe you could do it, but it would add a ton of complexity for something that in practice isn't a problem.
Only `b` has the equivalent of "superfluous parens".
It's practically your job to defend Rust, so I don't expect you to budge even one inch. However, I hate the idea of letting you mislead the casual reader that this is somehow equivalent and "just how languages work".
The grammar could've used `Generic[Specific]` with square brackets and avoided the need for the turbo fish.
It hasn't been my job to work on Rust in for years now. And even then, it was not to "defend" Rust, but to write docs. I talk about it on my own time, and I have often advocated for change in Rust based on my conversations with users.
If you're being overly literal, yes, the <>s are needed here for this exact syntax. My point was not about this specific example, it's that these forms are equivalent, but some of them are syntactically simpler than others. The existence of redundant forms does not make the syntax illegitimate, or overly complex.
For this specific issue, if square brackets were used for generics, then something else would have to change for array indexing, and folks would be complaining that Rust doesn't do what every other language does here, which is its own problem.
A compiler could disambiguate, but the goal is to have parsing happen without knowing if A is a type or a variable. That is the inappropriate intertwining of parsing and semantics that languages are interested in getting away from, not continuing with.
Anyway, just to be clear: not liking the turbofish is fine, it's a subjective preference. But it's not an objective win, that's all I'm saying. And it's only one small corner of Rust's syntax, so I don't think that removing it would really alleviate the sorts of broad objections that the original parent was talking about.
The problem here is that angle brackets are semantics dependent syntax. Whether they are brackets or not depends on semantic context. Conversely square brackets are always brackets.
Square brackets would be semantically dependent if they appeared in the same position of angle brackets. There's nothing magical about [] that makes the problems with <> disappear.
Well, the solution usually isn't in syntax, but it often is solved by way of code formatters, which can normalize the syntax to a preferred form among several equivalent options.
I suspect rustfmt would consider this out of scope, but there should be a more... "adventurous" code formatter that does more opinionated changes. On the other hand, you could write a clippy lint today and rely on rustfix instead
I think Perl-esque is apt, but that's because I've done quite a bit of Perl and think the syntax concerns are overblown. Once you get past the sigils on the variables Perl's syntax is generally pretty straightforward, albeit with a few warts in places like almost every language. The other area where people complained about Perl's opaqueness was the regular expressions, which most languages picked up anyway because people realized just how useful they are.
Once you're writing Rust at full speed, you'll find you won't be putting lifetimes and trait bounds on everything. Some of this becomes implicit, some of it you can just avoid with simpler patterns.
When you write Rust code without lifetimes and trait bounds and nested types, the language looks like Ruby lite.
When you write Rust code with traits or nested types, it looks like Java + Ruby.
When you sprinkle in the lifetimes, it takes on a bit of character of its own.
It honestly isn't hard to read once you use the language a lot. Imagine what Python looks like to a day zero newbie vs. a seasoned python developer.
You can constrain complexity (if you even need it) to certain modules, leaving other code relatively clean. Imagine the Python modules that use all the language features - you've seen them!
One of the best hacks of all: if you're writing HTTP services, you might be able to write nearly 100% of your code without lifetimes at all. Because almost everything happening in request flow is linear and not shared.
I'm trying to sell Rust to someone who is worried about it. I'm not trying to sound elitist. I want people to try it and like it. It's a useful tool. I want more people to have it. And that's not scaring people away.
Rust isn't as hard or as bad as you think. It just takes time to let it sink in. It's got a little bit of a learning curve, but that pain goes away pretty quick.
Once you've paid that down, Rust is friendly and easy. My biggest gripe with Rust is compile times with Serde and proc macros.
I think this depends a LOT on what you're trying to do and what you need to learn to do it. If you can get by with the std/core types and are happy with various third party crates, then you don't really need to learn the language very deeply.
However, if you want to implement new data structures or generic algorithms, it gets very deep very quickly.
Why would you say that? I feel this pushes people away.
"Hey, you might be able to use Rust trivially if you stick to XYZ, but if you dare touch systems programming you're in for some real hurt. Dragons everywhere."
Why say that? It's not even remotely true - it's a gradient of learning. You can use Rust for simple problems as a gateway into systems programming.
Rust is honestly a great alternative to Python or Golang for writing servers. Especially given that you can deploy static binaries or WASM.
We need more people learning the language, not to scare them away.
Rust is getting easier year over year, too! People can choose Rust for their problems today and not struggle.
Give them a cookie and let them see for themselves.
> Why would you say that? I feel this pushes people away.
It's not my obligation to evangelize for your pet language. I've spent enough time and written enough code in Rust to have a defensible viewpoint. This is public forum - I'll share my opinion if I want to.
Writing servers? Sure, go grab the crate that solves your problem and get on with it. Basically what I said above.
If I thought you would bother to do them, I could give you a list of concrete problems which ought to be super easy but are in fact really hard or ugly to do in Rust.
Phrases like "systems programming" have become so diluted that I'm not even sure what you mean. Once upon a time, that was something like writing a device driver. Now people use the phrase for things like parsing a log file or providing a web server.
I wanted to use Rust for numerical methods and data visualization. I didn't like the existing solutions, so I was willing to write my own Rust libraries from scratch. It was pretty painful, and the learning curve was steep.
> Why say that? It's not even remotely true
I didn't write the thing you quoted. Using a straw man argument like this is a lame tactic.
That article is really good, because it highlight that Rust doesn't have to look messy. Part of the problem, I think, is that there's a few to many people who think that messy version is better, because it "uses more of the language" and it makes them look smarter. Or maybe Rust just makes it to hard to see through the semantics and realize that just because feature is there doesn't mean that you need it.
There's also a massive difference between the type of C or Perl someone like me would write, versus someone trying to cope with a more hostile environment or who requires higher levels of performance. My code might be easier to read, but it technically has issue, they are mostly not relevant, while the reverse is true for a more skilled developer, in a different environment. Rust seems to attract really skilled people, who have really defensive code styles or who use more of the provided language features, and that makes to code harder to read, but that would also be the case in e.g. C++.
I think the fundamental issue with implementation-inheritance is the class diagram looks nice, but it hides a ton of method-level complexity if you consider the distinction between calling and subtyping interfaces, complexity that is basically impossible to encapsulate and would be better expressed in terms of other design approaches.
With interface-inheritance, each method is providing two interfaces with one single possible usage pattern: to be called by client code, but implemented by a subclass.
With implementation-inheritance, suddenly, you have any of the following possibilities for how a given method is meant to be used:
(a) called by client code, implemented by subclass (as with interface-inheritance)
(b) called by client code, implemented by superclass (e.g.: template method)
(c) called by subclass, implemented by superclass (e.g.: utility methods)
(d) called by superclass, implemented by subclass (e.g.: template's helper methods)
And these cases inevitably bleed into each other. For example, default methods mix (a) and (b), and mixins frequently combine (c) and (b).
Because of the added complexity, you have to carefully design the relationship between the superclass, the subclass, and the client code, making sure to correctly identify which methods should have what visibility (if your language even allows for that level of granularity!). You must carefully document which methods are intended for overriding and which are intended for use by whom.
But the code structure itself in no way documents that complexity. (If we want to talk SOLID, it flies in the face of the Interface Segregation Principle). All these relationships get implicitly crammed into one class that might be better expressed explicitly. Split out the subclassing interface from the superclass and inject it so it can be delegated to -- that's basically what implementation-inheritance is syntactic sugar for anyway and now the complexity can be seen clearly laid out (and maybe mitigated with refactoring).
There is a trade-off in verbosity to be sure, especially at the call site where you might have to explicitly compose objects, but when considering the system complexity as a whole I think it's rarely worth it when composition and a tiny factory function provides the same external benefit without the headache.
These are powerful tools, if used with discipline. But especially in application code interfaces change often and are rarely well-documented. It seems inevitable that if the tool is made available, it will eventually be used to get around some design problem that would have required a more in-depth refactor otherwise -- a refactor more costly in the short-term but resulting in more maintainable code.
You can either use the @example decorator to force Hypothesis to check an edge case you've thought of, or just let Hypothesis uncover the edge cases itself. Hypothesis won't fail a test once and then pass it next time, it keeps track of which examples failed and will re-run them. The generated inputs aren't uniformly randomly distributed and will tend to check pathological cases (complex symbols, NaNs, etc) with priority.
You shouldn't think of Hypothesis as a random input generator but as an abstraction over thinking about the input cases. It's not perfect: you'll often need to .map() to get the distribution to reflect the usage of the interface being tested and that requires some knowledge of the shrinking behaviour. However, I was really surprised how easy it was to use.
I suspect Minecraft was large enough to support an effective modding community from the start regardless of official support, so that there was always some kind of third-party unofficial mechanism (ModLoader, then Forge, then now Fabric and Quilt). Mojang probably punted it down the priority list because of that, or didn't want to impose a structure and kill those ecosystems. Technically speaking, Java is reasonably easy to plug stuff into at runtime, so that was never a barrier.
The original issue with official modding support, from my perspective, has always been a legal one. But the Mojang EULA explicitly allows modding now. So I would see this decision as one in a long line of decisions by Mojang to both normalise the legal relationship with modders, and beyond that giving a "thumbs up" to the community.
You may be interested in Mojo, it's a project by Chris Lattner. It aims to have Python-like syntax and smooth integration with Python but allow Rust-like low-level control (I believe it has a borrow checker). Unfortunately, I believe it's proprietary.
It may be a combination of both: the fact it is easier to stay in touch makes it more difficult to let go of friendships. But this may make those friendships feel less meaningful and therefore increase loneliness.
The cumulative distribution actually ends up pretty exponential which (I think) means that if you estimate the amount of time left in the outage as the mean of all outages that are longer than the current outage, you end up with a flat value that's around 8 hours, if I've done my maths right.
Not a statistician so I'm sure I've committed some statistical crimes there!
Unfortunately I can't find an easy way to upload images of the charts I've made right now, but you can tinker with my data:
cause,outage_start,outage_duration,incident_duration
Cell management system bug,2024-07-30T21:45:00.000000+0000,0.2861111111111111,1.4951388888888888
Latent software defect,2023-06-13T18:49:00.000000+0000,0.08055555555555555,0.15833333333333333
Automated scaling activity,2021-12-07T15:30:00.000000+0000,0.2861111111111111,0.3736111111111111
Network device operating system bug,2021-09-01T22:30:00.000000+0000,0.2583333333333333,0.2583333333333333
Thread count exceeded limit,2020-11-25T13:15:00.000000+0000,0.7138888888888889,0.7194444444444444
Datacenter cooling system failure,2019-08-23T03:36:00.000000+0000,0.24583333333333332,0.24583333333333332
Configuration error removed setting,2018-11-21T23:19:00.000000+0000,0.058333333333333334,0.058333333333333334
Command input error,2017-02-28T17:37:00.000000+0000,0.17847222222222223,0.17847222222222223
Utility power failure,2016-06-05T05:25:00.000000+0000,0.3993055555555555,0.3993055555555555
Network disruption triggering bug,2015-09-20T09:19:00.000000+0000,0.20208333333333334,0.20208333333333334
Transformer failure,2014-08-07T17:41:00.000000+0000,0.13055555555555556,3.4055555555555554
Power loss to servers,2014-06-14T04:16:00.000000+0000,0.08333333333333333,0.17638888888888887
Utility power loss,2013-12-18T06:05:00.000000+0000,0.07013888888888889,0.11388888888888889
Maintenance process error,2012-12-24T20:24:00.000000+0000,0.8270833333333333,0.9868055555555555
Memory leak in agent,2012-10-22T17:00:00.000000+0000,0.26041666666666663,0.4930555555555555
Electrical storm causing failures,2012-06-30T02:24:00.000000+0000,0.20902777777777776,0.25416666666666665
Network configuration change error,2011-04-21T07:47:00.000000+0000,1.4881944444444444,3.592361111111111
I actually think the design of DNS is really cool. I'm sure we could do better designing from a clean slate today, especially around security (designing with the assumption of an adversarial environment).
But DNS was designed in the 80s! It's actually a minor miracle it works as well as it does
https://matklad.github.io/2023/01/26/rusts-ugly-syntax.html
I found it reasonably convincing. For what it's worth, I found Rust's syntax quite daunting at first (coming from Python as well), but it only took a few months of continuous use to get used to it. I think "Perl-esque" is an overstatement.
It has some upsides over Python as well, notably that the lack of significant whitespace means inserting a small change and letting the autoformatter deal with syntax changes is quite easy, whereas in Python I occasionally have to faff with indentation before Black/Ruff will let me autoformat.
I appreciate that for teaching, the trade-offs go in the other direction.
reply