Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

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; }



See 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.


I 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


clone() 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.


> 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.


Asking 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"?


Yes, it would panic. Typically you handle the Result rather than unwrap directly.


I'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.


You'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.


To 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.


My 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.


> 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.*


Eh, 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.


When 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.


I’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.


My 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.


is 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"?




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

Search: