3 ms·
The author has reinvented the ?-operator from Rust and Ruby, but for Go. Here's the same function in Rust: fn decomp(filename: &Path) -> Result<Vec<u8>, i
by cbarrick 3y ago
The author has reinvented the ?-operator from Rust and Ruby, but for Go.
Here's the same function in Rust:
fn decomp(filename: &Path) -> Result<Vec<u8>, io::Error> {
let fd = File::open(filename)?; // File is automatically closed by its destructor.
let zd = GzDecoder::new(fd); // flate2::read::GzDecoder::new does not return an error.
let mut data = Vec::new(); // Rust makes the caller allocate the buffer for reads.
zd.read_to_end(&mut data)?;
Ok(data)
}
I think this is great. From a reader's perspective, it can dramatically improve readability in a lot of functions. From a writer's perspective, these kind of solutions make it easier to compose expressions without interleaving if-statements after every other line.
Yes, sometimes you want to add context to your errors instead of using this syntax. But you can always use verbose syntax when it's needed, and terse syntax when it's not.
- zakm 3y agoThe only argument I can think of against this would be that it would maybe slightly discourage adding the verbose details and would generally decrease the quality of error messages slightly. That said, even so it's probably worth the trade.
- slantedview 3y agoI agree, especially if you can _choose_ when you want to be a bit more verbose and add more detail, or not.
- shepmaster 3y agoRust libraries like my [SNAFU] allow you to both add additional details and use the `?` operator: read_to_string(path).context(ConfigFileSnafu { path })?; [SNAFU]: https://docs.rs/snafu/latest/snafu/ https://docs.rs/snafu/latest/snafu/
- bibabaloo 3y agoErrors are composable so this isn't such a problem in practice. Most of the prod code I wrote would do something like this use thiserror::Error; #[derive(Error, Debug)] enum Error { #[error("Could not open given decomp file: {0}")] FileOpen(#[from] std:io::Error), #[error("Compressed read error: {0}")] CompressedRead(#[from] gz::Error) } fn decomp(filename: &Path) -> Result<Vec<u8>, Error> { let fd = File::open(filename)?; // File is automatically closed by its destructor. let zd = GzDecoder::new(fd); // flate2::read::GzDecoder::new does not return an error. let mut data = Vec::new(); // Rust makes the caller allocate the buffer for reads. zd.read_to_end(&mut data)?; Ok(data) }
- Mavvie 3y agoHuh, can you explain that a bit more for a rust noob like myself? 1. How does it know how to create your Error enum? I guess it's from the #[from]? 2. What happens if your method tries to return something that's not an io::Error or a gz::Error? I guess the compiler catches that? 3. How would you handle doing this for multiple methods in the same file? Would you rename your enum to DecompError or something to avoid conflicts?
- masklinn 3y ago> How does it know how to create your Error enum? I guess it's from the #[from]? 2. #[from] is just a convenience library feature, in reality it’s because of the From conversion trait which ? invokes on the way out. Essentially it calls ReturnType::from(ValueType) to bridge the two. > What happens if your method tries to return something that's not an io::Error or a gz::Error? I guess the compiler catches that? If there is no available conversion to the return error type, compilation fails. > How would you handle doing this for multiple methods in the same file? Would you rename your enum to DecompError or something to avoid conflicts? That is an option, although the slightly sad truth is libraries usually have a single big error type and every function returns that. Convenient fine grained errors in rust remains unsolved, as far as I know. You can do it but it’s a lot of manual work.
- Mavvie 3y agoGreat, thanks for the reply!
- MrJohz 3y ago> Rust makes the caller allocate the buffer for reads. I don't think this is true in this context. The buffer initially will have a capacity of 0, and will grow to fit the available data, so that as read_to_end is inserting data, the buffer will be resized until all the data fits. However, if we had preallocated the buffer, or were reusing an existing buffer, then the buffer would only be grown if the data being read was too large for the buffer. In addition, there are other functions that can will never resize the buffer, and read only until the buffer is filled. Perhaps a better way of phrasing this is that Rust lets the caller control where the data will be written to.
- cbarrick 3y agoFair. "Allocate" is the wrong word. For the sake of a terse inline comment, it might be better to just s/allocate/create/.
- posix86 3y agoIn rust, adding context is easy: maybeError.map_err(...)?