6
votes

I am attempting to read values from a file in order to create a struct, and I'm getting a weird pair of errors. A super basic implementation of my code:

extern crate itertools;

use itertools::Itertools;
use std::io::{self, prelude::*, BufReader};
use std::fs::{self, File};

// The struct I will unpack into
struct BasicExample {
    a: String,
    b: String,
    c: String,
    d: String,
}

impl BasicExample {
    pub fn new(a: String, b: String, c: String, d: String} -> Self {
        BasicExample {
            a, b, c, d
        }
    }

    // I'm expecting that reading from the config file might fail, so
    // I want to return a Result that can be unwrapped. Otherwise an Err
    // will be returned with contained value being &'static str
    pub fn from_config(filename: &str) -> io::Result<Self, &'static str> {
        let file = File::open(filename).expect("Could not open file");

        // read args into a Vec<String>, consuming file
        let args: Vec<String> = read_config(file);

        // I transfer ownership away from args here
        let params: Option<(String, String, String, String)> = args.drain(0..4).tuples().next();

        // Then I want to match and return, I could probably do if-let here
        // but I don't have my hands around the base concept yet, so I'll 
        // leave that for later
        match params {
            Some((a, b, c, d)) => Ok(BasicExample::new(a, b, c, d)),
            _ => Err("Could not read values into struct")
        }
    }

    fn read_config(file: File) -> Vec<String> {
        let buf = BufReader::new(file);

        buf.lines()
            .map(|l| l.expect("Could not parse line"))
            .collect()
    }
}

Running cargo check to make sure I didn't miss anything, I get the following error:

error[E0107]: wrong number of type arguments: expected 1, found 2
  --> src/lib.rs:37:60
   |
37 |     pub fn from_config(filename: &str) -> io::Result<Self, &'static str> {
   |                                                            ^^^^^^^^^^^^ unexpected type argument

error: aborting due to previous error

For more information about this error, try `rustc --explain E0107`.

Seems a bit odd. io::Result should take <T, E>, and I've given it E, so let's remove that type argument and see what happens:

error[E0308]: mismatched types
  --> src/lib.rs:54:22
   |
54 |             _ => Err("Could not read values into AzureAuthentication struct"),
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `std::io::Error`, found reference
   |
   = note: expected type `std::io::Error`
              found type `&'static str`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

For some reason it is really not happy with the E I provided. I'm a complete beginner with rust, so maybe I'm just not sure what I'm looking at. What am I doing wrong here? The itertools ownership trick was borrowed (ha) from this wonderful answer.

System Details:

  • macOS 10.13.6
  • rustc 1.36.0 (a53f9df32 2019-07-03)
  • cargo 1.36.0 (c4fcfb725 2019-05-15)
2
Did you take a look at the documentation of std::io::Result? You will find that it actually takes only a single type parameter.Sven Marnach
Oh! You're completely right, it's not the same as Result, hence "Expected io::Error"C.Nivs
i like the way they gave different things the same nameguesty
@guesty they did, but they didn't. The module std::io means that the name Result is safe from shadowing, I just wasn't paying attention. In my code, I even noted io::ResultC.Nivs

2 Answers

9
votes

This is actually a super basic error, but one that looks arcane until you get to know (and love) std::io.

In short, std::result::Result (the result you know) !== std::io::Result. The documentation for the first is here, while the second is here

You'll notice on the second one that it is actually a type alias to Result<T, std::io::Error>. What this means is that it is effectively shorthand for that, where your error case is an instance of std::io::Error.

As a result, your code is incorrect when you are attempting to just Err() it with a string slice (since the slice is not std::io::Error, evidently).

There are multiple ways to fix this:

  • You can convert your entire error chain to another type (explicitly or by taking advantage of into() casts)
  • You can make your own errors return std::io::Error instances

There are valid cases for both options, which is why I'm mentioning both. The second is done relatively easily, like so (full paths are there for documentation purposes). Suppose you're returning an error which matches an entity not found. You would be able to do it like so:

`Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Could not read values into AzureAuthentication struct"))`

There is, however, a better way for your function:

pub fn from_config(filename: &str) -> io::Result<Self> {
    let file = File::open(filename)?;
    let args: Vec<String> = read_config(file); // This has no error possibility

    let params: Option<(String, String, String, String)> = args.drain(0..4).tuples().next();
    params.ok_or(std::io::Error::new(std::io::ErrorKind::NotFound, "Could not read values into struct")).map(|(a, b, c, d)| BasicExample::new(a,b,c,d))
}

This removes all the indirection from your method and neatly folds away the error types, one by one, so you don't have to worry about them. The Option gets turned into a Result thanks to ok_or, and all is well in the best of worlds :-)

7
votes

A common pattern in Rust is if your module uses a lot of Result<T, ModuleSpecificErrorType>, then you can make a custom Result<T> that abstracts out the error type. This custom type has one fewer generic parameter because the error type is hardcoded.

A std::io::Result<T> is an abstraction over std::result:Result<T, std::io::Error>.

See the documentation for io::Result.