Hacker News new | past | comments | ask | show | jobs | submit login
Things I hate about Rust (yossarian.net)
163 points by woodruffw 12 days ago | hide | past | web | favorite | 121 comments





> No current way to get the user’s home directory. std::env::home_dir() is explicitly marked as deprecated, and the documentation encourages users to rely on the dirs crate (which is currently archived on GitHub).

Yeah, this is a bit of a farce. On the one hand, it seems fair to me to not have this in the standard library - it's not something that needs special cooperation with the compiler or runtime, it's not something universal (what would it do on an embedded microcontroller?), and it doesn't matter if different crates use different implementations. On the other hand, deprecating a standard library function in favour of a crate owned by a random user seems like an unforced error.

There's discussion here:

https://www.reddit.com/r/rust/comments/ga7f56/why_dirs_and_d...

Including the fact that there is an actively maintained alternative:

https://github.com/brson/home

And a clue to why those repos are archived:

> I was wondering the same thing and contacted the author. He said that some people had "lost their shit" over some of the things he mentioned about rust and that to help them calm down, they will be archived until 2022.

!


>what would it do on an embedded microcontroller?

Throw an exception or return null?


An odd suggestion for a language that has no exceptions and hardly any concept of null.

I guess I'm not being technical enough with my language. In Rust speak, I mean panic or return an empty result with an error.

Wow, you really got them! Your pendantry is appreciated!!

> it's not something that needs special cooperation with the compiler or runtime, it's not something universal (what would it do on an embedded microcontroller?)

It's filesystem access, like the open file and read file functionality, so it needs some runtime support, and all filesystem access would fail on embedded microcontrollers, so I'm not seeing why it shouldn't be in the standard library.


Plenty of μC's have filesystems, probably the majority nowadays. They aren't necessarily running an operating system with a concept of users though. Besides, in C the file commands can be used to do non-file IO, so those functions still have a use even without a FS.

>probably the majority nowadays

I'd dispute that on the grounds that a microcontroller with a filesytem is likely doing something complicated enough that it lives on a board with at least one other microcontroller that doesn't.

I'll even go as far to say it's more likely that the majority of microcontrollers don't write to mutable storage other than RAM (if they have it at all) once they leave the factory.


If you think Rust strings are bad, you're obviously never developed on Windows.

From my failing memory, MBCStr, CComBStr, BStr, CString, XLString, and then all the unicode versions. Then you're developing in C++, so you also have std::string and std::wstring, oh and u16string, and u32string. Plus char, and wchar.


Author here: I've spent about half a decade writing Win32 applications, including things deep in NTFS.

I agree completely that everything about string typing on Windows is worse, especially once you stray off of the happy path. But the post isn't intended to equivocate against a ecosystem with 40 years of legacy APIs in it; it's to nitpick a new ecosystem that I otherwise love ;)


Agreed. Abstracting the OS and its different string versions is hard. Even more so when your language has to work on multiple OSes. And then add into the mix non-mutable by default.

This blog post [1] may interest you (if you haven't already read it).

[1] https://fasterthanli.me/blog/2020/i-want-off-mr-golangs-wild...


I don't see an alternative to interfacing with that though. If we were to just use UTF-8 strings for everything then there would be files we couldn't handle in various OSs for example. So while it's a bit unwiedly to use filepath.to_lossy_str() etc. I prefer it to a leaky abstraction that hides necessary complexity.

It wouldn't be an issue if OSes forbade non-UTF-8 paths...

It's the unfortunate case that we have OS environments that are usually, but not always, UTF-8, and there's no clear, reliable indication of whether or not UTF-8 is to be expected.


And this was the other article about Rust strings I meant to link:

https://fasterthanli.me/blog/2020/working-with-strings-in-ru...


"Not as bad as Windows" isn't exactly a high bar to clear.

Don't forget lptstr, lpstr, lpcstr, lpwstr, and a dozen more I forgot.

Don't forget _bstr_t! And VARIANT's holding strings (and all the VARIANT variants) and when to use the OLECHAR or TCHAR types and related functions instead of wchar or char...

Yeah, I guess maybe nothing will ever be as bad as strings in Windows with C++.

But rust should't go down this road any further.


If you develop for Win32, you only need to care about UTF-8 and UTF-16 strings and converting between them is just a native function call.

If that were true, then WTF-8 wouldn't exist.

I searched, and this comment was not a joke

For getting a users homedir I fall back to POSIX[0] (which windows follows in this instance fwiw).

There are some things that I'm bitten by as a rust newbie though; strings are definitely one of those, since it's more difficult to reason about strings as they're heap allocated, where as most "normal" types are not, thus to use Strings you have to learn the borrow checker..

Another issue I have is crate versions, and compiling and including _multiple versions_ of the same crate, because that crate is also a dependency on one of the crates I'm using.

Yet, another is crates that only support the nightly compiler, which you're not supposed to use for production, there needs to be some way of delineating the "stable" version of a crate with the "potential future" version of a crate.

Even if you stick to a version and the nightly compiler supercedes then you end up with the same error:

------8<------

    Caused by:
      process didn't exit successfully: `/Users/dijit/projects/rust/Rocket/target/debug/build/rocket_codegen-6474f79f6391da32/build-script-build` (exit code: 101)
    --- stderr
    Error: Rocket (codegen) requires a 'dev' or 'nightly' version of rustc.
    Installed version: 1.43.1 (2020-05-04)
    Minimum required:  1.33.0-nightly (2019-01-13)
------>8------

[0]: in POSIX the OS _must_ set $HOME: https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd...


> since it's more difficult to reason about strings as they're heap allocated

I don't think them being heap allocated is the issue. I think the issue you're running into is that String doesn't implement the Copy trait, meaning they're not automatically copied for you. Which is what you want, because you don't want to be implicitly copying large strings without realizing it. But yes, it does mean you have to learn the borrow checker.


Two sides of the same coin: it being heap allocated is why it does not implement Copy.

I guess my point was you'd have the same issue with things that are not heap allocated, if they also did not implement Copy.

I'm not sure about home dir but a thing I've needed is a standard place to store app stuff. On Windows it's a mess. There's AppData/Local, AppData/LocalLow, AppData/Remote and many apps store a ton of stuff in their app folder in c:\programs etc... On Mac I guess it's Library/Application Data but there's a bunch of other folders under Library. On Linux I read somewhere it's .config but there was no .config on my Linux installs and it seems strange to me that apps should be required to make this folder vs the OS and that the OS doesn't tell me where it is it's just convention. But in any case, I think I wish some standard library handled this.

Yes, I understand, and I feel your plight, as it stands this is what I would use:

$APPDATA on windows[0] or $LOCALAPPDATA

$XDG_CONFIG_HOME on linux[1], which is where you're getting the ~/.config from.

For MacOS, it's a mess[2], you should probably use ~/Library/Application Support/script_name/ for most things though.

[0]: https://ss64.com/nt/syntax-variables.html

[1]: https://wiki.archlinux.org/index.php/XDG_Base_Directory

[2]: https://developer.apple.com/library/archive/documentation/Fi...


> $XDG_CONFIG_HOME on linux[1], which is where you're getting the ~/.config from.

XDG_CONFIG_HOME is ~/.config if not otherwise specified.


My biggest gripe with Rust is that is doesn't spark joy. I intellectually agree with a lot of its goals but every time I write some Rust I just find it dreary. Maybe it's the ugly syntax, maybe it's the "code feel" of interacting with a grumpy compiler.

That said I know a lot of folks really dig it and think it's a breath of fresh air; more power to em'.


Coming from safety critical C/C++ I'm really happy about the grumpy compiler. It catches basically every mistake where you would normally in C++ have static analysis, ASan, MSan, UBSan. With thousands of false positives with one true positive on your static analysis tool. ASan catching your memory errors (maybe) and telling you the rough location if you exercise the right part of your code in your unit tests. UBSan doing the same for undefined behavior. Never mind these are all separate compiles that only warn you after ages of running on your Jenkins server.

The package manager is a joy to work with and supports multiple versions of tools.

The edition mechanism allows the language not to be dragged down by old cruft.

The proc macros make it possible to have insanely reusable libraries, like serde.

The normal macros are part of the AST so you don't make stupid mistakes that are hard to trace.

Rust is just so good. But I think it's only obvious if you've worked with C++. Most of this doesn't seem special coming from C#. Except the performance that is possible.

The borrow checker might cost you a few minutes, but it can save you days or even weeks of debugging, and maybe it will save lives some day when Rust makes it to safety critical applications.

One thing that did bother me for a long time was RLS auto complete, which was just really slow and useless. This has been fixed with the advent of rust analyzer.


The opposite here. Rust is the only language for me that sparks joy (I write numerical Python/C++ at work, well familiar with half a dozen other languages).

Like, I find it extremely pleasing to write any kind of prototype in Rust and make things work. I guess once you have borrow checker embedded in your head and your newly written code compiles without errors most of the time, it becomes more pleasing, but that's not even the main point. There's something about it that's "just right" for me, not anything in particular but a multitude of various things.


Do you enjoy J for numerical programming?

I know exactly what you feel. I am kind of in the same boat (I am a C++ developer trying out rust). One example is the lifetime specification makes it look like code from the code golf languages i.e. "&'a mut"

However, this sense is reducing every-time I see an example of some idiomatic Rust code. I appreciate that they've addressed a lot of C++ pain points.

1. Traits are awesome. (Simon Brand: How Rust gets polymorphism right - https://www.youtube.com/watch?v=VSlBhAOLtFA)

2. The lazy pipeline styled programming is idiomatic and produces optimal assembly. (Ranges in C++20 )

3. Extremely powerful enums. Straight out of functional langs.

These are only a few of the things top of my mind but it makes me appreciate Rust more and more.


>maybe it's the "code feel" of interacting with a grumpy compiler.

It's crazy, because this is that part I love the most. Both Rust and Elm just blow my mind every time the compilers not only throw up something I never would've noticed, but even tell me how to fix it! It's crazy to think how far we've come, and it still makes me giddy with excitement every time I see it.


I understand the feeling, but from my experience it fades away pretty quickly once you get the feeling of the language.

These days I'm working again with Rust to add a new feature to a project after not touching it for months, and that feeling of "oh dear god this is awful" that I felt when I was starting out with the language was back again. However, once everything clicked in, with the compiler telling me exactly what was wrong with the code, and the project compiling which meant that I could be damn sure that the code I wrote would work with little to no risk of breaking in unexpected ways... that sure did spark joy.

Just my experience.


That seems a lot like taking up smoking to me. I took a puff once. "Oh dear god this is awful" was pretty much my reaction. For some reason people experience this, then power through the "awful" until they've ingested so much they can't easily walk away from it.

I've never understood this. Why continue taking "awful" puffs? Why not just use/ingest something else that doesn't feel awful?

And yes, I get that languages are extremely subjective and some folks find it joyful not awful. Good for them. I'm glad they found happy. For everyone else though...


Adding my voice to the "it sparks joy" crowd. I think it depends a lot on who I am and what I'm trying to do; I'm really motivated by performance, and when I'm writing in Rust I feel I can do that and also maintain higher level structure.

I don't want to pile on, as quite a few people have already commented under this comment, but I would really like to know why you feel the compiler is "grumpy". Others and I have spent a lot of time trying to make the diagnostics clear, understandable, friendly and actionable. If there are cases we are failing at doing that, I would be more than happy to hear about them!

I hope it's not the tone, but rather how nitpicky the language feels, but the target is for most of the seemingly pedantic or arbitrary restrictions to give you guidance on what needs to be done instead.


I can't answer for GP, but its complaints about excess parentheses go well beyond helpfulness, into nannyism.

FWIW, during development I sometimes add #![allow(warnings)] on crate's root file to cut down on the output from lints, but I am rarely bothered enough to do so.

I kinda feel the same. For me, it's because most of the stuff I work on aren't real time systems, so go level GC pauses are acceptable. The mental tax of worrying about ownership just gets in the way of solving the problem in this case. For what it's worth, I've done Haskell professionally, and nowadays I'm not in love with it either.

If I was working on something where the best alternative is C++, I'd probably feel different. Right tool for the job and all of that.


Fighting with a compiler is a different feeling to be sure.

I'm primarily a C# dev, which normally has a pretty happy compiler.

But I've started playing with Websharper (Translates C#/F# code to JS.) One nice thing about the library is that once you understand the basics of composition, your JS -will- work as long as the compiler doesn't barf on what you did.

But, it has the drawback of seeing more errors from the compiler itself. While at first it was frustrating, I realized that the time I spent dealing with the compiler errors was still less time than I would have spent troubleshooting my own handwritten JS.


I think what excites me most as a Rust newbie (~2 weeks) is the ability to write low-level and performant code and at the same time be ensured a fair amount of safety by the compiler. Something that Python/C++/Go cannot offer me.

Also a fairly good development experience (compared to C++)


Definitely sparks joy for me. Tooling is fantastic and once you're experienced enough it feels very well put together.

I'm okay with this. To me, Rust is a tool for writing a small number of pieces of very serious infrastructure: kernels, drivers, firmware, high-scale daemons, etc. These feel like works that should be executed in a mood of sober seriousness, rather than joy. Rust is a double bass, not a saxophone.

I'm really hoping Swift breaks out of the app ecosystem soon and becomes mainstream on Linux, it's a joy to write and has a great compiler team behind it. Still missing some big features but I think they will be solved in Swift6.

Fully agree. It's a couple of years since I've used it, but Swift just feels great. Guard clauses, optional chaining, and many other features just seem to fit the way I like to write code. It's not without its warts, but it's one of my favorite languages syntactically.

That's a shame, I find Rust (and, ruby) spark incredible amounts joy to code in.

But I find that Go does not spark joy for me. ('.Dial'?!, first character case denotes public/private D:).

I guess that it is very subjective.


Well... it is maybe subjective, but I have the same problem. Or even worse, I'm not very convinced by Go because it uses the first letter to denote public/private access. I know this is a weak argument, but... :P

'.Dial' ?? What else would you expect instead of Dial ? If you get demoralized from an imperfect function name you are in the wrong domain of activity.


Go follows the Plan9 system call name instead of the UNIX one. Dial is much more powerful than the UNIX dance of "getaddrinfo"+struct sockaddr init+connect(2).

It might not seem much (and higher level languages usually abstract away the craziness that UNIX sockets are in C), but that's what the OS still gives you in 2020...


Plan 9's dial() interface is implemented in a user land library. In terms of the interfaces provided by the kernel, connecting a socket is anything but simple in Plan 9. See https://9p.io/sources/plan9/sys/src/libc/9sys/dial.c

The reason no standard interface has replaced getaddrinfo + socket + connect is probably because it's just barely simple enough for common usage in C, and C was never the language you turned to when you wanted to write something short and sweet--that's why Unix environments have always hosted a myriad of other languages. If initializing a network connection were as complex as in Plan 9, doubtless Unix would have provided a more succinct libc interface for the common case

The BSD Sockets API is also close to the simplest possible interface for supporting all the various address and socket option combinations that are possible. (The kernel provides mechanism, not policy.) So even if POSIX, Linux, or whatever included a better standard interface for initializing a connection, it would have to be in addition to the BSD Sockets API (or equivalent).


BSD Sockets are infamous for being significant issue in using anything other than IPv4 (getaddrinfo is copied over from XTI, aka Streams-related "evil" API).

They are literally a low-level API that happened to be part of IPv4-only stack because DoD had short deadline to get IPv4 ported to VAX and other new Unix machines.

And OS should provide a policy when it comes to networking, otherwise you end up with never ending story of working around other's software to implement them.


Plan9's "Dial" is definitely a product of it's time, literally meaning to "Dial" a phone.

More information here: relevant man entry: https://www.unix.com/man-page/plan9/2/dial/

It specifically mentions making a call.

The example even dials "kremvax". Usenet over AX.25 from the 80s. Pretty sure that is _actual_ dialling.

But, I'm curious as to why Go's "Dial" is more powerful than a standard connection in any other language.


Plan9's dial is derived from OSI approach of separating service and protocol (aka implementation), with name service as first-party component (BSD Sockets have it stapled over and it hurts).

The example with calling kremvax over datakit specifically goes to show-off that all that specifies datakit usage is the "dk" string - nothing else.

Similarly with XTI (and other OSI-oriented network APIs), what you are telling OS is "I want to have a stream oriented connection with graceful close, to service Y on host X", and you don't have to care at all whether it will be IPv4, IPv6, TUBA, CLNS over Ethernet, or direct serial modem exchange using HDLC.

Go doesn't have all of that flexibility because it works from userspace, but it reuses the "service, not implementation detail" approach and lets you concentrate on human-readable domain names and service names instead of providing a maze of "hardcoded IP" issues.


I used to fight with the Haskell compiler until I began to understand how it did things and what it expected and now I only fight with the compiler when I'm genuinely confused about something. I wonder how long it takes to achieve the same level with Rust.

I’d say it takes some time. Golang was definitely faster to spark joy for me.

I absolutely love the rust syntax, it makes me happy. Getting through compiler errors is a grind, but it does get better as you stop making the same mistakes.

It's nice for lower-level programming but I miss functional programming myself.

The `dirs` crate has been recently forked and revived - the new home is here: https://github.com/xdg-rs/dirs (and the crate is named `dirs-next`).

Given the relatively sparse nature of the standard library, and the cost of making mistakes (std::sync::mpsc, anyone?!) I can see why this is omitted. IMO it’s a fair criticism that pulling in lots of crates is less than ideal, though.


> std::sync::mpsc, anyone?!

Can anyone expand on this? I'm not well versed in Rust lore and have just used that module in a recent learning project.


It's cool, and was included probably because Go has channels and that user-base is valuable. But now there's crossbeam-channel, which is significantly faster, has handles that are more shareable, and it removes the `Result` wrapper around all the operations. So a drop-in replacement is technically possible, but doesn't get rid of `unwrap` everywhere, and there's no steam behind replacing the guts of it because the lesson that was learned says 'just deprecate it, let it live in a crate'.


Yes - sorry, this is the comment I was referring to. I'm not aware of _bugs_ in `std::sync::mpsc`, more concerns around things which could have been done better which are now not possible to retrofit because of the guarantees of the standard library with respect to backwards compatibility.

Perhaps Rust should make clearer that `String` is a string buffer and not just a string type. Other than that I think Rust does need different string types for different situations.

As to the standard library, I've actually started to think it's too big. I know this may sound like heresy for someone coming from a language with a big standard library. However, I think it should do whatever needs compiler support and have some traits for easier interop and that's about it. EDIT: Perhaps also some functions to help handle the more fiddly UB risks.

Rust has a great, easy to use ecosystem. More needs to be done to point out the trusted and well tested crates and to point people in that direction. The standard library itself is much harder to contribute to and has stability guarantees which can potentially make mistakes costly in the long term.


There's definitely a theory that the pairs of view and buffer (or borrowing and owning) types should have been more consistently named. I think steveklabnik usually manifests in these threads to express it. But it's something like:

str is to String

as &[] is to Vec

as Path is to PathBuf

... and the names should reflect that.


Yep, you nailed it. I wouldn't change Vec though. I would make the string types more consistent with the path types. The reason is that slices are more general than just vectors, whereas str/String and Path/PathBuf are virtually always only used together.

Fair enough. Although i'd rename Vec to List, because variable-length sequences are not vectors, and there's no need to propagate that mistake any further!

The problem is, some people think List means some form of linked list, not what a vector is.

Names are hard.


It’s not totally true right? &Vec would be the equivalent here, what you pointed to can only be obtained by doing vec.as_slice()

That is not correct, &Vec is fundamentally different. &str, &[T], and &Path are all "a pair of a pointer and a length," and String, Vec<T>, and PathBuf are all "a triple of a pointer, a length, and a capacity.".

&Vec<T> is "a pointer to a triple of a pointer, a length, and a capacity."


I guess I’m a bit confused sometimes as what you get when you borrow. Sometimes I can borrow, sometimes I need to call .as_slice() or as_path or as_str

Yeah, so thanks to “Deref coercion” a &Vec<T> will coerce into a &[T] in many places. That’s probably what you’re seeing.

That "String" should have been called "StrBuf" or something similar is a relatively widespread opinion. Oh well.

Skipped over my personal biggest annoyance, the inability to abstract over mutability (example: https://www.reddit.com/r/rust/comments/2a721y/)

Argh, I ran into this last week. I ended up just copying the code and nudging. I wonder if a macro could be used as a workaround?

Funny that's the exact same issue that C++ const cast 'solve', of course D's inout solution is much more elegant.

I have the same problem with strings as well. I think I understand the difference between String and &str but I wish it’d automatically convert String into &str when a function expects it.

Example: `std::env::var(“SOME_PARAM”).unwrap_or_else(|_| “localhost”.to_string())`

It just seems odd to me, something tells me there is a better way to do this but I couldn’t find it.


> I have the same problem with strings as well. I think I understand the difference between String and &str but I wish it’d automatically convert String into &str when a function expects it.

Yep! My understanding is that `AsRef<str>` is the right way to do this automatic conversion but I learned that by reading library code, not from the standard documentation. It'd be awfully nice if the compiler could do those sorts of conversions automatically; injecting `AsRef` everywhere adds a lot of visual clutter.


> injecting `AsRef` everywhere adds a lot of visual clutter

I agree!

`fn foo<T: AsRef<str>>(s: T)`

This is so difficult to read, one has to jump back and forth with eyes to make any sense of these. Why couldn't there be easier syntax for "AsReffing"? E.g.

`fn foo(s: @str)`

I invented @-sign there.


Syntax like @T was actually a type in ancient Rust. People really, really hated having even more sigils in the language, so we ended up removing them. There was also ~T.

IIRC, one of the sigils was for reference counting, and there was to be another one for garbage-collected types?

(I'm recalling the lunch conversations from when I sat in the room with all the Rust interns).


@T was supposed to be a GC'd type, but ended up being a refcounted type. It was removed before it actually turned into a GC'd type.

~T was Box<T>.

However, one issue here was that they didn't compose in the same way as types today; ~str existed, for example, but was not the exact same as String in terms of memory layout. Conceptually they're the same thing though.


Can use `impl Trait` I think?

``` fn foo(s: impl AsRef<str>) ```


Massive turbofish! :-D

There, you're converting a &str into a String, not String into &str. That involves allocation, so i think it's quite right that it's explicit.

Since "localhost" isn't assigned to a variable, couldn't it be a safe implicit conversion?

The thing is, "localhost" ends up being some characters [1] in the read-only section of the binary. You can safely read that, but you can't modify it or delete it (that won't work, and if it did work, it would make a hell of a mess!).

So this is fine:

  let host: &str = "localhost";
Because a &str is just a read-only pointer to some characters [1], and you're just setting host to be a pointer to those characters in the read-only section.

But this is not:

  let host: String = "localhost";
Because a String is not just a pointer, it's a pointer which implies ownership of an allocation on the heap [1]. That means you can modify the contents of a String, and when the String is dropped, that allocation will be freed. You can't just set up a String using a pointer into the read-only section. You need to make an allocation, copy the characters into it, and use that. Which is what str::to_string() does.

[1] And a length, but we can ignore that for now.


It's not really important whether it's assigned to a variable or not. It's important that "potentially expensive" operations like memory allocation are obvious. So you either need to call to_string, or call String::new("localhost").

The strings actually make sense. The difference between String and &str is same as between Vecs and slices. Don't use OsStr unless you have to.

The problem is naming though. Calling something a "slice" connotes the existence of a whole somewhere from which the slice was taken. The string types just call themselves same thing with some funny syntax and oddly inconsistent capitalization and abbreviation conventions.

This non-behavioral stuff matters. Almost literally no one who comes to rust understands the string types with any intuition initially. And strings are really important!

Much the same thing can be said about the evolution of other areas of rust syntax (macros and attributes come immediately to mind). The final result is a collection of syntactic soup with (mostly) well-defined and internally consistent behavior, but with a syntactic expression that bears the archaeological scars of its evolution.


> The problem is naming though. Calling something a "slice" connotes the existence of a whole somewhere from which the slice was taken. The string types just call themselves same thing with some funny syntax and oddly inconsistent capitalization and abbreviation conventions.

That is not the case.


Author here: I agree that they make sense!

I think the nuance is between "makes sense" and "fits into the mental model Rust otherwise encourages," i.e. thinking about `T` and `&T` as the fundamental building blocks for ownership semantics. In that context, being unable to directly instantiate a `str` (or having `&String` be a thing that you can occasionally produce when you mean `&str) is deeply confusing to newcomers.


I'd even made your same comment about Strings over on reddit last week - it's not like the ways it's doing it are wrong at all. It's more that newbies can suffer decision paralysis when you are obsessed with learning the "right way" to do something, but there's no clear answer. Do I `.map(String::from)`, do I `.map(From::from)` and let things infer? Do I `.map(|s| s.into())`? `.map(|s| s.to_owned())`? `.map(|s| s.to_string())`? You can drive yourself crazy over the most meaningless, pointless questions in your code.

The surprising thing is, I find rust an incredibly effective language at sparing me from worrying about trivialities. Once you learn the slight differences in what all those things mean, you can be pretty direct. Overall I find rust one of the harder languages to write, but one of the easiest to read, and reading code is generally where you'll spend more time.


This diagram may help:

https://docs.google.com/presentation/d/1q-c7UAyrUlM-eZyTo1pd...

Although it could really do with some more expository text!


A few of my own:

Declaring/initializing a medium sized array of structs, is simply too verbose if you have meaningful structure names. What is a few lines in C explodes into pages in rust.

Type inference on declarations but, not function parameters, WAT!

The scoped constructor syntax is designed to miss the concept of RAII.

Array's by themselves are nearly useless, might have just made Vec<> the default array type.

Variables end up being &mut even when its not necessary because the compiler forces the attribute to be carried along in a number of cases when its not rightfully needed.

As I've complained about before, many of the 3rd party cargo libraries need additional traits before they can be mixed with threads/mutexes unless your willing to use unsafe.

Then there are all the bad style choices, starting with the javascript like nested calling (aka obj1.call().call2().call3().call4().call5();) chains spread over whole pages of code. Mismatch braces (yah I know this one is everywhere in C/javascript/java too, doesn't make it right). Trait declarations that are scattered everywhere rather than being centralized like a C++ class.


> Type inference on declarations but, not function parameters, WAT!

This is a hard restriction in place on purpose. The compiler could perform inference for the input and output types as part of the language but that means that the actual low level signature of your function could change by changing either the implementation or the callers. This is a compromise we must have to avoid surprises in real programs that have public APIs. On the flip side the compiler doesn't have this restriction for inferring the return type. If you write a function signature `fn foo() -> _` with a body that can resolve to an unambiguous type, the compiler will give you a structured suggestion for the correct type. This way you get the benefit of inference when developing and the benefit of type safety and unambiguous documentation of your code at the cost of mild inconvenience.

> Array's by themselves are nearly useless, might have just made Vec<> the default array type.

I'm not sure what the actionable recommendation is here. Is it to have made [] the syntax for Vec? If so, that would have made Vec special from the language's point of view, when it doesn't need to. Today you can reimplement all of Vec in your own Rust code, but you can't for arrays, because the compiler and language need specific memory layout information about them.

> Mismatch braces

Could you expand? I'm intrigued what you mean by this.

> Trait declarations that are scattered everywhere rather than being centralized like a C++ class.

This enables very flexible design and composition of behavior. It feels very weird when coming from an OOP background, but I've come to prefer it after a while.


I'm confused about this list of complaints

What is verbose about declaring arrays of structs?

Arrays are fundamentally different than vectors, the same is true in Rust as in C++.

Types are only inferred for variable declaration, that works for closures too. You can't have an inferred type in a struct declaration either. It doesn't make sense for a function declaration imho, and having worked in languages that do it - I hate it. Horrible practice to drop type args from functions.

I think method chaining is really helpful, particularly the builder pattern. It's also supremely useful for abstracted logic or patterns like iterator combinatory, something that you don't find as cleanly in other systems languages.

Not sure what you mean about traits being scattered? They're interfaces like a pure virtual base class - implementing them is very similar to implementing a base class in C++. Except you can use generic arguments for implementations which makes for some really expressive mechanisms for abstraction.


> No way to invoke a command through a system shell. Yes, I know that system(3) is bad.

I think a crate that offers more shell-like convenience functions/macros for scripting would be more powerful than just adding a system()-equivalent to the standard library.


I quite like the way Rust handles strings. Better than Haskell.

I think it's pretty bad in both languages. In Haskell it's trivial to understand the difference between a ByteString and Text. The names give it all away. You do need to know the difference between lazy and strict values, but that's universal to the language and not hard to understand. I'd say that strings in Haskell are more annoying than hard to understand. In Rust, even figuring out why some of the string types exist in the first place and what they mean is hard, and you still have the interconversion problem, which now is not only annoying but also difficult to understand.

Haskell implementing strings as lists of characters is a problem. Basically, you can't use them for anything performant.

A proper string library should be a Functor, naturally, but it should also be much more like a rope and much less like a list under the covers and in its API.


I agree, but that's why you use Text and ByteString. Annoying? Yes. Hard to understand? Not at all. Rust strings to me are both annoying and hard to understand and explain.

As a Rust beginner, the point about strings really resonated with me.

Over time I found that I appreciated it more and more. There's a lot of weird and wonderful edge cases and it's a joy to see them encoded in the type system. You don't have to resort to stackoverflow questions like this: https://stackoverflow.com/questions/2050973/what-encoding-ar...

Not being able to execute in the current shell has been a bit of a pain...

Want to write a environment variable that lasts the lifetime of the current shell session? No way to do it.


Is that a Rust problem? How would you normally do it that fails in Rust?

That string situation in Rust sounds like a lot of cognitive load. I wonder why can a language like Nim (which competes with Rust) have such wonderful dev ux where you can just go `string` and Rust has this thing?

Each string in Rust has a specific purpose. OsString, for instance, is for storing data which might be in, say, UTF-16 instead of UTF-8.

Just a guess but probably because Nim uses a garbage collector and probably doesn't make the same memory safety guarantees that Rust does.

The garbage collector is optional and Nim is memory safe.

Just like Rust, you can unsafe structures when using libraries in C/C++.


There's two separate issues in play here:

The first is the distinction between "owned" (i.e., heap-allocated) and "borrowed" (types). Arguably, there could have been better language support for this to fix this issue.

The second is the fact that the underlying OS rules for strings and paths do not conform to any sane string requirements on most systems. If you collapse this into normal strings, then you end up with situations where you can't interact with your OS properly.


Rust prefers explicit, deal-with-the-complexity types. The OP lumped ownership-related types (String/&str) and the nightmare that is filepath encodings (OsString/OsStr).

The first case is related to ownership/borrowing as these concepts are fundamental in systems languages.

The second case is a hint that filepaths are more complex than a single string type can represent.


Reading this confirms that I was right to pick Go for a couple things.

My very strong opinion is that the language should impose as little cognitive load as possible. Rust seems like it makes you think a lot about Rust, which means you are not thinking about the problem you are trying to solve. Brain cycles are a finite resource.

I don't want to diss Rust too much though. I see it as a potential future replacement for C/C++ for bare metal stuff where you really do want to hand craft the code for maximal performance or directly interface with hardware. That is a different niche than building higher level stuff, and I don't think anyone has managed to make a language that is good for both (yet?).


Can't agree. Rust's goal, if you can call it that, is to make you think about things you should be thinking about anyway.

Coming from C, you should be thinking about lifetimes and mutability and sharing. If you dont, your program may work but be buggy. Rust forces you to think about that.

Go does the same thing in different ways at different costs. It trades memory/performance so that you don't have to worry about allocations and simply doesnt use standard shared mutability patterns (not exactly, but channels take up most of the same space).


I don't understand why the parent post of yours is getting this much downvotes. What's wrong with using a GC'ed language when performance requirements are permissive?

That's not precisely their statement even if it is their intention. I don't think anyone could argue that statement (though some may deny your axiom of lax performance requirements).

I think people are taking issue with the claims that _Rust_ is imposing the cognitive burden. As I said, these burdens already exist, languages just weren't forcing people to deal with them.


You mention cognitive load, however this post[0] about Go made me think that the "simplicity" of Go might come with it's own issues that lead to more cognitive load in the long run

[0]: https://fasterthanli.me/blog/2020/i-want-off-mr-golangs-wild...


> My very strong opinion is that the language should impose as little cognitive load as possible.

The key there is "as possible." What is possible depends on the constraints and the goals. Rust does "impose as little cognitive load as possible" generally, given its goals as a language. You can nitpick about some parts of this, but at least all of the examples in the post have good reasons why they need to be this way, thanks to the goals for the language.

Rust and Go have different goals, and so "as possible" is something that means different things for each language.

On the subjective side:

> Rust seems like it makes you think a lot about Rust, which means you are not thinking about the problem you are trying to solve. Brain cycles are a finite resource.

This is true at the start, but once you get over the hump, you don't really spend any time thinking about this. The strictness is what lets you not need to care as much: the compiler will check your work, so you don't have to think about things very hard. It takes a while to get there though.


Rust doesn't compete with golang. The latter is in the same space as python. Rust competes with C++.

Rust competes with both, though some golang programs are quite reliant on general GC and rust would not be appropriate for those as of yet.



Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: