Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to reduce boilerplate nested Result in Rust

Tags:

rust

I have code using a nested Result like this:

fn ip4(s: &str) -> Result<(u8, u8, u8, u8), num::ParseIntError> {
    let t: Vec<_> = s.split('.').collect();
    match t[0].parse::<u8>() {
        Ok(a1) => {
            match t[1].parse::<u8>() {
                Ok(a2) => {
                    match t[2].parse::<u8>() {
                        Ok(a3) => {
                            match t[3].parse::<u8>() {
                                Ok(a4) => {
                                    Ok((a1, a2, a3, a4))
                                }
                                Err(er) => Err(er)
                            }
                        },
                        Err(er) => Err(er)
                    }
                }
                Err(er) => Err(er)
            }
        }
        Err(er) => Err(er),
    }
}

Is there any function or composing way to reduce this? Something like Haskell or Scala programmers do:

fn ip4(s: &str) -> Result<(u8, u8, u8, u8), num::ParseIntError> {
  let t: Vec<_> = s.split('.').collect();
  Result
  .lift((,,,))
  .ap(() -> t[0].parse::<u8>())
  .ap(() -> t[1].parse::<u8>())
  .ap(() -> t[2].parse::<u8>())
  .ap(() -> t[3].parse::<u8>()) // maybe more concise in Haskell or Scala but I think it's enough :)
}
like image 574
o0omycomputero0o Avatar asked Nov 16 '17 06:11

o0omycomputero0o


3 Answers

The answer to your direct question is the questionmark operator which would allow you to replace your whole match block with

Ok((
    t[0].parse::<u8>()?,
    t[1].parse::<u8>()?,
    t[2].parse::<u8>()?,
    t[3].parse::<u8>()?,
))

where essentially ? will return the error immediately if one is encountered.

That said, Rust already provides APIs for parsing IP addresses. Even if you wanted to maintain your tuple approach (though why would you), you could implement your function as

fn ip4(s: &str) -> Result<(u8, u8, u8, u8), net::AddrParseError> {
    let addr: net::Ipv4Addr = s.parse()?;
    let octets = addr.octets();
    Ok((octets[0], octets[1], octets[2], octets[3]))
}

or just pass around the Ipv4Addr value directly.

like image 141
loganfsmyth Avatar answered Nov 12 '22 07:11

loganfsmyth


Though, I do not see anything bad in @loganfsmyth's answer, I want to add another solution.

Your problem is a very simple and general problem of all programming languages which can be solved very easily if you would have enough time or practice in optimizing solutions. There is some divide and conquer recursive technique which is usually used to solve such problems. For a start, imagine a more simple thing: parsing a single octet from a string. This is a simple parse which you already know. Then mentally try to expand this problem to a larger one - parsing all octets which is a simple repeating process of the smallest problem we have solved earlier (parsing a single octet). This leads us to an iterative/recursive process: do something until. Keeping this in mind I have rewritten your function to a simple iterative process which uses tail-recursion which will not cause a stack overflow as a usual recursion due to it's form:

use std::num;

#[derive(Debug, Copy, Clone)]
struct IpAddressOctets(pub u8, pub u8, pub u8, pub u8);
type Result = std::result::Result<IpAddressOctets, num::ParseIntError>;

fn ipv4(s: &str) -> Result {
    let octets_str_array: Vec<_> = s.split('.').collect();

    // If it does not contain 4 octets then there is a error.
    if octets_str_array.len() != 4 {
        return Ok(IpAddressOctets(0, 0, 0, 0))  // or other error type
    }

    let octets = Vec::new();

    fn iter_parse(octets_str_array: Vec<&str>, mut octets: Vec<u8>) -> Result {
        if octets.len() == 4 {
            return Ok(IpAddressOctets(octets[0], octets[1], octets[2], octets[3]))
        }

        let index = octets.len();
        octets.push(octets_str_array[index].parse::<u8>()?);
        iter_parse(octets_str_array, octets)
    }

    iter_parse(octets_str_array, octets)
}

fn main() {
    println!("IP address octets parsed: {:#?}", ipv4("10.0.5.234"));
}

Keep in mind that Rust language is a bit more functional than you might think.

Also, I would recommend you to read this book which greatly explains the solution.

like image 44
Victor Polevoy Avatar answered Nov 12 '22 06:11

Victor Polevoy


You can use early returns to prevent the nesting (but not the repetition).

Note the body of the Err arms of the matches:

fn ip4(s: &str) -> Result<(u8, u8, u8, u8), num::ParseIntError> {
    let t: Vec<_> = s.split('.').collect();

    let a1 = match t[0].parse::<u8>() {
        Ok(x) => x,
        Err(er) => return Err(er),
    };
    let a2 = match t[1].parse::<u8>() {
        Ok(x) => x,
        Err(er) => return Err(er),
    };
    let a3 = match t[2].parse::<u8>() {
        Ok(x) => x,
        Err(er) => return Err(er),
    };
    let a4 = match t[3].parse::<u8>() {
        Ok(x) => x,
        Err(er) => return Err(er),
    };

    (a1, a2, a3, a4)
}

But, as the others have said, Rust already has a built-in way to parse IP addresses.

like image 27
Kroltan Avatar answered Nov 12 '22 06:11

Kroltan