Ashish Negi
Ashish Negi

Reputation: 5301

Composing errors in Rust : concating results on Ok

I have following code. It works.

But i am more interested in writing make_tea where i call two functions : get_milk_from_cow and pour_milk. They both return Result<String, TeaError>.

How can i compose them so that i can keep concating Strings if they succeed otherwise return error.

enum TeaError {
    NoMilk,
    NoCup,
    NoCow,
    NoGas,
    NoSomething,
}

fn get_milk_from_cow(cow: bool) -> Result<String, TeaError> {
    if cow {
        Ok(String::from("get milk from cow"))
    } else {
        Err(TeaError::NoCow)
    }
}

fn pour_milk(milk: bool) -> Result<String, TeaError> {
    if milk {
        Ok(String::from("poured milk"))
    } else {
        Err(TeaError::NoMilk)
    }
}

fn make_tea() -> Result<String, TeaError> {
    let mut process = String::new();
    let step_cow = get_milk_from_cow(true)?;
    let step_milk = pour_milk(true)?;
    process.push_str(step_cow.as_str());
    process.push_str(step_milk.as_str());
    Ok(process)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn attemp_tea_one() {
        match pour_milk(false) {
            Err(v) => match v {
                TeaError::NoMilk => assert!(true),
                _ => assert!(false)
            },
            Ok(_) => assert!(false)
        };
    }

    #[test]
    fn attemp_tea_two() {
        match make_tea() {
            Err(_) => assert!(false),
            Ok(_) => assert!(true)
        };
    }
}

I tried :

process.push_str(get_milk_from_cow(true)?
       .push_str(pour_milk(true)?.as_str()))

but it gives :

error[E0308]: mismatched types
  --> src/errors.rs:27:22
   |
27 |     process.push_str(get_milk_from_cow(true)?.push_str(pour_milk(true)?.as_str()));
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected &str, found ()
   |
   = note: expected type `&str`
              found type `()`

as push_str does not return string.

Edit:

fn append(s1: String, s2: String) -> String {
    s1 + s2.as_str()
}
fn make_tea() -> Result<String, TeaError> {
    let process = append(get_milk_from_cow(true)?,
                         pour_milk(true)?);
    Ok(process)
}

so, i can do append(append(funcA(), funcB()), funcC()) and so on.. I am still learning about lifetimes and think about whether append can still be improved in memory allocation.

Upvotes: 0

Views: 1008

Answers (2)

E_net4
E_net4

Reputation: 30092

First things first: there appears to be nothing wrong with your code above, but I'll be assuming that you are looking for something more idiomatic.

Although requiring slightly more memory than your approach, the most elegant way to concatenate results of strings is this:

fn make_tea() -> Result<String, TeaError> {
    vec![get_milk_from_cow(true), pour_milk(true)].into_iter()
        .collect()
}

Playground

Explanation:

  • The vector is consumed into an iterator of Result<String, TeaError> (owned, not references).
  • collect then relies on two implementations of FromIterator:
    • impl<A, E, V> FromIterator<Result<A, E>> for Result<V, E> where V: FromIterator<A> produces a result containing either the result of building A from an iterator of T or the first error retrieved from the iterator. This is like turning an iterator of results into a result with a collected iterator.
    • impl FromIterator<String> for String concatenates all strings into an owned string

So, as soon as you already have an iterator that turns your process into a sequence of independent operations, you can just collect them.

Now, in order to prevent subsequent operations from being called after an error is found, then it's easier to stick to the ? operator.

fn make_tea() -> Result<String, TeaError> {
    Ok(vec![get_milk_from_cow(true)?, pour_milk(true)?].into_iter()
        .collect())
}

Playground


The vector had to be created because arrays do not provide iterators of owned elements (&T instead of T). However, we can go around that with an extra mapping:

fn make_tea() -> Result<String, TeaError> {
    Ok([get_milk_from_cow(true)?, pour_milk(true)?].into_iter()
        .map(|a| a.as_str())
        .collect())
}

This will map elements from &String into &str, which can be collected likewise.

Upvotes: 2

Veedrac
Veedrac

Reputation: 60237

This code does redundant work on the starred lines:

fn make_tea() -> Result<String, TeaError> {
*   let mut process = String::new();
    let step_cow = get_milk_from_cow(true)?;
    let step_milk = pour_milk(true)?;
*   process.push_str(step_cow.as_str());
    process.push_str(step_milk.as_str());
    Ok(process)
}

process.push_str(step_cow.as_str()) just makes an unneeded copy of step_cow. Instead, try

fn make_tea() -> Result<String, TeaError> {
    let mut process = get_milk_from_cow(true)?;
    process.push_str(&pour_milk(true)?);
    Ok(process)
}

or, more conveniently,

fn make_tea() -> Result<String, TeaError> {
    Ok(get_milk_from_cow(true)? + &pour_milk(true)?)
}

Upvotes: 2

Related Questions