100 days with Rust: a series of brick walls
brandur.org
brandur.org
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.
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.
Imagine if car makers worked the same way.
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.
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.
Doesn't almost every major programming language have Futures though? (C++, Java, Python, Ruby, JavaScript, Go).
It seems a fair criticism for so common a building block.
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.
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.
|
212 | / fn call(&self, payload: Self::Request) -> Self::Future {
213 | | let request = self.create_request(payload);
214 | |
215 | | let work = async_block! {
... |
254 | | FutureResponse(Box::new(work))
255 | | }
| |_____^
note: ...so that the type `impl futures::__rt::MyFuture<<[generator@src/client.rs:215:20: 252:10 self:&client::Client,
request:hyper::Request for<'r> {futures::Async<futures::__rt::Mu>, (), fn(std::result::Result<hyper::Response, error::Error>) -> std::result::Result<<std::result::Result<hyper::Response, error::Error> as std::ops::Try>::Ok, <std::result::Result<hyper::Response, error::Error> as std::ops::Try>::Error> {<std::result::Result<hyper::Response, error::Error> as std::ops::Try>::into_result}, futures::MapErr<hyper::client::FutureResponse, [closure@src/client.rs:217:59: 217:85]>,
hyper::Response, std::option::Option<std::string::String>, &'r hyper::Response, hyper::StatusCode, fn(std::result::Result<hyper::Chunk, error::Error>) -> std::result::Result<<std::result::Result<hyper::Chunk, error::Error> as std::ops::Try>::Ok, <std::result::Result<hyper::Chunk, error::Error> as std::ops::Try>::Error> {<std::result::Result<hyper::Chunk,
error::Error> as std::ops::Try>::into_result}, futures::stream::Concat2<futures::stream::MapErr<hyper::Body, [closure@src/client.rs:234:49: 234:75]>>}] as std::ops::Generator>::Return>` will meet its required lifetime boundsAnyways, I think it has still long way to go to match the length of even common C++ template related error messages...
And latest C++14 and C++17 changes also help library writers to error check the type parameters.
Of course those that have to use other compilers still need to face the sea of incomprehensible error messages.
And in any case, better stay away from template meta-programming libraries, at least until modules and concepts eventually land.
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.
https://jvns.ca/blog/2018/01/13/rust-in-2018--way-easier-to-...
This is so awesome.
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.
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.
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.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?
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.
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>.
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".
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.
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.
use error::{Error, ErrorKind, Result, ResultExt};
fn some_func(v: &str) -> Result<u32> {
v.parse::<u32>().chain_err(|| ErrorKind::ParseIntError)
}
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))
}
But then you'd have to box every error.... I think. I don't remember it that vividly.
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.
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.
And I also find that SQL can be quite infuriating to deal with, and had a high-friction experience with Rust.
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.
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.
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.
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.
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?
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.
We are constantly tweaking the layout of stuff, and have some larger plans on the way as well.
Node is very straightforward:
https://nodejs.org/api/index.html
Python has a library reference and a language reference:
https://docs.python.org/3/library/index.html
https://docs.python.org/3/reference/index.html
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):
https://doc.rust-lang.org/std/index.html
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:
https://doc.rust-lang.org/nightly/
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.
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
https://doc.rust-lang.org/std/boxed/struct.Box.html is a good example.
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
> 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!
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.
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.
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.
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.
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 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.
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.
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.
But anyway!: https://doc.rust-lang.org/error-index.html#E0038 <-- these are restrictions on Box<Trait>. The one that screws me is the first one, requiring Sized.
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?
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.
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<>.
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.
impl Material for Lambertain {
...
fn box_clone(&self) -> Box<Material> {
Box::new(Lambertian {
..self
})
}
}
Holllllly shit I think that works.Why am I allowed to do this and not just derive Clone and Copy!?!
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.
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.
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)
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:
struct Foo {
v: Vec<i32>
}
let foo = Foo { v: vec![1,2,3] };
Can you elaborate on what trouble you're having?> 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.
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 ?
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.
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?
Can you write an example?
// 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: fn speak_static<T: Bark>(dog: T) {
dog.bark();
}
fn speak_dynamic(dog: Bark) { // wait for it...
dog.bark();
}
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).Furthermore, calling this function is trivial:
speak_static(rover); // woof
speak_static(marmaduke); // WOOF
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`:
fn speak_dynamic_box(dog: Box<Bark>) {
dog.bark();
}
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>.)
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.
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
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'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.
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:
https://play.golang.org/p/3qfd5B8_ktK
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`.
[1] https://forum.golangbridge.org/t/free-memory-of-slice/3713/2
---
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.
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.
https://play.rust-lang.org/?gist=8d88b83daa2f389892bfa95c8db...
extern crate rand;
use rand::Rng;
#[derive(Debug)]
struct MyStruct {
v: Vec<u32>
}
impl MyStruct {
pub fn new() -> MyStruct {
let mut rng = rand::thread_rng();
let n = rng.gen_range( 4, 10 );
MyStruct {
v: rng.gen_iter().take( n ).collect()
}
}
}
fn main() {
let ms = MyStruct::new();
println!( "{:?}", ms );
}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.
1) GC is lazy, whereas RC is eager;
2) the use of "pure" RC may lead to memory leaks.
Here is a random example that actually recommends the opposite (RC in old generations, mark and sweep in the young generation):
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.
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.
Recursive structures are a problem although in my experience you can usually just alter the ownership rules a bit instead.
FWIW there's a Rust GC library that provides opt-in GC for when you need it. https://github.com/Manishearth/rust-gc
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.
in particular, boost.polycollection is a nice implementation of heterogeneous vectors: http://www.boost.org/doc/libs/develop/doc/html/poly_collecti...
Baloney. In C++ vector<any> or vector<variant> accomplish this task without any problems whatsoever.
structs being classes with public access by default, with default assignment operator behaving the same way as C code would expect.
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
https://en.wikipedia.org/wiki/Object_slicing
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").
Object slicing is a problem with C++, period.
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.
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.
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?
Just because you're a perfect developer doesn't mean some downstream dependency isn't going to come along and wreck your day.
If it was that simple we'd never need kungFuDeathGrip in Gecko. Yet, we need it a lot: https://searchfox.org/mozilla-central/search?q=kungfu&path=
(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.)
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.
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.
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).
> 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, 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.)
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.
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.
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.
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.
> universally terrible documentation and lacking examples, unstable APIs, type annotation hell, and so much more.
The docs in Rust are by far some of the best I've seen, without specifics it's really hard to understand what issues he hit.
So you'd suggest adding a manner of grouping methods in rustdoc?
Collections, strings, dates, asynchonous operations.
https://doc.rust-lang.org/std/vec/struct.Vec.html
FWIW I'm not a huge fan of Python's docs because I can't quickly scan them to track down that one detail about a function I was using.
Rust's docs also let you collapse everything which makes browsing them much faster.
I find myself always just looking at the source to make sense of things rather than the docs which kind of defeats the point.
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.
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.
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
So I think Python docs are good for dynamic languages, while Rust documentation pleases people used to static languages.
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).
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 went on to just use the basic sockets instead and that was pretty easy.
I've been learning Tokio just this week, and I think this has to do with their ongoing refactoring away from the tokio-core crate to the tokio crate.
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.
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.
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.
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:
https://github.com/danieldk/conllx-utils/tree/master/src/bin
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.
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 wrote some thoughts about "complexity" and language design here: http://words.steveklabnik.com/the-language-strangeness-budge...
I also think the difference between incidental and inherent complexity is important...
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.
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.
Rust is a better C, not a better C#.
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.
(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.)
"2014-11-28T21:00:09+09:00".parse::<DateTime<FixedOffset>>()
With chrono, I think that's all you need. If you want to name the timezone you can use chrono-tz: let tz: Tz = "Europe/London".parse().unwrap()
let date = "2014-11-28T21:00:09Z".parse::<DateTime<Utc>>().with_timezone(&UTC)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.
https://msdn.microsoft.com/en-us/library/system.datetime.par...
With the main page for a class listing the most common use cases.
Basically that performance hotspot where a managed runtime isn’t an option.
Rust is still new, it took C++ about 20 years to reach where it is, and even now there are domains where it cannot replace C for various reasons.
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 :)
And it's kinda fun!
What's there not to understand? :)
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.
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...
Why was this even posted here?
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.
"but I’m starting to feel seriously concerned by how glacially slow it is to get anything done."
https://adventofcode.com/2017/leaderboard https://github.com/sciyoshi/advent-of-rust-2017
I had the same experience. After getting lifetimes (more or less) this became my largest headache... I wish for stronger type inference and features like this: https://downloads.haskell.org/~ghc/7.10.1/docs/html/users_gu...
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...
Welcome to systems programming.
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.
Once I "got" how to structure my programs in a Rust-friendly way, it's got nice and easy to use.
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.
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.
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 tricky things are only if you want to have 2-way pointers without refcounting overhead.
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.
This is why one of the main features of zig listed on the home page is:
"Small, simple language. Focus on debugging your application rather than debugging your knowledge of your programming language."
> 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 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.
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.
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 :)
> 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.
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.
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.
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).
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
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.
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 that would be the case, you would have much less competent programmers who would be able to do their job properly.
I believe the software industry is still too young and immature.
Raising the bar makes sense in any engineering.
Don't shift the argumentation towards inequality.
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.
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.
Rust gives me a similar feeling.
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.