As someone who has been programming in Rust for nearly a year, even for commercial purposes, this article is baffling to me. I've found the compiler messages to be succinct and helpful. The package system is wonderful. It's dead easy to get something off the ground quickly. All it took was learning how and when to borrow.
I can see where the author comes from. I've been working with ^W^W fighting against Tokio this week, and the error messages are horrible. Representative example:
error[E0271]: type mismatch resolving `<futures::AndThen<futures::Select<futures::stream::ForEach<futures::stream::MapErr<std::boxed::Box<futures::Stream<Error=std::io::Error, Item=(tokio_uds::UnixStream, std::os::ext::net::SocketAddr)> + std::marker::Send>, [closure@src/server/mod.rs:59:18: 59:74]>, [closure@src/server/mod.rs:60:19: 69:10 next_connection_id:_], std::result::Result<(), ()>>, futures::MapErr<futures::Receiver<()>, [closure@src/server/mod.rs:74:18: 74:74]>>, std::result::Result<(), ((), futures::SelectNext<futures::stream::ForEach<futures::stream::MapErr<std::boxed::Box<futures::Stream<Error=std::io::Error, Item=(tokio_uds::UnixStream, std::os::ext::net::SocketAddr)> + std::marker::Send>, [closure@src/server/mod.rs:59:18: 59:74]>, [closure@src/server/mod.rs:60:19: 69:10 next_connection_id:_], std::result::Result<(), ()>>, futures::MapErr<futures::Receiver<()>, [closure@src/server/mod.rs:74:18: 74:74]>>)>, [closure@src/server/mod.rs:78:34: 83:6 cfg:_]> as futures::Future>::Error == ()`
--> src/server/mod.rs:85:15
|
85 | return Ok(Box::new(server));
| ^^^^^^^^^^^^^^^^ expected tuple, found ()
|
= note: expected type `((), futures::SelectNext<futures::stream::ForEach<futures::stream::MapErr<std::boxed::Box<futures::Stream<Error=std::io::Error, Item=(tokio_uds::UnixStream, std::os::ext::net::SocketAddr)> + std::marker::Send>, [closure@src/server/mod.rs:59:18: 59:74]>, [closure@src/server/mod.rs:60:19: 69:10 next_connection_id:_], std::result::Result<(), ()>>, futures::MapErr<futures::Receiver<()>, [closure@src/server/mod.rs:74:18: 74:74]>>)`
found type `()`
= note: required for the cast to the object type `futures::Future<Item=(), Error=()>`
I have hope that things will improve on this front when `impl Trait` lands.
EDIT: After re-reading this, I want to add that I don't mean to hate on Tokio. I like the basic design very much, and hope that they can work out the ergonomics issues and stabilize the API soon.
>I see. Rust is aiming to be closer and closer to C++ every day!
Or you know, it's aiming nothing of the short, and this is early, still unsorted, behavior, while the language has been simplifying things (e.g. the early sigils and lots of other stuff), and plans even more simplification and friendliness.
I've definitely seen much worse with C++. These kind of errors you get though when you use Tokio and combine many different futures together, the future wanting to have static lifetimes and me using a reference to self inside an async block, hopefully solved this year.
The grievances you and the article's author mention seem less to do with Rust itself, and more to do with this seemingly horrible futures library. As far as I can tell, it's still in the rust-lang-nursery, which is an indication it's not ready for prime time yet.
Don't get me wrong, it's a great library! You can do pretty darn fast systems with it, all type-checked and correct. Just don't try to fit in too many things into one thread yet, wait for async/await and non-statical lifetimes in the core.
I hope async programming doesn't become the standard in Rust. So much work has gone into allowing clean and safe threading, but people seem to be led towards the async libraries, which IMO solves a scaling problem only 1% of users will have. It's great that they exist, but if you're not expecting to have a c10k class problem, you can use threads and you'll probably have a better time.
If you're a library, creating threads causes side effects for the host program, and doing async when called doesn't. For instance, in a single-threaded program, it's always safe to call malloc() after fork(). In a multi-threaded program, another thread might have called malloc() and picked up a global lock; since only the thread that called fork gets copied (since you don't want to do the other threads' work twice!), there's now a copy of that global lock being held by a thread that doesn't exist, so calling malloc() will deadlock.
My personal interest in Rust is as a C replacement, including as a replacement for existing libraries that are written in C. While I agree threads can be very efficient (after all, the kernel implements threading by being async itself, more or less), they're annoying for this use case.
I just want to be able to say "do this thing or time out in 5 seconds", and that's like all the 'async' I need in rust. Everything else, nah.
But that's just me - many people need these 0 cost abstractions, this is Rust's focus, and I'll have to wait for the higher level stuff for my projects.
For a long time C++ and Javascript did not have futures. Rust is relatively new, and futures aren't part of the language proper yet. The problem is that the author's criticism of Rust seems to all hinge around a bleeding edge feature.
I would agree with the author's criticism if it was "futures are in the lang nursery and still not ready to use," rather than: Rust is bad, because I got nasty errors when using this work-in-progress library.
Rust's futures leverage the type system to have extremely minimal overhead; many of those languages don't try to do that. That's where the difficulty comes in.
If we'd all start programming in stable, mature languages instead of letting peer pressure goad us into using betaware and worse, work would be a lot simpler.
It would also encourage organizations large and small to start releasing complete, polished products instead of the "move fast and break things" crap that has infected the industry.
Your "stable and mature" languages were the crazy research projects of old. No one is forcing you to do anything, but let us not forget the nature of our "mature" technologies and the process by which they form.
This is still an unstable library (futures and Tokio are both still 0.1 release) actively being developed, in general I tend not to see crazy compile errors working on synchronous projects. FWIF I’ve made a Chip8 emulator with Rust and am working on a Z80 emulator now.
Yeah, sync projects are just pure pleasure to work with. The language is ergonomic, errors are easy to read and tooling is the best ever.
The problems right now start when you want to go async. I follow the development because I want to see easy, safe and fast way of writing async programs, and there's lots of interesting development happening with Rust.
Yeah, i have recently finished a tokio based server. Working with future combinators is very frustrating. I accidentally captured a variable in a closure(should be cloned and moved), and it didn't tell me where it happened, just an error saying requires 'static lifetime for the variable.
The new async/await stuff will help a lot with this; it'll enable borrowing across futures, which will remove this requirement and make things a lot simpler.
The last tokio re-ergo really helped me and my classmate.. now we are getting stuff done, and we are enjoying it a lot, since we are combining our connections as instances of async state-machines (which, by the way, a given state may be a sub state-machine).
I recently wrote a few projects in Rust (C/C++/Go/JavaScript/Java/Python as background), and very much like the language. My 2 cents from my endeavors with Rust
I felt like all type errors are backwards. That is, "got" was the target you are giving your type to, not the type that you are passing. This may only happen in some cases, but I just started tuning the content of those errors out and instead adjusted randomly until things worked or the message changed.
I was often getting obscure type errors that were not at all related to the issue, and sometimes the compiler just insisted that just one more burrow would do, no matter how many burrows you stack on. This is definitely because I did stupid things, but the compiler messages were only making matters worse.
String vs. str is a pain in the arse. My code was littered with .as_str() and .to_string(). I never had the right one.
Enums are super nice, but it's very annoying that you cannot just use the value as a type. My project had a lot of enums (user-provided query trees), and it was causing a lot of friction.
There are also many trivial tasks where you think "Of course there is an established ecosystem of libraries and frameworks for this", and end up proven wrong. I mostly did find one library for the thing I needed, but often immature. The HTTP server + DB game seems especially poor.
In the end, I had to quit the fun and get work done (and others did not find playing with new tools as fun as I did), so I ported the project to Go and got productive. I took a fraction of the time to write in Go, the libraries are just so much more mature, it performs significantly better than the Rust implementation (probably because of better libraries—definitely not stating that Go is faster than Rust here), compile takes 1 (one) second rather than minnutes, and there is in general just much less friction.
On the flipside, it takes about 2-3 times as much Go than Rust to do the same task, even if it was way easier to write the Go code. The code is also a lot uglier. As an especially bad case, something that was a serde macro call in the Rust version is 150 lines of manually walking maps in the Go version.
> My code was littered with .as_str() and .to_string().
PSA: If you have a variable that's a String, you can easily pass it to anything that expects a &str just by taking a reference to it:
fn i_take_a_str(x: &str) {}
let i_am_a_string = "foo".to_string();
i_take_a_str(&i_am_a_string);
Every variable of type &str is just a reference to a string whose memory lives somewhere else. In the case of string literals, that memory is in the static data section of your binary. In this case, that memory is just in the original allocation of your String.
Ah, I found that out later but had forgotten all about it. :)
I don't remember how I found out, but it seemed oddly magical until I just now read the docs: String implements Deref<Target=str>. Makes more sense now.
I still had a bunch of to_strings()'s, though, as things tended to take String whenever I had &str's. I found this to be a very unexpected nuisance.
EDIT: Maybe I needed as_str() as the & trick doesn't work if the target type cannot be inferred as &str?
FWIW, this is why we go over this stuff in the book now; lots of people struggle with it, it's not just you.
And yeah, Deref doesn't kick in everywhere, so you may need the .as_str() in those situations. It should be the extreme minority case, generally. Same with .to_string(), though moreso. Most stuff should take &str, not String.
It's relatively rare that APIs should be taking ownership of `String`s from you; the majority of the time arguments should be borrowed. I'm curious what cases you ran into most frequently that required `String`.
as things tended to take String whenever I had &str's
Functions should prefer &str or perhaps T where T: AsRef<str>. Note that if you write code that needs an owned String, you could consider taking some T where T: Into<String>, because this allows you to take many kinds of string types, such as &str, String, Box<str>, and Cow<'a, str>.
I don't remember the details, but I just recall that I ended up with a converting nightmare.
Your suggestions make sense, but I can't help but think that there is something fundamentally weird about basically having to use generic programming just to take a string arg. The most sensible thing would be "everything uses &str".
Your suggestions make sense, but I can't help but think that there is something fundamentally weird about basically having to use generic programming just to take a string arg.
Indeed, it's the hard trade-off to make every time need a string reference, do you want a function that is slightly more general or one that has an easy-to-read signature?
It becomes even more interesting when defining trait methods. E.g.:
trait Hello {
fn hello<S>(who: S) where S: AsRef<str>;
}
Has the benefit of being more general, but cannot be used as a trait object (dynamic dispatch), since it wouldn't be possible to generate a vtable for &Hello.
I generally prefer the generic approach for functions/methods that want to own a String, since using some_str.to_owned() to pass something to a method is not very ergonomic (relative compared to &owned_string for a function that takes &str). But you have to be certain that you don't want to use the trait as a trait object.
It's just a tradeoff, like any other. If you use a generic, you add complexity, but can accept a wider set of types. If you don't, things are simpler, but you accept a smaller set of types. I personally find the AsRef to almost always be overkill. YMMV.
> Enums are super nice, but it's very annoying that you cannot just use the value as a type. My project had a lot of enums (user-provided query trees), and it was causing a lot of friction.
Note that OCaml has this feature of using-a-enum-value-as-a-type (or using a subset of enum values as a type, etc.). It works very well, but it quickly produces impossibly complicated error messages.
I'd like Rust to have this feature, eventually, but not before there is a good story for error messages.
I am not very familiar with OCaml, but why would such a feature result in complicated error messages? I can only really imagine two new error scenarios: "Expected Enum::ValueA, got Enum" and "Expected Enum::ValueA, got Enum::ValueB".
Oh, and added thing that bugged me a lot: The error part of Result<T, E>. During my short time of coding Rust (I'll get back to it later), I never really found a way to ergonomically handle errors.
I find it really awkward that the error is a concrete type, making it so that you must convert all errors to "rethrow". Go's error interface, and even exception inheritance seems to have lower friction than this.
The purpose of `chain_err` here is to add on top of the previous error, to explain what you were trying to do, instead of passing up the previous error (in this case, `std::num::ParseIntError`).
If you don't like that, you can do something like this:
use std::boxed::Box;
use std::error::Error;
fn some_func(v: &str) -> Result<u32, Box<Error>> {
v.parse::<u32>().map_err(|e| Box::new(e))
}
It doesn't have to be the same type, there just has to be a suitable implementation of the 'From' trait to perform the conversion if the types don't match.
? calls a conversion function. It only won't work if the error you're trying to return won't convert to the error that's in the type signature. Even then, you have options, like .map_err.
We have gone a bit down a tangent here. My original complaint was about the friction of different error types compared to other languages (like, say, Go).
Having to implement a bunch of From traits (unless you need io::Error, because everything seemed to have conversions from/to that), or having to implement inline error conversion through map_err, is such friction. I might go as far as consider it the most cumbersome error system I have used. Clean idea, cumbersome implementation.
My comment about '?' not working with mismatched types was mostly just to say that it doesn't fix anything, it just adds a bit of convenient syntactic sugar.
There is zero friction if you use either error_chain/failure crate and you can still recover the precise underlying errors. The only thing is it allocates.
Yeah - I mean I'm only learning it as a kind of hobby at the moment but, working as a Java programmer, I find it incredible how helpful the compiler can be. It even gives you little helpful syntax hints and stuff.
Writing Rust code isn’t that hard, sure. The annoyences start when you try modifying code or moving things around.
Prototyping and editing code makes for most of my work, and Rust makes that a chore. That’s my main gripe with the language. It’s more like moving through molasses than encountering a brick wall.
It's interesting how perspectives differ; I love refactoring Rust code more than any language I've ever used, as it catches so many of my errors when doing so for me, at compile time.
I love Rust when it's refactoring time, the compiler essentially spits out a checklist that you just need to work through. And once it's done complaining it feels pretty confidence inspiring.
But I'll agree that Rust is unpleasant for prototyping. What I find myself doing a lot when starting out a project is just figuring out if some snippet of code will work. There's no REPL to just run it in. Then I have to either set up a scaffolding project just to run it, or just shove it somewhere along the working path and move it to it's real spot later. Except sometimes that messes up the borrow, or the signature and I have to decide between temporarily altering my working code to accommodate this small test or writing mode code without testing to reach the next test point.
And after everything works with your scaffolds and shims, you have to rip it all out and put your snippet where you wanted it in the first place. And then fill in all the gaps that prevented you from testing that snippet where it is in the first; hopefully it works, otherwise you're backtracking and rebuilding scaffolds you just ripped out.
There are times I just want to write a function and not declare return type, and not have the compiler complain about non-exhaustive matching cause there's only one usage and it's output is going straight into a `println!("{:?}", thingy)` anyway.
I guess the C++ equivalent is: I know when the code reaches this point it'll segfault and blow up, but I don't care because if it made it that far that means the thing I'm prototyping ran and gave me some feedback that I could act on. Rust just forces you to write everything instead of just things up til the prototype point.
Do you know about unimplemented!()? I use #[test] functions for what I'd throw into a REPL and if it's not horrible you can keep them as actual tests later.
That's really nifty! Not quite was I was thinking of though, but it gave me a starting point for some research before I came across https://github.com/rust-lang/rfcs/issues/1911 which describes why unimplemented!() doesn't quite work and what I'd like instead.
Ever tried changing an owned field in a struct to a borrowed one with lifetimes?
Be prepared to change not only the struct and its fields but also everything else where it appears.
Yes, the compiler will catch your mistakes, but no, it's not something I love.
I think the Rust devs like C++'s abstraction level. Rust is about the same (more modern), it just catches your errors. I don't think it's possible to make Rust as ergonomic as Python, but I do think it's possible to move a little in that direction without losing Rust's strengths.
Actually that is the goal of the other languages, Swift, Haskell, OCaml, ParaSail...
Influenced by Rust's success bringing affine types into mainstream.
To keep using automatic memory management as their default way of managing memory, while offering some escape hatches based on affine types for low level optimizations.
Indeed, this is painful. Plus that lifetimes tend to percolate through the codebase. This is logical and necessary, if a struct is bound to a lifetime, then another struct holding this struct is also bound to that lifetime. It can be painful regardless ;).
> All it took was learning how and when to borrow.
That's disingenuous. It's really not as simple as you're trying to make it sound. But I don't understand the point of this guy's blog post, unless it's just to whine. There's so little real content in his post.
> As someone who has been programming in Rust for nearly a year, even for commercial purposes, this article is baffling to me.
The author introduces himself as having a Python background, and as someone who experienced some challenges wrapping his mind around SQL. I wouldn't expect anything different.
I write hardware drivers (mixed kernel/user-mode) for 100Gb/s FPGA network accelerators at work in C and C++ for 3 different OS's. I'd argue that I am very much the target audience for Rust.
And I also find that SQL can be quite infuriating to deal with, and had a high-friction experience with Rust.
Wait, what? Rust has what I consider the best documentation I've seen of any language. The docs explain things at a high-level, but concisely, and have numerous examples. The formatting is good, the keyboard navigation support is good, it's well-linked, and it has convenient features like links to the source and the ability to collapse everything but method headers for easier browsing. And it's extremely easy to add docs to your own project.
Maybe there are some dark corners filled with poorly documented unstable APIs that I haven't seen?
I wrote this piece hastily, and it really wasn't intended for this broad of an audience — I won't redact the existing wording I still think it's roughly right, but I do regret a lot of it.
Rust's docs are amazing in certain contexts — the book is great, the built-in support for documentation on types/functions/etc. is amazing, and compiling code examples are a very laudable idea.
What I'm speaking to here is more once you get into the broader ecosystem and start using a lot of non-core libraries. Oftentimes I found that the front page docs explaining the basic premise were concise and well-written, but that things got quite a bit harder when you started diving into individual classes and functions (put another way, as you started deviating from the happy path). This doesn't apply everywhere, but often the comments are very minimal and the documentation relies heavily on "types as documentation" in that there's a big list of all the traits and the functions on those traits that are implemented. In many, many cases there's little in the way of detail or examples.
I've written quite a bit of Rust now and have used many of the headliner projects. So far there have been very few crates where I didn't have to resort to eventually checking out the source tree and figuring out how to do some particular thing by examining its source code and test suite. I won't call out any single project in particular, but I found this to be the case in every one of `actix`, `clap`, `diesel`, `error-chain`, `horrorshow`, `hyper` and `juniper`, just to pick a few from the top of my `Cargo.toml` (it also happened with many other libraries). It's great that you can do this and open source is awesome, but ideally I could get by on just documentation, which is what you can do in many other languages.
Unfortunately, read with little context, it sounds like the tone of my piece was intended to crucify, but it's not. It can be simultaneously true that Rust's docs can still use lots of improvement and that the Rust team is doing an amazing job of improving them (there's just a lot of work and a long way to go). Both these facts are true with Rust.
I take documentation bugs seriously. If you did this with any of my crates, please file bugs. My guess is that other crate authors might feel the same, and that they would also appreciate bug reports.
Writing good docs is super hard, because in order to do it well, one must sink themselves entirely into the perspective of someone who is seeking answers. This is hard when you already have the answers.
> I take documentation bugs seriously. If you did this with any of my crates, please file bugs. My guess is that other crate authors might feel the same, and that they would also appreciate bug reports.
Will do! For what it's worth, I also had `chan` in dependencies list, but didn't list it above because it's one of the minority where the docs and examples were very good.
More generally, I feel quite bad for complaining about things instead of going in to improve them (through bug reports or patches), but there has to be a balance between making forward progress and stopping to help shore up the tooling. That said, I haven't been doing enough of the latter lately, so I'll make more of an effort.
Yeah, totally, I hear you. I certainly don't file issues for everything I should either. Mostly, my aim was to make it clear that documentation is one of my priorities. It isn't everyone's priority, so some might think that filing documentation bugs isn't wanted or something. But they definitely are, for me at least!
> What I'm speaking to here is more once you get into the broader ecosystem and start using a lot of non-core libraries.
Don't even know how many times I've had to dig into the python sources to figure out why something wasn't working as intended...
My favorite: I was trying to get memory buffer working for an object (following the official docs) using the C-api and it just didn't work no matter how much I fiddled with it so I go digging through the python sources and find out the fully documented feature I was attempting to use wasn't even implemented. Well, half the PEP was implemented.
I'm probably just funny that way since if I can't figure something out from docs I just go read the sources.
Writing good docs is hard for sure! Especially for developers, you just want to write cool code :) but take into an account lifetime of the projects, compared to python all of them pretty young. For example I started actix-web just 5 months ago, sure it needs more documentation
I wonder idly if it would be useful to have 'cargo doc deps' that would generate docs for all your dependencies (or top level dependencies) in a project.
The rust docs are super useful, but not everyone bothers to publish them, and people tend to forget to pit full working examples in them.
Far far far better documentation. The Rust documentation is a mess and difficult to read for many people, but many involved in Rust seem to deny that it is a problem.
Docs team lead here. Specific feedback on improving the output of the docs is absolutely, 100% welcome. Without knowing what "it" is, I can't say if we're "denying that it is a problem."
We are constantly tweaking the layout of stuff, and have some larger plans on the way as well.
FWIW I find the book very easy to follow and understand, but the auto-generated docs to be kind of confusing.
I think because:
a) the general layout feels a bit weird to me. I can't work out which bits are important and which bits aren't. There is no table of contents or similar structure
b) as I'm still learning Rust the function signatures are often black magic to me, which adds to the confusion
This is a huge page, and it's overwhelming with no table of contents, or without things I probably don't care about collapsed. It is not clear to me how to use Box from this page.
Specifically for Box there is a code example on how to create a Box (great) but no indication of how to use one. Turns out you just use it and it works transparently like T would. Unlike a monad like Option or whatever. I don't know if this is obvious is you're an expert looking at the Struct signature at the top, but it's not obvious to me. It's also not obvious from the module documentation: https://doc.rust-lang.org/std/boxed/.
Then sections are broken down into different implementations against Box. Is that helpful? As a noob I don't know why that matters, I primary want a list of functions that I can interact with.
I see a lot of the function signature contains links, specifically to any other struct / trait / whatever. This is really helpful. It would also be great if there was some way of getting up to speed with parts of the structure I don't understand as well, as Rust has that Scala / Haskell trait of crazy complicated function signatures. I realise putting links on all bits is infeasible, so perhaps that is a documentation page itself, similar to how SQL structures are documented: https://www.sqlite.org/lang_select.html
Thanks! This is helpful. It’s true that the API docs are mostly written from an “I know Rust” perspective; but some of these kinds of things wouldn’t harm that.
> Is that helpful? As a noob I don't know why that matters, I primary want a list of functions
Yes, as it lays out the requirements for each one. Not every method is always available; it depends on what’s in the box!
For the languages I've used (Node and Python), I'm just comparing the ease of finding documentation on the language homepage. A typical flow is: Homepage > docs > reference/api
All 3 of these pages are blatantly obvious index pages. For Rust, this is where I assume you're supposed to end up (there are way too many links on the "Documentation" page, I'll address this at the end):
You have to navigate/scroll to see the actual list of modules/types/macros or minimize the "about" section. The first thing that I think I should see on an API docs page is an index with maybe a very short blurb/link in regards to the prelude. If the API reference needs a "How to read this documentation" that is long enough to block you from seeing the index itself, something is wrong. Any user that clicks into an API specification either knows how to read it or knows how to click a link that says "about these docs". If they didn't, they would likely be in the "learning" section. Beyond that, the section has a list of 4 links, 3 of which are anchors (that are both out of order and already listed on the left nav) while the prelude link goes to a completely separate page.
To address the Documentation landing page, you have "Learning Rust" and "References". Learning Rust > The Rust Programming Language and References > Syntax Index effectively both go to the same page. At the bottom you have a "Project Policies". Are those really part of "Documentation"? I generally think of "language" documentation, not "organization" documentation when I see the header. I would expect a documentation landing page to be a lot more minimal and easier on the eyes, similar to:
What that page has in common with almost every other language documentation page that I just visited: Bullet points and/or indentation.
EDIT: I should add that I browsed around various language pages while writing this. To add some context for my opinions, I found the Elm docs to be far and away the easiest to navigate excluding the fact that they don't have links to the source code per method. I made it to their API docs without ever hitting an incorrect link. Julia was similar, but I mis-clicked on the manual rather than the standard library because everything is on the same page and neither section collapses.
> The wall is impassable until a point that you make a key discovery which lets you make your through. Often that discovery is a eureka moment – it felt impossible only hours before.
These moments are my favourite, or at least most memorable, parts of learning. Which is what attracts me to hard, important concepts because they tend to be the most rewarding once you begin to grasp it.
I'll never forget when SICP's connection between programming and abstraction started to really click, after much effort, and the way it blew my mind, despite having programmed for a couple years. Really grasping the fundamental concept took the whole experience of programming to the next level - and made learning other things easier like a rolling snowball.
The fact the author's walls never got "smaller" is a real problem, that can be disheartening.
That said, brick walls aren't always bad things. Although it's often hard to tell whether it's a reflection of a poor design/structure of the thing you're learning, or just the learning material, or the learner themselves. Then there is the question of "necessary evils" of steep learning curves which, who knows, might be required entry-fee for a truly great language or tool (see: Emacs/Vim).
Haskell was much the same way for me. Each cliff I climbed had a new big cliff waiting. It got me into learning not just pure FP but also lambda calculus/set/category theory. It felt never ending. I ultimately never went "all in" with Haskell (maybe because of this) but what I did learn has been very useful in my non-Haskell programming elsewhere, so it's not all for naught. But it was admittedly a hard and significant time investment which isn't for everyone... nor necessary for being a productive programmer. But I don't regret it.
I am currently learning Rust and I feel this intensely[1].
I really want to like Rust, but I feel like the way they cope with no GC (lifetimes, borrowing) fights me at every turn, and really simple situations in other languages[2] become these intensely painful situations. Every time you think you've worked out how to fix a problem you find while you've fixed that one you've actually created 2 more.
Want to have a data structure of variable size (eg a struct with an Vector in it)? You can't do that, structs have to be fixed size. OK so I'll make it a reference to a Vector. That's great, but now you can't have a factory function because the lifetime goes out of scope. OK so I'll wrap my reference in a Box. OK that's great but now your OTHER reference: a trait (because traits are also of unknown size) is complaining. OK I'll wrap that in a Box as well. Sorry, you can't wrap this trait in a box because before your trait requires implementors to implement copy because at one point you have to use the trait in more than one place and references break the lifetimes and--- THROWS LAPTOP OUT WINDOW
I'm going to keep chipping away at it for a bit longer, but I can feel my interest waning.
[1] Except for the bad docs bit, I really like the docs, and the error messages are definitely earnest in their attempts to help you.
[2] that I'm familiar with, which are no languages that don't auto-GC
-- EDIT --
To everyone taking the first line of my intentional rant paragraph and pointing out it works fine… you're correct, I am mistaken! Let me paste the reply I made to the first person:
---
You're absolutely right. What I really meant was that you can't have Vec<T> where T is a trait without also wrapping that in a box, which for me in turn doesn't work for a bunch of other reasons.
In any case, my point remains the same: it is very challenging-- at least for someone used to just creating data structures and letting GC handle it-- to build code that does what you want, and you spend large amounts of time "fighting" the compiler. I am OK if people wish to peg that on me being stupid or whatever, it doesn't change the core point: it's hard for new people to get into, and if you're wanting there to be less Electron apps and more native apps things like Rust being easy to use seems important for that.
It's important to note that in C++ you can easily run into problems with what you're describing: structs (objects) with different sizes being put into a vector. You may very well get the program to compile, but then run into odd bugs which come about because your objects get clipped to the size of the smallest possible (the base class). So any overridden methods which expect extra data in a subclass will not behave as expected.
So what you usually do here is have a pointer and a VTable and all that jazz. But there's been a resurgence in interest in putting data into contiguous blocks of memory. Check up on data driven design.
I don't think it's fair to compare GC'd languages to Rust's complexity. At least, compare C to Rust. But still, to be truly fair, weigh the usability differences against the safety differences. There are a lot of trade-offs here and perhaps this isn't the right way for you to go forward, but keep a broad view of the other aspects at play.
> It's important to note that in C++ you can easily run into problems with what you're describing: structs (objects) with different sizes being put into a vector.
This is simply wrong.
> You may very well get the program to compile, but then run into odd bugs which come about because your objects get clipped to the size of the smallest possible (the base class).
That has nothing to do with object sizes. You're describing the object slicing problem
> The cause of this problem is ignorance and incompetence regarding fundamental aspects of the language.
No! You can rationalize any language problem away with this argument. That's why it's a fallacy with a name: "no true Scotsman" (as in: "no true C++ programmer would let object slicing introduce a bug").
> No! You can rationalize any language problem away with this argument.
Sorry, the sole problem is incompetence. Coupling incompetencr with the inability to learn is a problem that no programming language solves. If you insist in shooting yourself in the foot, it makes absolutely no sense to complain that the gun is broken because it doesn't guide bullets away from your foot when you intentionally aim at it.
The object slicing problem is C++ 101. It isn't the language's fault that people who don't know the very basics end up doing mistakes.
"Don't use heap memory after it's been freed" is C++ (and C) 101 as well. Yet if the bar for C++ competence is "never wrote a use-after-free", then your bar for "competence" excludes essentially everyone in the industry.
Whether a theoretically "competent" C++ developer makes mistakes related to object slicing is a completely uninteresting debate. Whether experienced C++ programmers make that mistake in practice is the interesting question. And they do.
> Yet if the bar for C++ competence is "never wrote a use-after-free"
The bar for C++ competence in that regard is RAII.
Furthermore, your strawman doesn't hold up. The object slicing problem is actually like someone being foolish enough to cast a 64-bit int to a 32-bit int and then complaining that the programming language is broken because the 32-bit variable doesn't hold 64 bits. Of course it doesn't. Why should it? And why make excuses an play the pin-the-blame game when the problem is caused by ignorance? Heck, you're assigning heterogenous objects of variable-length to a data structure that by definition was designed to hold homogeneous sequences of fixed-size objects, and even still you want to pin the blame on the language? How, if you clearly don't know what you're doing?
(KungFuDeathGrip is the pattern of introducing an extra RAII refcount incrementer/decrementer on the stack to avoid subsequent method calls doing something that would free an object from underneath the current stack frame.)
If you think about this as a correct/incorrect thing, you're going nowhere. There's this usability concept that so many people resist to apply to dev tools as if programmers were not regular humans.
RAII only works for stack or static global allocated resources.
So if the heap allocated data isn't hidden behind some kind of smart pointer, RAII won't help.
Which unless one writes 100% of the code, or there are strict code reviews in place, there isn't any guarantee that all heap allocations are guarded with RAII based handles.
Okay, sure, if you want to get into the nitty gritty, that's fine. But I just wanted to provide a high level overview of how putting differently-sized structs into a vector can be problematic.
Sure, ignorance, whatever. Indeed. That's why I brought it up.
But my greater point still stands: do you want the compiler to catch these things? If so, you have trade-offs.
> Okay, sure, if you want to get into the nitty gritty, that's fine. But I just wanted to provide a high level overview of how putting differently-sized structs into a vector can be problematic.
A vector is a container for homogeneous sequences of fixed-size objects that are stored contiguously. Even if we ignore the slicing problem, don't you see a problem in trying to shove sets of square and rectangular pegs into a container that was specifically designed to support only round pegs of one specific size?
There are times when you need to do something conceptually like having a list of things of similar, but different kind. There are different ways to go about this. Briefly mentioned in my original post: object pointers; or breaking up the data according to DDD (just because something is conceptually one item, does not mean it cannot be spread into different data structures. after all, if you have an array full of things, there's likely one property in common between those things of different kind that is similar and germane to their being in the collection).
> So what you usually do here is have a pointer and a VTable and all that jazz. But there's been a resurgence in interest in putting data into contiguous blocks of memory. Check up on data driven design.
> It's important to note that in C++ you can easily run into problems with what you're describing: structs (objects) with different sizes being put into a vector.
Baloney. In C++ vector<any> or vector<variant> accomplish this task without any problems whatsoever.
And you can do the same in Rust with Box<Any>. What Rust doesn't have is the object slicing gotcha, which is probably a language design mistake in C++.
1. Automatic reference counting is a really good alternative to GC. It takes a little bit more book-keeping, but the performance characteristics are predictable since allocations/frees are handled along the way. Many GC implementations require execution to be halted while the reference graph is traced, which makes it a non-starter for applications trying to deliver predictable real-time performance.
2. Most of the limitations in Rust you're lamenting are there to ensure that your code is safe and performant. Rust requires adapting to very different design-patterns than you might be used to to get these benefits: if something is hard to do in Rust it's probably an anti-pattern with respect to memory performance or safety. Then again if performance isn't your main concern, maybe you don't need Rust.
Yes, slower than modern GC, but predictable and deterministic. Note that the parent comment never claimed it was faster, just that it avoids 'stop the world' which can be a problem in realtime contexts (e.g. games, audio).
Reference counting isn't inherently more predictable than tracing garbage collection. Take the example where your thread is the last one to deref a gigantic object graph. You're stuck holding the bag on traversing the entire graph and destructing it all at once, when a tracing gc could do it piecemeal (and on a dedicated background thread, instead of on your real worker threads).
If objects that hit zero refs have their reference pushed into the queue of a dedicated GC thread, is that not still called RC, or is that called something else?
Reference counting can also lead to big cascades of objects being freed, which can make it impractical in realtime context as well. This makes RC useless for systems which have hard constraints here (e.g. automotive).
And of course, once you are multi-threaded, you can more or less forget about "predictable and deterministic" with RC, too.
In contrast, there are real time capable garbage collectors.
Ehhh, except that rust's borrowing system is a really great aid to prevent those sorts of things. And, one can easily make a thread responsible for deallocating the cascading objects. The point being that reference counting allows one more control over how deallocation is done.
Besides, the real time capable garbage collectors are basically just backed in implementations of exactly that. With reference counting, it can be written in the language itself, rather than as a tunable external part of the implementation, which is useful.
The problem with reference counting is that it don't work with recursive structures and counters may overflow. Overflow or reaching some max value and returning runtime error are both problems.
On 64-bit platforms you're not going to overflow the counter. 32-bit platforms these days are embedded & they too are highly unlikely to overflow the counter.
Recursive structures are a problem although in my experience you can usually just alter the ownership rules a bit instead.
Of course it always depends on context, but doubly linked lists are often far from optimal in terms of performance when it comes to sequence-like data structures.
A naive implementation of a doubly linked list is probably going to mean you are allocating list members one at a time, and over the course of execution you may be re-ordering items, inserting items in the middle of the list or adding items over time. All of this probably leads to memory fragmentation in a data structure you are probably going to be iterating over, which is far worse for performance than allocating a contiguous block of memory of a fixed size.
So yes, Rust may be pushing you away from implementing naive linked lists by making that more complicated. But if you care about list performance (which you very well may not in your problem domain), you probably should not be implementing your own sequence data structures. You should be using a mature, optimized implementation which, under the hood, is probably implemented using those "hard" design patterns which Rust is designed to make easier.
struct Task {
next: *mut Task,
prev: *mut Task,
regs: TaskRegs,
// other shit here
}
is the direct translation.
> Or are doubly-linked lists antipatterns now?
They're often not what you want, yes. But if you are in that situation, in the worst case you're in the same place as C, and you can write the Rust the same way if you'd like.
Doubly linked lists are impossible in safe rust by definition since the only safe way to modify a memory location is via a &mut reference. And Rust guarantees that that any memory location can only be pointed to by only one &mut reference. A doubly linked list by definition has two pointers pointing to each node, so no, your example will not work.
If it has no protections then why even use rust? You cannot both claim safety and then claim versatility by abandoning said safety. Because without your claimed safety, I might as well use C and not do battle with my compiler.
The claims of safety and versatility are not in opposition. There are cases where the compiler can't prove what you're doing is safe, so take the guards off and do it yourself. In the 99% case (I've literally never written unsafe Rust), you leverage its tooling to better communicate intent and safety.
Clinging to an exceptional case and pretending it's the rule doesn't hold in practice.
> To counter a statement of the form "nothing of type X is good", only a single example is needed.
“If something is hard in Rust, it is probably an anti-pattern” (emphasis added) is not equivalent to a statement of the form “nothing of type X is good”. It is equivalent to the form “most things of type X are not good”, which cannot be rebutted by a single example.
Minor point: you can have Vectors in structs. Vector is a fixed-size type on the stack so it works fine. And other types in structs too. EDIT: sorry, I see you got to that before I'd finished this post :)
Rust is a language where you have to kind of take a step back before working with it to read about the design tradeoffs and why they were made. Certain things you're used to doing with other languages just won't work (e.g. doubly linked lists), and trying to coerce the language into being something it's not leads to you going on one of those frustrating circular rabbit hole journeys you described so nicely above, usually ending up with laptop defenestration.
I'm a week into Rust. I find like you the compiler quite helpful, the docs comprehensive but a bit impenetrable, and the ownership & lifetime system logical on its own terms but very, very unnatural to learn. I don't actually know if its worth it yet.
I am really, really enjoying the O'Reilly Rust book ("Programming Rust"). Maybe sign up for a 10 day Safari trial (free, no card) and give the first few chapters a read. It may help reset you.
> lifetime system logical on its own terms but very, very unnatural to learn
That is a succinct explanation of it, yes! Like I can read the documentation and nod along, and the rules seem simple. Actually then remapping your brain and how you want to achieve things seems incredibly challenging
Actually then remapping your brain and how you want to achieve things seems incredibly challenging
The thing that I have come to appreciate more strongly is that you actually have to think about the same ownership rules in other languages to write bug-free code. Consider e.g. the following Go code:
Whether a subslice is still referring to the same array as the slice that it was derived from depends on whether there were any appends that required allocation of a larger backing array. Whether/when this happens is an implementation detail.
Rust prevents this ambiguity by not letting you borrow a `Vec` simultaneously as immutable and mutable (or multiple times mutably). Since the `push` method takes `&mut self`, you cannot have any mutable/immutable slices of the same data.
Another example. If you take a slice of a large array and only use that slice from there on, the slice will (obviously) prevent garbage collection of the backing array [1]. Again, Rust's ownership system prevents such memory leaks, since a slice reference cannot outlive its (owned) `Vec`.
I would argue that the cognitive load in other languages is actually higher if you want to write correct code (except perhaps in languages with immutable data structures, such as Haskell), since you have now compiler helping you out. It's just that other languages allow us to cheat when it comes to ownership. For worse or better.
Well, that's down to practice. And on that subject...
One thing I like to do with a new language is to re-implement a self-contained but non-trivial program that I've built before. I have a few but my favorite is a genetic algorithm which breeds rule sets for a cellular automaton to get the CA to solve some simple computation problems. It's not at all complex, a few hundred lines when done in Python, but it's more than a tiny example. I like it for learning new languages as it touches on various data structures, it's inherently parallelizable, it's not especially bound to any particular programming paradigm, it lends itself to exploring various useful concepts and constructs (function pointers, closures, recursion, etc.), and it is fun and interesting to work on.
So maybe you have something similar, something where you really understand the problem and the solution, freeing your brain to focus entirely on seeing how it would map to Rust.
I have been looking for such a problem to use as you to, to kick the wheels of languages. I would like to also include some basic linear algebra and/or signal processing to test numeric stuff. But do you have an example upbon github or similar? I've got a couple decades of c++ experience and I'd like to move more into rust for my home projects but I haven't had one that really tested me with data structures and threads.
I'm still in the early stages with this one in Rust. I spent a lot of time this weekend on it and I'm finding no exaggeration in those stories of fighting the borrow checker! It's been a pig, honestly. I am OK with the point of it all, if anything it's made me more aware of limitations of not taking this strict approach in other languages. I can see all sorts of possible bugs that can't happen in Rust. Doesn't make writing it any easier.
I'm starting to think that Rust is really just a clever set of constraints that forces good behavior on the programmer.
Happy to throw the Python stuff over the wall at you although it isn't especially pretty (it was my 'learn Python' project). I'll put it up on Github this week & ping you back here.
> Want to have a data structure of variable size (eg a struct with an Vector in it)? You can't do that, structs have to be fixed size.
I'm unclear what you mean here, because Vecs in Rust do have a fixed size. You can see this by using std::mem::size_of on a Vec: for any type, a Vec is three words in size. You can see this documented in the stdlib documentation for Vecs: https://doc.rust-lang.org/std/vec/struct.Vec.html#guarantees
"Vec is and always will be a (pointer, capacity, length) triplet. No more, no less."
So what you're asking for, a Vec in a struct, works just fine:
(Responding to myself to reply to the parent's edit)
> You're absolutely right. What I really meant was that you can't have Vec<T> where T is a trait without also wrapping that in a box, which for me in turn doesn't work for a bunch of other reasons.
Ah, it sounds like you're using traits as types directly, which is very much discouraged by Rust (especially in conjunction with taking references to those traits). What the language really prefers for you to do is to use traits as bounds on generic types to get rid of the dynamic dispatch and the consequent complications with lifetimes. The only time I'd suggest using traits in the manner you've described is when you need a heterogenous collection, which isn't common in my experience. You've just so happened to stumble across one of the patterns that I most suggest beginners not to do. :P
> In any case, my point remains the same: it is very challenging-- at least for someone used to just creating data structures and letting GC handle it-- to build code that does what you want, and you spend large amounts of time "fighting" the compiler. I am OK if people wish to peg that on me being stupid or whatever, it doesn't change the core point: it's hard for new people to get into, and if you're wanting there to be less Electron apps and more native apps things like Rust being easy to use seems important for that.
I hope that nobody here's making you feel stupid, that would be pretty silly. I've been helping people learn Rust for a long time and it's indeed common for people coming from GC'd/dynamic languages to feel like things are pretty alien (to some degree attributable simply to the differences inherent to systems programming). I as well came from Java/Python and found that there were enough people in the Rust community with that same background that there's no air of elitism suggesting that one ought to feel like a moron for e.g. not knowing what a pointer is. We're all here to help each other, eh? If you ever want to give learning Rust a try again and get stuck, feel free to come ask questions on #rust at irc.mozilla.org or reddit.com/r/rust.
> What the language really prefers for you to do is to use traits as bounds on generic types to get rid of the dynamic dispatch and the consequent complications with lifetimes
Isn't this basically equivalent to only using templates in C++ code ? and doesn't it kill build times & prevent reusability across different shared objects ?
Dynamic dispatch will surely always be faster to compile than static dispatch, but static dispatch has crucial runtime performance advantages (e.g. it enables inlining, which is the ultimate meta-optimization). And extreme runtime performance is one of Rust's raisons d'etre. Having fast compilation is surely a worthwhile goal, but, for Rust, not if it comes at the expense of runtime performance.
What do you mean by "prevent reusability"? There's nothing about static vs. dynamic dispatch that prevents code or types from being used across different compilation units.
I read that as being about reusing the implementation - the actual machine code bytes which encode a function. If i call a function in library which uses dynamic dispatch, the compiler just emits a few instructions to jump to an existing implementation in the library. If i call a function in a library which uses static dispatch, the compiler will include the relevant monomorphisation of the function, and any other generic functions it calls.
> If i call a function in a library which uses static dispatch, the compiler will include the relevant monomorphisation of the function, and any other generic functions it calls.
Indeed, and that's what precisely one wants if they've chosen static dispatch since the alternative approach is opaque and inhibits optimization. Inlining across compilation units is a rather important feature! I don't see how that prevents reusability, though?
> Ah, it sounds like you're using traits as types directly, which is very much discouraged by Rust (especially in conjunction with taking references to those traits). What the language really prefers for you to do is to use traits as bounds on generic types
Sure thing. [Preemptive postscript: damn this got long. TL;DR don't use trait objects, just use generics. You'll thank me.] Here's the setup: we have several different types, and those types implement the same trait (think of it like an interface from other languages).
// Define two different types
struct Chihuahua;
struct GreatDane;
// Define a trait with a method
trait Bark {
fn bark(&self);
}
// Implement that method for both types
impl Bark for Chihuahua {
fn bark(&self) { println!("woof") }
}
impl Bark for GreatDane {
fn bark(&self) { println!("WOOF") }
}
Using instances of these types looks like so:
let rover = Chihuahua;
let marmaduke = GreatDane;
rover.bark(); // woof
marmaduke.bark(); // WOOF
Now say that you want to write a function that accepts any type that implements the Bark trait. As I mentioned before, there's two ways to do it: the static way, and the dynamic way. Here's what both versions of the function look like:
In the first one, there's a generic type (the "T"), which we have bounded by the `Bark` trait. At compile-time, for each different type that you use with this function it will generate a new copy of the function with "T" replaced with whatever type you actually used (this might seem excessive, but it's crucial for further optimizations).
The fact that it's so easy to use these functions is what we mean when we say that Rust "prefers" static dispatch. I'll come back to this in a moment.
For the dynamic version, it's different because there's no generics at all. Instead, the function is just taking a normal parameter of type `Bark`. Looks simple, right? In fact, it even looks simpler than the static version! The illusion of simplicity is what makes this so pernicious to beginners. In fact, I've lied to you completely: despite seeming like this should work, it doesn't even compile. That's because, unlike many other languages, Rust doesn't heap-allocate (or "box") things by default. It has to pass function parameters, unboxed, on the stack. And trying to generate a single version of a function whose parameters have unknown size is pretty fundamentally unsafe.
So we have to give this parameter a size. If you're coming from a high-level language, even this is already probably an alien concept (especially since "size on the stack", which is what we care about here, isn't the same thing as "total size of every memory allocation this type might transitively point to").
Anyway, we give this type a size by sticking it behind a pointer. There are many different pointer types we can use depending on one's need. The simplest is probably `Box`:
Of course, using a `Box` implies a heap allocation, and, since Rust loves speed, it also loves to prefer stack allocation to heap allocation. So what you might actually want to do instead is use a reference, which will let you avoid the heap altogether:
fn speak_dynamic_ref(dog: &Bark) {
dog.bark();
}
Now you have a function that takes a stack-allocated reference to a stack-allocated vtable. There's still two pointer indirections to calling `bark()`, which isn't great, but at least we've gotten rid of that heap allocation.
It doesn't end there, though. If you try to just call `speak_dynamic_box(marmaduke)`, which is how easy it was for `speak_static`, the compiler will error. That's because `speak_dynamic_box` doesn't take a `GreatDane`, it takes a `Box<Bark>`, which isn't even close to the same thing. So you have to call it like this:
speak_dynamic_box(Box::new(marmaduke) as Box<Bark>);
Not only do you have to box it up manually, but you have to cast it into a trait object. Not pretty, and definitely not worth avoiding generics for.
And all this is still understating the restrictions on trait objects. For example, once you cast to a trait object, you can't cast back to the original type (the original type is lost, and if we let you cast back then you'd be able to turn `rover` into a `GreatDane`!). Furthermore, because of various inherent restrictions to how vtables work, not all traits can even be used as trait objects (and trying to explain the technical justification behind these rules, known collectively as "object safety", is enough to make anyone's eyes glaze over). Furthermore, getting back to the `speak_dynamic_ref` example, this only looks as simple as it does (and it doesn't really look simple) because of how simple our example is. If you try to expand this example into anything useful, then you quickly need to really know what you're doing with lifetimes lest you fall into despair.
To summarize, trait objects are an advanced feature that should only be attempted by people who need dynamic dispatch. Rust is designed to favor static dispatch. Don't be fooled by the apparent simplicity of defining functions or structs that take traits as types. In fact, in the near future we'll be introducing a new keyword to make it absolutely clear when trait objects are being used, solely so that new users don't fall into the trap of thinking that they're a simpler path forward than generics.
> Not only do you have to box it up manually, but you have to cast it into a trait object
There's an implicit coercion from Box<T> to Box<Trait> if T impls Trait, if it's 'obvious' that such a coercion is required, such as passing a Box<T> as an argument with type Box<Trait>.)
You can, but depending on what you actually want to do with that struct it can be pretty grotty to instantiate. Very much not beginner territory. I believe there's currently an approved RFC for making it easier to work with dynamically-sized data.
> I really want to like Rust, but I feel like the way they cope with no GC (lifetimes, borrowing) fights me at every turn, and really simple situations in other languages[2] become these intensely painful situations.
Lifetimes/borrowing aren't for dealing with no GC, they are for dealing with a number of issues (e.g., shared-state parallelism) that GC doesn't help at all with.
> In any case, my point remains the same: it is very challenging-- at least for someone used to just creating data structures and letting GC handle it-- to build code that does what you want, and you spend large amounts of time "fighting" the compiler.
In a sense, I think that's intentional with Rust. Not unnecessary difficulty, but Rust forces a lot of complexity that would otherwise be easy to overlook and cause runtime bugs to be dealt with upfront by the developer.
For the domain Rust aims at, that probably makes writing correct code easier on balance, but it does make lots of simpler cases harder and higher-overhead than they would be in Python, or even Java, or even in some cases C++, which also isn't GCed, but still leaves a lot of what Rust bakes into static compile-time checks as runtime footguns.
There's nothing at all preventing you from having a Vec in a struct, and the use cases for variable sized structs are pretty rare. It sounds more like you conflated having a different problem with having a Vec be in a struct and stuff like that poisoned your further attempts to understand the language
You're absolutely right. What I really meant was that you can't have Vec<T> where T is a trait without also wrapping that in a box, which for me in turn doesn't work for a bunch of other reasons.
In any case, my point remains the same: it is very challenging-- at least for someone used to just creating data structures and letting GC handle it-- to build code that does what you want, and you spend large amounts of time "fighting" the compiler. I am OK if people wish to peg that on me being stupid or whatever, it doesn't change the core point: it's hard for new people to get into, and if you're wanting there to be less Electron apps and more native apps things like Rust being easy to use seems important for that.
It fundamentally doesn't make sense to have a Vec<Trait>, because Vec expects each of its elements to be the same size (otherwise you couldn't index into it in O(1)), but different types that implement Trait can have different sizes. Other languages allow this because they automatically box everything, but Vec<Box<Trait>> should accomplish the same in Rust; I'm curious what the "other reasons" you referred to are, that make that unsuitable. Alternately, if any given instance of the Vec is only expected to have values of a single type (but the type can be different for different Vecs), you may be able to accomplish what you want with generics instead.
That said, trait objects in Rust are pretty broken in general, so - this is just speculation, but - I think your problem might actually arise from that, and the Vec<Trait> issue is a red herring.
I didn't really want to get into the nitty gritty because it detracts from the point (I considered not having the rant paragraph at all for fear that it would just be deconstructed and the exact examples becoming the focus of what I said and not what I was actually trying to say).
I can agree with the initial frustration coming from a GC-ed language until one figures out the right way to design code and structure data in a language with manual memory management. But I got over it eventually.
I'm also curious about this nitty gritty bit. I see this:
Generally, Self : Sized is used to indicate that the trait should not be used as a trait object. If the trait comes from your own crate, consider removing this restriction.
I guess you needed that sized restriction for some reason, assuming this was a trait of your own?
Yeah. So I'm following a ray tracing book that uses C++ as example code, and am using Traits as a form of interface: there is a trait of Material that has a function on it, and then there are different "implementations" of Material with different implementations of that fn, and can have arbitrary data stored against them.
I need to have them sized because I need to copy them at a point where I only understand they are a Material and not what the actual struct are, and I need to do this because I can't get a reference to work in this instance due to the fun of lifetimes.
AFAICT the way to do this without Traits in Rust would be to have an enum, and then have an external function that takes the enum, matches against it and runs different code depending.
To me the second one is kind of gross, but I may just go with it, or just give up and do something else.
Would it work to add a boxed_clone() function to your Material trait that returns a Box<Material>? (or create a BoxedClone trait if you need that pattern more generally)
Implementations of boxed_clone() could still use &self.clone() to limit boilerplate.
Alternatively, if the Materials are not going to be modified and you just need multiple references to the same object, you could replace the Box<> with an Rc<> or Arc<>.
> there is a trait of Material that has a function on it, and then there are different "implementations" of Material with different implementations of that fn, and can have arbitrary data stored against them.
Do you mean like a class hierarchy+virtual functions, or just having allocated data of arbitrary length past the end of a struct?
If it's the former, C++ fares no better in this regard but it's pretty easy to do in Rust. You just have a cloner in the interface. If it's the latter, that's pretty nonstandard and Rust doesn't make that easy for good reason. Unless this raytracer is doing something wild the code should hopefully be refactorable into a more normal struct+trait layout.
Copy and clone have specific meanings and must resolve to the type of the class itself - basically copy or cloning a class of type A gets you another class of type A always.
It's possible, from a language sense, to have an implementation of box that could directly clone the trait, but I don't believe it would have a reasonable api.
> In any case, my point remains the same: it is very challenging-- at least for someone used to just creating data structures and letting GC handle it-- to build code that does what you want, and you spend large amounts of time "fighting" the compiler.
This is a fundamental problem of languages that don't use a managed runtime. You either take the C route and just hope the user did things right or try and enforce it somehow. If you're used to writing GC-code, your problems around understanding object lifetimes are probably going to manifest as 'fighting the compiler' with Rust whereas with C they would manifest as intermittent and hard-to-track segfaults.
If I follow the github in your profile and take the Rust issue you opened, in C that mistake may never manifest except in certain control paths after you've turned off debug mode, and you'd compile code easily but sit banging your head against the wall.
I write a decent amount of Rust and write both C and C++ professionally, and in most cases Rust is drastically easier to write nontrivial code in because it catches all sorts of lifetime issues and has so many QOL improvements.
> What I really meant was that you can't have Vec<T> where T is a trait without also wrapping that in a box, which for me in turn doesn't work for a bunch of other reasons.
You can use any type of pointer to refer to a trait object, not just Box. In particular, you can use &T where T is a trait. The vtable work happening at runtime is the same, but the object can live e.g. on the stack or in a vec somewhere. Of course, if you're using references, you have to convince the compiler that the object is going to stay put for as long as the reference exists, as usual for Rust. When that's not practical, usually a Box or Rc/Arc is the go-to solution. Could you tell me more about what makes Box not work for your use case?
Aside: Trait objects are one of the more complicated features of Rust, and they run into tricky limitations (like "object safety"). It's often people's first instinct to use trait object anywhere they would've used a shared base class in some other language, but that's not usually the best pattern. Using concrete types with trait bounds (`&T .. where T: MyTrait` rather than `&MyTrait`), or inventing a new enum to hold all the types you expect, or even just trying to make ordinary composition work, is usually both easier and more performant.
> You can use any type of pointer to refer to a trait object, not just Box. In particular, you can use &T where T is a trait.
You can, though due to the extra annotations required I don't suggest that people new to the language try to use trait objects with references. (Hell, I try to keep people new to the language away from trait objects entirely, they're pretty restrictive.)
Yeah I added a followup bit probably after you wrote your comment. My suspicion now is that parent is running into the "C++ inheritance -> Rust trait objects" mismatch, and that they should probably try to use something else.
My understanding is that technically the Vec object itself is fixed size under the hood (a pointer and size field), but as far as I can tell, this is what you're after. It needs no references or boxes.
> What I really meant was that you can't have Vec<T> where T is a trait without also wrapping that in a box
In what language can you do something like that? In C++, it would compile but anything that you put in the vector would get sliced down to the base type. In Java, everything you put in would get boxed.
To nit though, I think the right thing to do regarding the vector is the tie the vector's lifetime to the struct. (I don't know how exactly to do that syntactically)
>Even something as simple as abstracting a new helper function often turns into a veritable odyssey because getting type annotations right can be so difficult (especially where a third party library is involved).
This is a sentiment I can't say that I share or even understand. You have a compiler doing inference, it will tell you what the types are if you ask?
A lot of times when I find myself writing something where I'm unsure of the concrete type -- I'll just stick a bogus type ascription in the expression somewhere. Then I run `rustc` knowing full well that it will fail. Somewhere in the error will be a message of the form "found <x> expected <y>" and now I know what the inferred type is.
>The horribly anti-user futures system, compiler messages generated by a misused macros that are second only to C++ template errors in how egregiously difficult they are to parse, universally terrible documentation and lacking examples, unstable APIs, type annotation hell, and so much more.
Given the author's dig at the `futures` crate though, I have a feeling a lot of the verbosity in errors they are seeing is due to the extremely long chains of combinators that the futures crate encourages. I haven't run into these errors myself since I'm avoiding async IO in rust until an `await` style abstraction becomes available. In my opinion: jumping into using an async library/runtime that is undergoing heavy development is probably not the best place to start when it comes to learning Rust.
I think this is one thing that Go definitely got right: having concurrency baked into the language that encourages a "syncrhonous-style" of programming is absolutely the way to go for approachability. I'm not sure that I'd go so far as to call `futures` user-hostile, as the tokio devs are doing great work, but it certainly isn't user-friendly yet.
> A lot of times when I find myself writing something where I'm unsure of the concrete type -- I'll just stick a bogus type ascription in the expression somewhere. Then I run `rustc` knowing full well that it will fail. Somewhere in the error will be a message of the form "found <x> expected <y>" and now I know what the inferred type is.
I've found myself doing this a lot in order to find the type of an expression. I stick it in a variable, add an `variable.asdf();` somewhere and look for the error message "no method asdf() on type <x>".
Perhaps someone should suggest that to Rust, if nobody already has. (A quick Google didn't show anything, but I didn't try very hard.) Or even implement it; there's a decent chance that's about as easy a compiler feature as someone could start with.
This is a gap I would like to be filled by an IDE. Specifically, I'd like the inference that tells me the correct type in a compile error to auto-complete a function declaration for me. It would make the experience of pulling code into a helper function much easier.
They're completely different kind of docs that Python's. Python's are written by hand, with many examples and tips how to use the various tools. Rust's documentation very much feels generated, and the last time I checked methods were not grouped. So vast portions of a page are consumed by variants of methods (overloaded methods?) which work exactly the same except they take different argument type. Python documentation is text with methods and code samples inbetween. Rust's documentation is like someone iterated over functions and types and printed a message for each. There's A LOT of redundancy.
That's because the trait section only shows what traits a type implements. If you want to see how to use a trait just click that trait's name for the trait specific documentation.
Sometime I don't know what trait I need, but I do know what kind of operation I want. In documentation such as Python's, I can "ctrl + F" for word similar to what I want to do and I usually find what I need. In rust, that textual description is usually hidden on the trait's page. This is probably more frustrating to beginners who don't know what most of the basic traits are. In general though, the standard library is better about this than other libraries.
The built-in Rustdoc search isn't perfect, but it should search the textual description for you. It might only search the first line? I should dig into that code again...
Vec may have examples, but lots and lots of other pages don't, and people learn by example. Python library writers add documentation similar to what they're familiar with. Rust library writers are going to document in a way that resembles typical Rust documentation.
That's why I was careful to call it different, not bad. Coming from Python it's a culture shock. They are very different ways of handling documentation, and I take into account the docs are under construction. I'll try to get more used to it, see how it develops, and then form an opinion. So far Rust docs are awkward to use for me.
They problem isn't that they're auto-generated, but that they're poorly organized or displayed (not sure exactly; hard to put my finger on). Everything on godoc.org (e.g., https://godoc.org/github.com/weberc2/httpeasy) is autogenerated and the readability is top notch.
By contrast, Python (which is my day-job language) is a hot mess. Usually everything is on one page, and it's often unclear which class's `__str__` method documentation you're looking at. SQLAlchemy's docs are absolutely awful in this regard. Further, links between things are poor and inconsistent (precisely because they're not autogenerated), and the dynamic nature of the language means its up to the documentation author to be explicit about the expectations (this is less a problem for well-formed functions, but for things like Pandas where every function takes a dozen combinations of arguments, it's a nightmare).
Another thing: lists of function signatures are cumbersome to read. For example:
pub fn last_mut(&mut self) -> Option<&mut T>
pub fn get<I>(&self, index: I) -> Option<&<I as SliceIndex<[T]>>::Output>
pub fn get_mut<I>(
&mut self,
index: I
) -> Option<&mut <I as SliceIndex<[T]>>::Output>
Quite often when I program I try to read the list of operations a type implements and find one that does what I need. The list above is 3 consecutive methods from Vec documentation, and it's actually from collapsed (i.e. abridged) list. I actually cut some of that collapsed info out ("where" sections) because it's not relevant at the moment. What I'm looking for at first is "last_mut", "get", "get_mut". Now the syntax is highlighted so function names are brown, but I still have to fish out function names from that soup. The "collapse" button actually hides text description, not type information.
I find looking at the left side (table of contents) much more effective. But I never felt it was necessary when reading Python documentation.
I don't know, maybe I'm just not used to static languages.
I'm curious which docs you are talking about. The Python standard library has much less examples than Rust's in my experience. numpy has excellent documentation, but that's some library.
Also, I don't see the redundancy you are talking about. There is no overloading, except for traits, and for those the documentation is in one place and all the implementations are just listed, which seems pretty minimal to me.
Coming from well established js libs to python I have found the documentation really hard to deal with.
I'm not entirely sure why that is. I really like getting to a repo on github and having the docs in the readme. Both python and rust have their own language specific doc implementations ReadTheDocs and docs.rs (I think). And you usually have to go to a separate site to view them which is fine, but I really dislike ReadTheDocs. It anyways seems really hard to find what I actually want. Take flask and alembic
The Python criticism I heard is that it makes it harder for library writers because they have to dive into source code. But here's the thing, when using a dynamic language with duck typing you don't really care what type specifically something is, only that you can iterate over it. I experienced the difference when porting someone's Python application to Rust. In Python, I didn't care what type the argument is, only what operations are performed. When writing the Rust equivalent, the Rust compiler was quick to complain it wanted to know the exact type.
So I think Python docs are good for dynamic languages, while Rust documentation pleases people used to static languages.
I did find the documentation for promises and tokio to be pretty confusing. But I had only been learning Rust for a few days at that point, so I think I was probably just not ready for it.
I went on to just use the basic sockets instead and that was pretty easy.
I have been working with Rust, full time, for a little less than 100 days. Like the author, I also came from Python (I authored Yosai).
Unlike the author, I haven't been hitting brick walls. I also have observed all of the warning signs that the futures bridge on the tokio highway is unfinished so I didn't cross the barrier and still try to use it anyway.
Instead, I have been working on myriad other synchronous parts with great success. I've made substantial progress in this time largely due to the Rust community and --- the Eco system! Sure, I've had to build custom parts but there haven't been showstoppers.
I'm from the Ruby community, did a year of Scala after leaving Ruby. I'm now with rust and after the first struggle, I don't really have trouble with Rust anymore. I know the design I need to do to get certain things done, I know what is allowed now easily with purely concurrent Rust, and when do you need to split things up into threads. Right now I can refactor a project from threads to reactor in a couple of days and deploy to production with Rust. And I know everything just works.
Async consumers that deal with events and trigger stuff on internal and outside services. RabbitMQ in, http out, response back to RabbitMQ kind of things.
I'm interested to know what attracts Python programmers to Rust. It's a very different language so I'm surprised to see so many python programmers take it up. What are your reasons for working with it?
I didn't quit Python. That would be shooting myself in the foot. I just won't be using Python everywhere for everything. Programming in Python is such a pleasant experience because I can do so much with it with so little effort. Rust enables the same but in other ways. Further, I can bridge the two!
I'm obviously about as far from the modal Rust user as one can get, but at this point the language has completely melted away into the background. I'm often tempted to write smallish scripts in dynamic languages, but even for those I frequently choose Rust just for the Cargo ecosystem. In particular I never see a reason to use C++ unless I'm contributing to a codebase that's written in it.
It takes different programmers different amounts of time to get to this point, and I certainly believe we can do better to make it easy to get up to speed, especially when async I/O is involved. But it does come eventually.
Same here. I used to use Python for small one-off scripts, but I now primarily use Rust. I can write Rust nearly as fast as Python and Go now, plus there are a lot of useful crates for the type of command-line utilities that I write.
For me the benefit is that a Rust utility usually works when it compiles, plus I can easily deploy the binaries on other machines (without going through another pyenv/pip dance).
To give an example: I work a lot with dependency treebanks on CoNLL-X format. Over my past to years with Rust, I have accumulated a bunch of utilities for doing things like paritioning data, merging treebanks, shuffling treebanks, extracting forms/lemmas, checking for cycling graphs, etc. I use them nearly daily:
Same here. But then I have 5 years of experience writing Rust. Wow, that feels like a totally ridiculous thing to write, but it's true.
These days I work at a startup where we are writing everything in Rust and then write C binding and Python binding to Rust code in order to connect to the outside world.
We've been putting a lot of work into making Rust easier to learn. A lot of that stuff is just starting to land now. Some of it is inherent, but some of it is also just rough edges. Including things that the author talks about here.
So, my message is, we hear you! We're working on it. Might want to check back in in a few months.
Watching from a distance I have to concede that from a safety, dynamism of the community and package management using cargo Rust has improved upon C++ in meaningful and measurable way.
However on the complexity of the language front, I wonder if Rust will also join C++ in the realms of "too complex" 10 years from now. Would love to get some Rust experts to weigh in here on their thoughts about managing complexity of the language long term and whether being a simple systems programming language is an end goal.
Rust is certainly not the "end of history" for programming languages. But, taking on that task is for future generations of language devs; it's an open research question today. I can only hope so!
One question I had after reading your post is - We currently build languages with purely additive models, ie features can be added to a language over time. Overtime the interaction between features creates "incidental complexity". Was wondering if you know of any languages that have been able to drop old features / ideas in a controlled manner.
Lua has been known to remove things. The language still remains small and clean after over 20 years. The creator has given some talks about how breaking compatibility (e.g removing features) requires its own thought process. There are different types of changes. The easiest to change are the ones that can be detected immediately in existing real world programs whereas the hard changes are the kinds that could go silently undetected by users until it is too late.
It's a goal of Rust to be simple--one look at the RFC repo is enough to see how important that is in the language design--but there are other goals too. In particular, nobody has successfully proposed any alternative to memory safety without GC that doesn't involve lifetimes and borrowing (and reference counting is just a form of GC).
My hope is that, over time, we figure out simpler ways to achieve the safety goals that Rust is pioneering right now, and so there will be simpler/easier alternatives that do the same.
If in ten or twenty years we can't do better than now, we've failed to make progress.
So yes, in the future we will hopefully consider Rust "too complex".
(Disclaimer: I don't have much experience with Rust, speaking as an outside observer).
(0) Automatically and statically, in which case there is an unavoidable tradeoff between the simplicity and the power of the static analyses. Example: non-lexical lifetimes.
(1) Automatically and dynamically, in which case you need performance-draining checks. Example: slice indexing.
(2) Manually, i.e., leave it as an exercise for the programmer, in which case you need a language with a formal semantics, because how else are you supposed to prove anything? Example: none that I know of in Rust or any other similar language.
There are no other options, although you can take a pick and choose approach for different use cases. IMO (2) has not been given the attention it deserves.
---
So, do you have any concrete idea regarding what could be made simpler?
I'm looking forward to the full article, because it's nice to know that I'm not alone here. I too started learning Rust with great optimism, and had the same experience: I expected that things would be difficult starting out, but assumed it would get easier as I acquired more knowledge, and then it didn't. Mostly this was for the same reasons as the author cites: Rust futures are incomprehensible, and there is rarely documentation to answer simple questions (one that I ran into the other day was "How do I parse a string into a datetime in a particular timezone?", which took about an hour to answer, compared to the 60 seconds it would take in something like C#)
I'm saying this as a lover, not a hater. I really believe in the mission of Rust, and I really want to like it, but I realized that even after a few months of learning, it was still taking me orders of magnitude longer to accomplish things in Rust than in C# (or even C++), and I wasn't finding the tradeoff worth it.
I think it really depends on what you're working on. Rust is designed to make some specific hard problems easier, like performant memory management and threading concerns. As a result of its focus it makes a lot of easy problems harder. Some of those will get better as the ecosystem matures, but it will likely never be as easy to use as other high-level languages which prioritize developer ergonomics.
For a select few domains, Rust is a god-sent. But if the performance-critical aspects of what you're working on are already abstracted several layers away from you, you're probably better of using a language which hides all the book-keeping which Rust puts at the top level.
I’m observing some similar muttering in the JavaScript community with Promises. Chain them incorrectly and you get an error with no actionable context.
The most popular 3rd party promise framework in JavaScript collects extra cause and effect data when you run the code in development mode. It helps but it still isn’t always enough.
I fundamentally don’t understand how people still think writing tools and libraries is the same process as writing production code. Which is to say: I wrote it, I understand it, ship it!
It’s a profoundly different process. I don’t think we give enough kudos to the people who can do it well.
I keep telling my ambitious jr devs the same two pieces of advice. The one relevant here is: nobody is going to look at your code until something is broken. Which means they are already having a bad day. Don’t make it worse.
Oh. For a similar reason (nobody reads your code unless they're looking at a problem): describe your piece of code in plain english. If you can make the code work the way you just described it, the code will age better.
(It might not be the fastest, but everyone will know if it's correct. When you're hunting for a bug it's important how quickly you can eliminate false positives to get to the real culprit. And you can make good code fast but you can't make fast code good.)
But trying to find stuff like this with Rust's documentation-- when you're using a search engine instead of relying on having a human to interpret the question for you-- is usually tremendously frustrating. My experience has been that Rust developers lean heavily on autogenerated documentation, where each method of a struct/enum is heavily documented but there are few to no examples or "E2E scenario" explanations to get you started from scratch.
That's why the above scenario took me an hour: first I had to discover chrono, then a while to realize it wouldn't do what I needed and actually had to use chrono_tz, then a while longer to find where the documentation for time format strings live, etc. All the methods were documented just fine, but there was no high-level explanation leading to them.
When I just did the search for C#, the first non-StackOverflow link was to Microsoft's autogenerated documentation. Without any examples, I'm not sure whether System.DateTime.Parse does what you want. Where would you have looked to get those examples/E2E scenarios for C#?
Did you look at the READMEs of those libraries? Most of the common use cases are documented fairly comprehensively there - I pulled those examples almost directly out of those documents
I've been learning Rust for a little while, and while I agree that it can be frustrating, it's nowhere near as bad as trying to learn Haskell. I regularly get a friend of mine to explain things to me, because the documentation and community for Haskell are 90% incomprehensible to anyone who doesn't have a comp-sci degree or a higher education in mathematics. Example: https://wiki.haskell.org/Lifting. It's not that I don't get the concept (after being given a much better explanation), it's just that the Haskell community in particular really seems to struggle with communication. And I think that's where Rust is actually in a good place for learning.
Rust has some confusing concepts, and it does feel like you're fighting the compiler sometimes, but I can see what it's trying to achieve and it's improving very rapidly. I find that I can more-or-less stumble my way through it until I get where I want, as the documentation that is about is generally well-written, even if it doesn't cover everything yet :)
Yeah that bit was alright, it was everything after that I struggled with ;)
As an example; I don't know what a functor is. I've not come across that term in anything I've done before, so I click on it to read up and get the definition: "The Functor typeclass represents the mathematical functor: a mapping between categories in the context of category theory. In practice a functor represents a type that can be mapped over.", which uh, doesn't really help. I only did mathematics to GCSE level here in the UK, during which I never came across most of the terms used frequently in Haskell's documentation :D
There's a lack of examples of "real-world" usage in Haskell. e.g. here's how you'd use lifting, in a practical example. e.g. why is lifting a thing and what possible uses does it have when writing an application? Because to my knowledge I've never done it before, so why is it good?
Thankfully my friend sent me this: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_..., which is a much better explanation. It's probably due to a difference in my background (predominantly web, applications, self-taught etc) and the more academic history of Haskell, but I find the vast majority of examples irrelevant. Luckily there are a -few- books/tutorials around that explain the language more clearly and why I might want to write software with it.
It's a shame because it looks like a really cool language, but it is seriously hard to get into. Whereas with Rust, it just kinda makes sense to me.
I did mathematics to first year university level and didn't come across any of the terms used in Haskells.
Our programmers group is currently working through category theory with - https://bartoszmilewski.com/2014/10/28/category-theory-for-p... - and a lot of it is utterly bewildering, but at least I am starting to understand some of what Haskell is going on about. As for real world usage, however, I still have a long way to go...
I think what happened here, is the author wanted to do something with futures and the whole frustration is comming from not understanding that Rust Futures are just not ready yet. The whole language and ecosystem has plenty of stuff in the pipeline to make them better.
Without commenting on the original article, this is my main complaint. Plus, I'm not a fan of the sigil heavy syntax, not that modern C++ is much better.
I think it sounds exactly right when a developer comes from a higher level language(like C# or Java with the rich base libraries) to a much low level one.
I'm coming from working in Typescript in Nodejs for the last 3 years and I've found it incredibly approachable. It's possible that it's because OOP is less prevalent in JS compared to the ones you mention as well as the author's use of Python. Then again, maybe it's just syntax similarities.
I do not have experience with Rust, I had similar experience like author when coming to C/c++ , you need to also understand what a linker is, how compilation works, other low level things to be able to understand c/c++ errors. In C#/Java things are simpler so is normal to be hit with ton of new things that you need to understand
The horribly anti-user futures system, compiler messages generated by a misused macros that are second only to C++ template errors in how egregiously difficult they are to parse, universally terrible documentation and lacking examples, unstable APIs, type annotation hell, and so much more.
These are all very subjective. I personally love the error reporting; The examples and docs are excellent; Many times I've needed unstable nightly api features and have appreciated how easy it is to enable.
I feel like a lot of difficulties people run into with Rust are because they're trying to do things the "right" way. You can often make your life easier with liberal use of reference counted objects, instead of solving difficult lifetime constraints, but it doesn't feel like the perfect way to do it. Often people are pursuing zero-cost abstractions to their detriment. They then compare the language unfavourably against languages where those zero-cost abstractions weren't even an option.
I think this is an important point. When prototyping or developing one-off utilities, I'm not looking to shave off CPU cycles or run everything under 6 MB. I wish someone would write a "Rust for <insert-gc-lang> Programmers". :)
I feel my productivity (and code quality) would increase a lot. For example right now I have to struggle so hard if I would like to write generic code, while in haskell that's more or less the default thing (the compiler infers it) if I just omit type annotations...
PG had this comment about programming languages: The correct solution of a problem is the most simple solution. Just like in math, you can solve one problem in multiple ways, but the correct one is the shortest, beautiful looking one.
When I look at Rust, it gives the feeling "could be simplified". Obviously it's a new language and it comes with lots of cool stuff, but it looks ugly, doesn't excite those who seek simplicity.
If I had the time to invest in learning Rust, I'd use that time for building something new with any language that makes me productive.
There are some ways in which Rust can be simpler; see my above comments about learn ability. But a lot of the complexity is inherent in the choices we've made for the constraints of the language. For example, including a GC would make things much simpler in some ways! But it would also make it not appropriate for many of the use cases that are crucial to Rust's existence.
Exactly my feelings. Interesting how some people claim a rather different experience. Rust was voted the most loved technology on StackOverflow, which is unbelievable to me.
> Rust was voted the most loved technology on StackOverflow, which is unbelievable to me.
I absolutely adore Rust because my code has an order of magnitude fewer bugs when it is time to actually run the end result.
A lot of programmers have an ugly tendency to blame the language/library/framework/weather for their problems before questioning their skills or knowledge. They are absolutely not suitable to be Rust developers.
I don't think so. Rust is a brilliant language in theory, and for many high-performance applications. There's a lot to love from afar. But it still hasn't made good on its promises to be a great language for everyday application development. That said, the goal is lofty and laudable and the community's positive attitude and ability to take constructive criticism are world class.
As a long-time C guy, it's nice to see Rust as a modern replacement. But most of the time, such low level programming is not needed.
The popularity of Rust and its friendly hand-holdy docs almost don't even make sense for what it is, to me anyway. I suspect it will end up frustrating many high-level developers.
The problem with Rust is that it is very hard to program interconnected graphs.
Basic Rust does not allow cyclic references, which means that almost every graph needs to use special tricks to make it work. These tricks are variants of reference counting. A trick that was copied directly from C++.
It feels like having to build a car with only a screwdriver and hammer. Each section of your graph needs to be managed separately. With non-safe rust or C++ you can put whole panels or sections on your car. And a garbage collector works like a robot: usually effortless.
The easiest way to build graphs in Rust is to store all your graph nodes in a Vec, and use indices instead of references to refer to other nodes. Also a cache locality win.
This is really the only practical way to do it, yes. But it's gross and means that you have to write custom code to print the data structures. That might sound trivial, but the ability to autoderive pretty printers is super useful when debugging.
That's fine if you're actually running a bunch of graph algorithms, but it's not a good fit for, e.g., a parse tree where each node has a link to its parent as well as links to its children. At least, I for one don't want to have to pull in a relatively large library just to define such a simple data structure.
And if your nodes can be of different types, you probably need to make them "enums" instead of "objects".
So essentially you need these tricks to work with graphs, which means that your program design is really not as easy as with a garbage collected system, or manual memory management.
I think that many new users have problems with complex graphs in Rust, especially cycled graphs.
To be clear, cyclic references are just as much an issue in GCed languages. In Python I had to use weakref lib to ensure cyclically referenced objects are still GCed. Rust is the same in that aspect, you use the Rc builtin type and create a weakref for one of them.
No, GCs can handle cyclic references. You don't need weak references merely in order to ensure that a cyclic structure is eventually freed once it's become inaccessible. That's not to say that there might not sometimes be good reasons for using weak references in a GCed language, however.
This sort of feedback would much more helpful and credible if you added what sort of project you're using Rust on, your technical background, and concrete facts (not just metaphorical opinions / complaints) about what goes wrong. As it is, this article helps no one make informed technology decisions. See https://vimeo.com/9270320.
> I fixed a lot of code after updating to the latest Rust compiler each day.
When referencing your experience, it might be good to point out that this is before Rust hit 1.0 (commits mention Jan 2015 and 1.0 was released May 2015). This problem goes away and a lot has improved about the language and ecosystem since then.
> But nothing in life is free. GTK is sufficiently complicated that a Rust "safety wrapper" is in order. The one available was not complete.
I think GUIs might still be a weak point for Rust but it sounds like this has gotten better. I hear a lot of positive reactions around relm
> And then I tried to abstract the font rendering code into a widget concept, and everything broke down. The Rust compiler has many false negatives - situations where it is a compile error due to safety, but actually it's pretty obvious that there are no safety problems.
Any examples of false negatives?
The main source of them is borrows that don't need to live to the end of the scope ("non-lexical (borrow) lifetimes"). While in most cases this is easy to work around (add a scope), things are progressing for the Rust compiler to understand these.
I consider myself a good enough engineer, and I've picked up a lot of languages. Recently I built and launched a server agent for Cronitor. Going in I knew it needed to be portable and compile to an executable. I considered Go, Rust and C.
I decided to spend a day and build 3 versions of a basic Usage block to get a feel for each. I'd never written Go or Rust, and only passable C.
I did C first and then tried Rust. I gave up. It was too much of a learning curve for a ~3 mos project. I ended up using Go. I'll write up a blog post soon, Go is not without challenges, but I'm comforted by blog posts like this to see that I'm not the only mortal here still challenged by Rust.
While people can learn Go in a day, it is basically impossible to learn Rust in a day.
The only reason people can learn Go in a day is that Go is similar enough to a language the person already knows. Rust is different enough to every other language in existence (including C and C++) to make learning in a day impossible.
I don't think "deciding to spend a day" is the right way to evaluate programming languages. Unfortunately, it is the common way to evaluate programming languages and Rust fares very badly in such evaluation, in my opinion, much worse than its fair score.
Author here. I want to apologize a bit for the tone of this article — it was written from a place of frustration, and this part of the site is very much akin to a development journal — these are short articles without a lot of concrete facts that are not really intended for broad or comprehensive consumption. In no way is this meant to be an anywhere-near comprehensive critique on Rust.
I'll quickly point out that Rust is my favorite programming language, even if reading this piece in isolation would give you the opposite impression. There are about a million things to like about it: The syntax, type safety, sober choices around language design, built-in documentation facilities, conventions, `rustfmt`, linting with `clippy`, toolchain management with `rustup`, community (in the sense of project management and organization), development momentum, ecosystem, and a whole host of other things are all downright incredible. Some of the people working around the core are undoubtedly some of the smartest throughout the entirety of professional software.
That said, I wouldn't take back anything I wrote here because even when I read it back today with a much more optimistic place (I bypassed many "brick walls" that I alluded to since I wrote it), I still think it's true. Months later, even after writing a lot of Rust, I often still don't feel productive.
No language is perfect, and it's probably healthy to have the occasional counterpoint to its fairly consistent positive press. I've spoken to people who don't know much about the language and refer despairingly to the "Rust Evangelism Strikeforce", which I think is largely internalized rationalization that what they've read is too good to be true, so there must some nefarious element at work. Rust really does deserve its good press, and hopefully the occasional dissenting opinion and subsequent discussion will help convince them of such.
IMO, Rust has a few existential threats — the one that I think most about is that despite being well passed 1.0 at this point, there's a distinct lack of pragmatism around getting core features like concurrency nailed down and shipped. Futures and Tokio are widespread at this point, but the APIs are still changing, and the error messages that they produce are still quite awful. Even once they're fully feature-complete and stable, they're still not going to be very pleasant to use — you have to think really hard about how you're doing future composition. Contrast this to a concurrency system like Go's, where you can more or less pretend that you're writing synchronous code, and let the runtime take care of the heavy lifting. I love how much careful thought and consideration is being put into the development of a cohesive concurrency model, but unless it can get to the point where it's stable and far more user-friendly, people (like me) who are trying to use the language to build things are going to continue having a hard time.
In summary, Rust is awesome, the core team is awesome, and the development community is awesome. I'm confident that the usability problems it has today will be worked out.
There's a lot of stuff coming down the pipeline that I think would help you a lot.
> there's a distinct lack of pragmatism around getting core features like concurrency nailed down and shipped.
For one example; we're working really hard right now on getting async/await shipped, and dealing with ergonomics problems around it. The recent changes are to make it simpler, not make it harder. For example, you should need to think a lot less about future composition, since you'll be able to borrow across futures, as just one example.
There might be a disconnect between what we're working on and what we're perceived to be working on. Messaging is hard!
You're 100% right that criticism is the only way to evolve, and so I appreciate calling out things that aren't great. Details help, but as you said, this blog post wasn't intended for this audience, which is a problem I feel quite deeply, personally. So I get it :)
Thanks for not taking it personally :) that's not always easy!
> For one example; we're working really hard right now on getting async/await shipped, and dealing with ergonomics problems around it. The recent changes are to make it simpler, not make it harder. For example, you should need to think a lot less about future composition, since you'll be able to borrow across futures, as just one example.
>
> There might be a disconnect between what we're working on and what we're perceived to be working on. Messaging is hard!
Sufficed to say that I can't wait to see these improvements come out. It may end up being the case that I simply got started on this project a little too early — six months from now many of my problems may have evaporated already.
Yeah, much work is being done! As a long time user of Rust (for many years), I'm not super confident about using futures yet without the support of `impl Trait` - I mean, I could manage it, but I definitely would not want to inflict it on more sceptical coworkers. Thankfully this stuff is being worked on, and should be shipped in the not to distant future. The Rust team is great, and manages to get a huge amount done despite the many challenges in front of them! All while remaining positive and friendly! :)
I'm not sure how easy Rust claims to be... The main idea here is " I love that Haskell-esque feeling where a compiling program is usually a working program." Rust will probably get easier, but it's main goal is safety.
The main problem I have with a pure safety focus is that the warnings often feel like overkill for 99% of programs. For example, when gcc yells about comparing signed and unsigned ints, I find that there rarely is a true safety concern involved. I hope Rust is better than that.
> For example, when gcc yells about comparing signed and unsigned ints, I find that there rarely is a true safety concern involved
except for the small detail that stuff like "-1 < 1" is false when comparing int and uint because the int gets casted as the latter ? -Wsign-compare should be used as -Werror=sign-compare
> For example, when gcc yells about comparing signed and unsigned ints, I find that there rarely is a true safety concern involved. I hope Rust is better than that.
In Rust these are treated as two completely different types so this is a compiler error and not a warning. You need to explicitly cast one of them to make the comparison and integer types are not automatically converted in any way. You have to do the same for any other operation involving the two types (e.g. addition).
You could certainly define the `ParitalOrd` trait for the two types to make the comparison possible (I don't know the implications off the top of my head). The language defaults to safe and explicit.
> You could certainly define the `ParitalOrd` trait for the two types to make the comparison possible
I don't believe that's possible. Unless something changed relatively recently, the trait implementation has to be either a) in the module where the trait itself is defined (so the standard library in this case), or b) in the module where the type you're implementing the trait for is defined (again, the standard library). Since you can just modify the standard library, implementing PartialOrd would not be possible for e.g. comparing u16/s16 (or whatever).
I've been dabbling in Rust on and off since 2014, and this has always been my feeling. I thought my background in C++ would make things reasonably easy, and while I have little fondness for C++, it's still easier to get things done than with Rust, which is not at all what I expected given my 4 years with the language.
There's a lot to like about Rust, but it falls far short of the "easy-as-Go" promises made by many of its proponents.
Really? I was hearing that 2 or 3 times a week before Tokio was popularized. "Rust is as easy as Go for writing server apps!" "You don't need goroutines or async for high performance servers; Linux threads work fine!". When people started hearing about Tokio, many people seemed sure it would be easier. None of this is meant to bash these people; I think their goals and vision is laudable and I hope they get there; I just think their optimism blinded them a bit.
My impression is that people use Rust for correctness, security, and speed over ease of use. Although I do think once you get over the initial learning curve (which is admittedly large) it mostly makes sense.
The more I read about those new-wave languages, the more I end up thinking that the simple languages like python and C are the clear winners.
I started to learn about rust, but honestly it feels like it has the same design and features of ADA, in term of code correctness. It's a great thing, but I still wonder if it's really useful for everyone. Usually ADA was used in aerospace.
And I still believe C++ has a good future, of course as long as you keep using the good parts.
If software companies got recalls and were sued for lack of quality like in other industries, by now everyone would care about how their code gets written.
You’re assuming that professions that have been around for hundreds of years like lawyers or physicians or engineers don’t have incompetent practitioners :)
There are a few things in life you just should not do. One of them is jump from your only language experience being Python into an advanced language like Rust when you’re struggling to grok SQL.
I don't care for this assumption that people who struggle with a programming language are so inexperienced anything they struggle with is their fault. It might be the case that Rust will always be an advanced language that beginners should avoid until they've mastered something else, but feedback from beginners could still make the experience easier for programmers who take the ideal path and learn it as their second or later programming language.
The other reason I dislike this assumption is that it's frequently wrong. Judging by the rest of his blog, the author does grok SQL. I'm pretty sure the Python to SQL example is either hypothetical or describes the author when he first learned SQL well in the past.
I don't mean to pick on you. I just see this attitude repeatedly in software development, and it leads to unnecessarily difficult software.
Your comment stated that going from Python to Rust while struggling with SQL is something "you just should not do". That implies that the struggles they describe are the result of trying to learn Rust before they're ready—an attitude I disagree with, even if it were true that the author was struggling with SQL.
I stand by "anything"—your post didn't make any exceptions. I should have been clearer that I was referring to an attitude your comment implied rather than an attitude you have, though, and for that I apologize.
Then perhaps he should become a fictional writer then. So what parts of this “story” are even true? But more importantly, what is the point of writing a fictitious story like this? If he’s not struggling with SQL and Rust, then what is it? Clickbait? I think you mean an illustrative example of Rust-bashing.
The introduction is a generic example of learning to program and the kind of difficulty curve you encounter there. Nowhere does it suggest that it's the current state of the authors knowledge.
He's (the OP) also correct. SQL is really, really weird when you come from an imperative/functional perspective.
I've forgotten most of this as I've gotten used to SQL, but you can use variables before you define them (except when you can't (damn you group by not handling my alias)). Like all of this makes sense to me now, but it was pretty painful when I started. I distinctly remember WTF-ing my way through my first weeks with SQL.
I knew people are different. But if somebody can say that Rust compiler generates complicated error messages, then I just can't imagine how different people can be.
You can spend a month or two and get used to the bulk of Rust out of raw exposure. After seeing the same compiler errors over and over, you start to pattern match and learn how to jiggle the handle to get what you want.
For example, have you ever accidentally passed &Request across a thread boundary? The compiler explodes with UnsafeCell errors deep within the inner sanctums. It feels impenetrable for a while. You throw some clone() at it and whatever your go-to guesswork is. Then 30 minutes pass and you realize you just needed to pass Request instead of &Request. And at the end of i you really haven't learned much beyond "I'll try that sooner next time."
I think that's the hardest part about Rust. It takes quite a bit of mastery to actually understand and work around the more difficult compiler errors, like one that seems to threaten the whole abstraction you were going for. But adding a `+ Send + Sync` somewhere suddenly fixes it and it's not obvious why. It can feel very precarious.
Anyways, I think most of Rust's issues are just tooling issues. For example, imagine being able to hover identifiers to see their scope shaded in and where they'll get dropped -- instead of just error-message ASCII art. Or being able to hover something to see that it can do something because it implements this certain trait.