error handling when unwrapping several try_into calls - rust

I have a case where I need to parse some different values out from a vector.
I made a function for it, that returns a option, which either should give a option or a None, depending on whether the unwrapping succeeds.
Currently it looks like this:
fn extract_edhoc_message(msg : Vec<u8>)-> Option<EdhocMessage>{
let mtype = msg[0];
let fcnt = msg[1..3].try_into().unwrap();
let devaddr = msg[3..7].try_into().unwrap();
let msg = msg[7..].try_into().unwrap();
Some(EdhocMessage {
m_type: mtype,
fcntup: fcnt,
devaddr: devaddr,
edhoc_msg: msg,
})
}
But, I would like to be able to return a None, if any of the unwrap calls fail.
I can do that by pattern matching on each of them, and then explicitly return a None, if anything fails, but that would a lot of repeated code.
Is there any way to say something like:
"if any of these unwraps fail, return a None?"

This is exactly what ? does. It's even shorter than the .unwrap() version:
fn extract_error_message(msg: Vec<u8>) -> Option<EdhocMessage> {
let m_type = msg[0];
let fcntup = msg[1..3].try_into().ok()?;
let devaddr = msg[3..7].try_into().ok()?;
let edhoc_msg = msg[7..].try_into().ok()?;
Some(EdhocMessage {
m_type,
fcntup,
devaddr,
edhoc_msg
})
}
See this relevant part of the Rust Book.

Related

Trying to get rid of unwraps

I have this line in my program:
let date = file.metadata().unwrap().modified().unwrap();
Can it be changed into form of if let Ok(date) = file.metadata().something.... and still be one liner?
Forgot to add: can't use ? operator, bc this is in a closure in for_each().
Using Result::and_then:
if let Ok(date) = file.metadata().and_then(|md| md.modified()) {
// stuff
}
Using the "try" operator (?):
// containing function returns `Result<T, E>` where `E: From<io::Error>`
let date = file.metadata()?.modified()?;
If you're inside a closure which must return (), and you want to ignore the error, I'd actually recommend using let else as such:
let Ok(metadata) = file.metadata() else { return };
let Ok(date) = metadata.modified() else { return };
// ...
This has the advantage that it doesn't increase the indentation level.

Why does a value created inside a function borrow and how can I avoid this pattern?

I'm new to rust but an engineer of over 6 years in various other languages from Javascript to Go.
I'm wondering why here the value is borrowed when I convert the response body to an "object".
I understand that the function owns the value and then the value is destroyed when the function returns BUT functions exist to create and return values. So there's clearly something fairly big I'm missing here. Can someone set me straight?
let response = match self
.client
.index(IndexParts::IndexId(index, id))
.body(json!({
"index": index,
"body": doc,
}))
.send()
.await
{
Ok(response) => response,
Err(err) => {
return Err(Box::new(err));
}
};
let response_body = match response.json::<Value>().await {
Ok(response_body) => response_body,
Err(err) => {
return Err(Box::new(err));
}
};
let response_map = response_body.as_object();
Ok(response_map)
I understand that the function owns the value and then the value is destroyed when the function returns BUT functions exist to create and return values. So there's clearly something fairly big I'm missing here.
You need to return an owned value, not a reference into a local. I assume what you're doing now boils down to:
fn foo() -> &Map<String, Value> {
let x = serde_json::json!({}); // except you get it by http
x.as_object().unwrap() // except you do proper error handling
}
This doesn't compile because you're returning the reference to a local value. Instead, you need to return the value itself:
fn foo() -> Map<String, Value> {
let x = serde_json::json!({}); // except you get it by http
match x {
Value::Object(o) => o,
_ => unreachable!(), // you'd return Err(...)
}
}
But even this is more complicated than you need. Since you already deserialize the value yourself, and handle the errors, you can simply ask serde to deliver a Map<String, Value> to begin with:
let response_body = match response.json::<Map<String, Value>>().await {
Ok(response_body) => response_body,
Err(err) => ...
};
Of course, you'll also need to adjust the return type to return the actual value instead of a reference.

How can I pull data out of an Option for independent use?

Is there a way to 'pull' data out of an Option? I have an API call that returns Some(HashMap). I want to use the HashMap as if it weren't inside Some and play with the data.
Based on what I've read, it looks like Some(...) is only good for match comparisons and some built-in functions.
Simple API call pulled from crate docs:
use std::collections::HashMap;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let resp = reqwest::blocking::get("https://httpbin.org/ip")?
.json::<HashMap<String, String>>()?;
println!("{:#?}", resp.get("origin"));
Ok(())
}
Result:
Some("75.69.138.107")
if let Some(origin) = resp.get("origin") {
// use origin
}
If you can guarantee that it's impossible for the value to be None, then you can use:
let origin = resp.get("origin").unwrap();
Or:
let origin = resp.get("origin").expect("This shouldn't be possible!");
And, since your function returns a Result:
let origin = resp.get("origin").ok_or("This shouldn't be possible!")?;
Or with a custom error type:
let origin = resp.get("origin").ok_or(MyError::DoesntExist)?;
The most common way is with if let:
if let Some(origin) = resp.get("origin") {
origin.do_stuff()
}
For more fine grained control, you can use pattern matching:
match resp.get("origin") {
Some(origin) => origin.do_stuff(),
None => panic!("origin not found!")
}
You could also use unwrap, which will give you the underlying value of the option, or panic if it is None:
let origin = resp.get("origin").unwrap();
You can customize the panic message with expect:
let origin = resp.get("origin").expect("Oops!");
Or compute a default value with unwrap_or:
let origin = resp.get("origin").unwrap_or(&String::from("192.168.0.1"));
You can also return an error instead of panicking:
let origin = resp.get("origin").ok_or(Error::UnknownOrigin)?;
Your options are a plenty.
if let Some(origin) = resp.get("origin") {
// do stuff using origin
}
origin = resp.get("origin").unwrap()
// will panic if None
resp.get("origin").map(|origin| {
// do stuff using inner value, returning another option
})
resp.get("origin").and_then(|origin| {
// same as map but short-circuits if there is no inner value
})

Why can't this be done with if?

I'm trying to handle errors received from an async call:
let res: Result<TcpStream, Box<dyn std::error::Error>> = session.runtime().borrow_mut().block_on(async {
let fut = TcpStream::connect(session.configuration().socket()).await?;
Ok(fut)
});
I tried to do it the old school way with an if but the compiler didn't like it:
if res.is_err() {
return Err(res);
}
After some googling I came across this:
let mut stream = match res {
Ok(res) => res,
Err(res) => return Err(res),
};
which feels very much the same but with Rusts' equivalent of a switch statement. Why can't I use the if?
if res.is_err() { return res } should work. Result is an enum with two variants: Ok which by convention holds a "successful" result, and Err which holds error information. As John pointed out, wrapping the existing Result (which happens to hold an Err) in another Err result doesn't make sense - or, more precisely, doesn't match the return type of the function.
When you use match, you unpack the result into its constituent values, and then in the error case re-pack it into a new result. Note that instead of the match statement use can use the ? operator, which would compress the declaration to just:
let mut stream = res?;

Why is this hashmap search slower than expected?

What is the best way to check a hash map for a key?
Currently I am using this:
let hashmap = HashMap::<&str, &str>::new(); // Empty hashmap
let name = "random";
for i in 0..5000000 {
if !hashmap.contains_key(&name) {
// Do nothing
}
}
This seems to be fast in most cases and takes 0.06 seconds when run as shown, but when I use it in this following loop it becomes very slow and takes almost 1 min on my machine. (This is compiling with cargo run --release).
The code aims to open an external program, and loop over the output from that program.
let a = vec!["view", "-h"]; // Arguments to open process with
let mut child = Command::new("samtools").args(&a)
.stdout(Stdio::piped())
.spawn()
.unwrap();
let collect_pairs = HashMap::<&str, &str>::new();
if let Some(ref mut stdout) = child.stdout {
for line in BufReader::new(stdout).lines() {
// Do stuff here
let name = "random";
if !collect_pairs.contains_key(&name) {
// Do nothing
}
}
}
For some reason adding the if !collect_pairs.contains_key( line increases the run time by almost a minute. The output from child is around 5 million lines. All this code exists in fn main()
EDIT
This appears to fix the problem, resulting in a fast run time, but I do not know why the !hashmap.contains_key does not work well here:
let n: Option<&&str> = collect_pairs.get(name);
if match n {Some(v) => 1, None => 0} == 1 {
// Do something
}
One thing to consider is that HashMap<K, V> uses a cryptographically secure hashing algorithm by default, so it will always be a bit slow by nature.
get() boils down to
self.search(k).map(|bucket| bucket.into_refs().1)
contains_key is
self.search(k).is_some()
As such, that get() is faster for you seems strange to me, it's doing more work!
Also,
if match n {Some(v) => 1, None => 0} == 1 {
This can be written more idiomatically as
if let Some(v) = n {
Ive found my problem, Im sorry I didnt pick up until now. I wasnt checking the return of if !collect_pairs.contains_key(&name) properly. It returns true for some reason resulting in the rest of the if block being run. I assumed it was evaluating to false. Thanks for the help

Resources