Need a `flat_map` on Result, I think

I am trying to write a function that, in words, does something like:

try to open a file. If it succeeds, read it, if the amount of data read is 64 bytes, yield those bytes as a Result. If any error along the way occurs, Give me the error as a string (normalizing it)

pub fn read_keypair(path: &String) -> std::result::Result<[u8; 64], &str> {
    File::open(path).map(|mut file| {
        let mut buf = [0u8; 64];
        file.read(&mut buf).map(|size| {
            match size {
                64 => buf,
                _ => Err("invalid file size"); // not right
            }
        })
    }).map_err(|e| e.to_string().as_str())
}

The error is an understandable "you can't return different types":

164 | /             match size {
165 | |                 64 => buf,
    | |                       --- this is found to be of type `[u8; 64]`
166 | |                 _ => Err("invalid file size"),
    | |                      ^^^^^^^^^^^^^^^^^^^^^^^^ expected array `[u8; 64]`, found enum `Result`
167 | |             }
    | |_____________- `match` arms have incompatible types

I think what I want here is a flat_map - what's the idiomatic way of handling this?

I think you want and_then.

But this is not going to work -- the to_string() value is only temporary, so you can't return &str from it. The way Rust's elided lifetimes work, your return value is pulling its lifetime from the only borrowed parameter, path, which doesn't make sense. Can you use Result<_, String> instead?

3 Likes

Better yet, don't use String as your error type, that doesn't confer any machine-readable information. If you have an I/O function, return io::Error instead.

You shouldn't take a &String parameter, either – it's more than useless, it's harmful to performance. If you have a string slice, that is, an &str that comes from somewhere else (and you do not have access to a String), then this forces the caller to allocate and copy a String just to be able to pass it by reference. You should be able to just take a &str which can be coerced from anything string-like. It would be even better to look at the signature of standard I/O functions and mimic what they do – they take an AsRef<Path>, because this also allows paths that don't conform to Rust's requirements around str (notably, UTF-8 validity).

Your function would also be much more readable using ? instead of Result combinators. All in all, I would write it like this:

pub fn read_keypair<P: AsRef<Path>>(path: P) -> io::Result<[u8; 64]> {
    let mut file = File::open(path)?;
    let mut buf = [0u8; 64];
    let size = file.read(&mut buf)?;
    
    if size == 64 {
        Ok(buf)
    } else {
        Err(io::Error::new(io::ErrorKind::InvalidData, "invalid file size"))
    }
}
6 Likes

This definitely works - As for the Err value - I struggle to see what advantage this gets me (functionally), but I'll accept the wisdom given here as I have no basis to challenge it.

As a matter of style, I prefer to use functional programming style - Hence why I was trying to leverage the map and and_then, etc.

You may want to replace read with read_exact in case the OS doesn't return 64 bytes in one syscall (Like if the path goes to a device file instead of a regular file). You could instead check for the ErrorKind::Interrupted and ErrorKind::UnexpectedEof manually if you want as well. Also these don't error if your file is more than 64bytes (maybe not be what you). If you want files to be exactly 64 bytes, you can either use read in a loop and error if you don't get an EoF after 64 bytes, or you could use the File::metadata method to get the length. The latter could suffer from a TOCTOU and may only work on regular files, but is probably good enough for most use cases.

2 Likes

why not just wrap the buf in Ok?