4 ms·
My coworker and I were pair programming some Rust today. We were working on a convenience wrapper whose whole purpose is to reduce boilerplate. So, by its natur
by sgtnoodle 3y ago
My coworker and I were pair programming some Rust today. We were working on a convenience wrapper whose whole purpose is to reduce boilerplate. So, by its nature there's a lot of internal generic traits, thread safety, etc.
He knows Rust well but software design not so much. I know software design well but Rust not so much. My experience can be summed up with:
let &mut writer = Writer<T>::writer::new(connection.clone()).clone();
// TODO ???
writer.write(*data.clone()).unwrap();
Meanwhile in C++ I'm like
if (!write(connection, data, data_len))
{
return false;
}
- urthor 3y agoSee as someone who knows Rust fairly well. let &mut -- Essential keywords. writer = -- Duh Writer<T> -- Generic types. Important. ::writer::new -- Boilerplate. (connection.clone()).clone; -- Relentless cloning is a big problem. ... .unwrap -- "Consistent names for optional types," is an issue in Rust. Every module has different jargon for Some(x). Rust certainly isn't perfect. The borrow checker creates... awkwardness, that requires .clone hacks to solve. Flip-side is, I'm coding in CMake right now. I've had to create multiple bash scripts for manually deleting my build files and general day to day. Software is a young profession. Everything is shit.
- tayo42 3y agoI feel like those clones don't get enough attention. At a glance, you don't really know what those clones are doing. Is it allocating, is just increasing reference counter? Makes code hard to understand
- urthor 3y agoclone() makes an immutable, deep copy/recursive copy of the original data structure. clone() itself makes perfect sense. Why the programmer needed those clones... is a legitimate issue with Rust. In my experience so far, it's usually to hack around an interaction between the borrow checker and the output of a function constructed using dot syntax.
- dist1ll 3y ago> clone() makes an immutable, deep copy/recursive copy of the original data structure. No. clone() calls std::clone::Clone::clone. For many std collections that means deep/recursive copy. For custom data structures, it can mean anything. It depends entirely on your implementation of the Clone trait.
- oriolid 3y agoAsking as someone who has tried learning Rust but didn't get very far, doesn't unwrap essentially mean "this might panic but I don't care to implement error handling here"?
- ww520 3y agoYes, it would panic. Typically you handle the Result rather than unwrap directly.
- spease 3y agoI’m not sure why the API wouldn’t implement Write for connection so you could do: connection.write_all(data) Where data could be an argument that impls ToOwned or Into so that it would either use the object you pass in (if by value) or implicitly clone it (if by reference). Basically this looks more like an API design problem than a limitation of the language.
- sgtnoodle 3y agoMy example wasn't meant to be taken so literally. I'd say there's some truth to there being an API design problem to untangle, though. It's a relatively new language in a repo with dozens of contributors all frantically trying to get their work done. I'm just the salty senior engineer that's getting sucked in before wheels fly off. Stepping back even further, I think there was perhaps an overeager desire to use multithreading, both unnecessarily and at the wrong level of abstraction. That lead to a lot of lifetime management complexity at the lowest, deepest level. Then, folk slapped on progressively higher level abstractions, applying band-aids as they went rather than refactoring the lower layers.
- ww520 3y agoI'm sorry but that doesn't look normal. Code smells with all those clone()'s and unwrap(). It allocates a new Writer and then clones it?! "let &mut writer=" isn't right. The new'ed Writer is a struct; why is there a need to get a reference to it? Why is data being clone() and then immediately de-referenced to create a copy of the clone? A normal writer API would look like one of the variations: Writer<T>::new(&connection).write(&data) Writer<T>::new(&mut connection).write(&data) // if conn needs changes. It's rare to unwrap(), which can cause a crash at runtime, but to handle the result. E.g. to write more data if the previous write is successful. let mut writer = Writer<T>::new(&connection); let mut written_len = 0; written_len += writer.write(&data1)? // ? returns the err if it's Err written_len += writer.write(&data2)? // or unwrap the result value written_len += writer.write(&data3)? As you can see, "writer.write(&data1)?" is equivalent to the C++ version.
- sgtnoodle 3y agoYou're interpreting my example more literally than I intended. :-) What does it say about the language, when parody is indistinguishable from poorly written code? The actual code we were working on involved functions returning closures with mutable captures, so the borrow checker was especially persnickety.
- ww520 3y agoTo be blunt, the code you copied out as example is crap. I don't know what you expect how it's being interpreted. It says more about the programmer than the language. > functions returning closures with mutable captures That sounds like another bad design, but to each his own.
- sgtnoodle 3y agoMy apologies if I hit a nerve. I'm just trying to have fun chatting about the foibles of contemporary software development with fellow nerds on HN. I expected something like urthor's post, where they seemed to appreciate the levity of my comment, and responded with some interesting insights. I also appreciated the follow up comment raising a serious concern about the prevalence of cloning and resulting confusion in realistic codebases.
- inejge 3y ago> let &mut writer = Writer<T>::writer::new(connection.clone()).clone(); // TODO ??? writer.write(data.clone()).unwrap(); It's not difficult to arrive at the caricature of baroque generic code if you combine lack of knowledge with miscommunication. The knowledgeable coworker should be aware that a writable object implements the Write trait, and know, or find out, the signature of the Write::write() method. Even fully generic, a function accepting a connection and returning the result of writing a block of data is not too gnarly: use std::io::{self, Write}; fn write_data<W: Write, D: AsRef<[u8]>>(connection: &mut W, data: D) -> io::Result<usize> { connection.write(data.as_ref()) } No unwraps, no clones. Because they're not necessary here. But someone has to know the language and the idioms. Even your "easy" C++ code depends on knowing that write() returns zero on success, and that integers can play the role of booleans in conditions.*
- inejge 3y agoEh, zero on failure, I suppose. The real write(2) syscall returns -1 on failure, and a non-negative value is the number of bytes actually written. But the general point still stands.
- sgtnoodle 3y agoWhen I wrote my hypothetical example, I was imagining the return type to be a bool. My apologies for overloading "write", a more representative example function name would have been something like "FooWrite". I appreciate your follow-up! I totally agree with you that folk writing code need to understand the semantics of what they're building on top of. Humans and now AI models have a great capacity for generating tons source code without understanding the semantics, though.
- chalcolithic 3y agois there a chance that someday unwrap will be visible to the typesystem and we can safely proclaim that "neither this library not its dependencies contain an unwrap"?