What is Result<T>?

I found a function with this signature and it confused me: fn read_uptime() -> Result<u64>. When I read about Result it seemed straight forward. It is an Enum that returns Ok or Err. As seen here: https://doc.rust-lang.org/std/result/index.html

How can a function return a Result without any mention of the error? I found this example here: https://rust-lang-nursery.github.io/rust-cookbook/errors/handle.html. I've seen it in other places as well out in the wild but I don't understand this example in particular.

When we use the read_uptime function in main() how can it return anything other than an i64? I see the match arm for the error but clearly we don't have an error in the function signature.

It's common for libraries to make a typedef of Result with their Error type baked in to reduce repetition. You can even find an example of in the Standard Library's fmt module (std::fmt::Result)

To add to @spunit262's answer, you can tell the exact type of a function by clicking on it in the rustdoc.

For instance, at first glance std::fs::read appears to return Result<Vec<u8>> with only one type. But if you click on the "Result" in the documentation, you get to https://doc.rust-lang.org/std/io/type.Result.html (std::io::Result's doc page) and can see that it's a type defined as:

type Result<T> = Result<T, Error>;

Clicking on the second Result in the doc page will bring you back to https://doc.rust-lang.org/std/result/enum.Result.html - the "real" Result type. (And clicking on Error brings you to std::io::Error)

So, if you were to fully expand it, std::fs::read's signature would read:

std::result::Result<std::vec::Vec<u8>, std::io::Error>
2 Likes

Others can dive into the technical details better than me. But you gave the answer to that question in your post already:

So there we have it. The function does not return a i64 it returns an enum which may have a value which is an i64. That is the 'Result' enum.

Of course being a 'Result' enum it may not be carrying the result at all. It could be carrying an 'Error' instead.

So one should do a match on that 'Result' that is returned to see if it is a valid result or an error.

That example you linked to is reading a file to get the uptime, '/proc/uptime'. Which of course could fail.

In that case that code checks the return Result not with a 'match' but using the '?' syntax:

File::open("/proc/uptime")?.read_to_string(&mut uptime)?;

That '?' is a short hand for doing a match on the enum. It will return immediately from the function if the read fails, with an Error value in the Result enum.

If there is no error reading the file the function proceeds to return an 'Ok' as the Result enum.

1 Like

The function does not return a i64 it returns an enum which may have a value which is an i64.

Ah Ok. So I've already been using this with Option. I guess I didn't connect the dots on only specifying the Type that could be returned if Ok().

So we could return some type of Error with Result but haven't explicitly said which type of Error. We have declared that the Ok result will contain an i64 type variable though.

or with unwrap(), unwrap_or(), unwrap_or_default(), unwrap_or_else(), expect(...), map(), and_then(),..... is that clear? :wink:

1 Like

We haven't said. The library author, who created the type, had.

Makes sense :+1: I thought I had a good handle on Errors. Apparently I just didn't understand Result that well. And Enums maybe. But this seems clear now.

No, not really. The implicitly in-scope std::result::Result is shadowed by the typedef, so after using the typedef, the bare name Result<i64> will refer to a type that is an alias for std::result::Result<i64, SomeErrorType>.

Result isn't special and the language doesn't assume anything about either of its type parameters. It's only that there has been a bit of "rewriting of history", so to say, using the typedef.

1 Like

Ok that makes more sense. If I think of it as an alias that helps create a mental model. Will research how this typedef is done for further understanding.

1 Like

It literally is just an alias. It's pretty similar to shadowing a variable name only for types. So it's less special than it might appear. For example, with your own types you can do:

mod mymodule {
    pub struct MyType<T, U> {
         pub foo: T,
         pub bar: U,
    }
}
type MyType<T> = mymodule::MyType<T, u16>;

Here the first MyType takes two generic types. The second MyType aliases the first but fills in one of the types for you so it only needs to take one itself. It doesn't matter that they have the same name because they are declared in different scopes.

2 Likes