Why Option and Result?
Many languages use null
\ nil
\ undefined
types to represent empty outputs, and Exceptions
to handle errors. Rust skips using both, especially to prevent issues like null pointer exceptions, sensitive data leakages through exceptions, etc. Instead, Rust provides two special generic enums;Option
and Result
to deal with above cases.
💭 In the previous sections, we have discussed about the basics of enums, generics and
Result
&Option
types.
As you know,
- An optional value can have either Some value or no value/ None.
- A result can represent either success/ Ok or failure/ Err
// An output can have either Some value or no value/ None.
enum Option<T> { // T is a generic and it can contain any type of value.
Some(T),
None,
}
// A result can represent either success/ Ok or failure/ Err.
enum Result<T, E> { // T and E are generics. T can contain any type of value, E can be any error.
Ok(T),
Err(E),
}
💭 Also as we discussed in preludes, not only Option
and Result
, and also their variants are in preludes. So, we can use them directly without using namespaces in the code.
Basic usages of Option
When writing a function or data type,
- if an argument of the function is optional,
- if the function is non-void and if the output it returns can be empty,
- if the value of a property of the data type can be empty,
we have to use their data type as an Option
type.
For example, if the function outputs a &str
value and the output can be empty, the return type of the function should be set as Option<&str>
.
fn get_an_optional_value() -> Option<&str> {
//if the optional value is not empty
return Some("Some value");
//else
None
}
In the same way, if the value of a property of a data type can be empty or optional like the middle_name
of the Name
data type in the following example, we should set its data type as an Option
type.
struct Name {
first_name: String,
middle_name: Option<String>, // middle_name can be empty
last_name: String,
}
💭 As you know, we can use pattern matching to catch the relevant return type (Some
/ None
) via match
. There is a function in std::env
called home_dir()
to get the current user’s home directory. However, not all users have a home directory in systems like Linux, so the home directory of a user can be optional. So it returns an Option
type; Option<PathBuf>
.
use std::env;
fn main() {
let home_path = env::home_dir();
match home_path {
Some(p) => println!("{:?}", p), // This prints "/root", if you run this in Rust playground
None => println!("Can not find the home directory!"),
}
}
⭐ However, when using optional arguments with functions, we have to pass None
values for empty arguments while calling the function.
fn get_full_name(fname: &str, lname: &str, mname: Option<&str>) -> String { // middle name can be empty
match mname {
Some(n) => format!("{} {} {}", fname, n, lname),
None => format!("{} {}", fname, lname),
}
}
fn main() {
println!("{}", get_full_name("Galileo", "Galilei", None));
println!("{}", get_full_name("Leonardo", "Vinci", Some("Da")));
}
// 💡 Better create a struct as Person with fname, lname, mname fields and create a impl function as full_name()
🔎 Other than that, Option
types are used with nullable pointers in Rust. Because there are no null pointers in Rust, the pointer types should point to a valid location. So if a pointer can be nullable, we have use Option<Box<T>>
.
Basic usages of Result
If a function can produce an error, we have to use a Result
type by combining the data type of the valid output and the data type of the error. For example, if the data type of the valid output is u64
and error type is String
, the return type should be Result<u64, String>
.
fn function_with_error() -> Result<u64, String> {
//if error happens
return Err("The error message".to_string());
// else, return valid output
Ok(255)
}
💭 As you know, we can use the pattern matching to catch the relevant return types (Ok
/Err
) via match
. There is a function to fetch the value of any environment variable in std::env
called var()
. Its input is the environment variable name. This can produce an error if we pass a wrong environment variable or the program cannot extract the value of the environment variable while running. So, its return type is a Result
type; Result<String, VarError>
.
use std::env;
fn main() {
let key = "HOME";
match env::var(key) {
Ok(v) => println!("{}", v), // This prints "/root", if you run this in Rust playground
Err(e) => println!("{}", e), // This prints "environment variable not found", if you give a nonexistent environment variable
}
}
is_some(), is_none(), is_ok(), is_err()
Other than match
expressions, Rust provides is_some()
, is_none()
and is_ok()
, is_err()
functions to identify the return type.
fn main() {
let x: Option<&str> = Some("Hello, world!");
assert_eq!(x.is_some(), true);
assert_eq!(x.is_none(), false);
let y: Result<i8, &str> = Ok(10);
assert_eq!(y.is_ok(), true);
assert_eq!(y.is_err(), false);
}
ok(), err() for Result types
In addition to that, Rust provides ok()
and err()
for Result
types. They convert the Ok<T>
and Err<E>
values of a Result
type to Option
types.
fn main() {
let o: Result<i8, &str> = Ok(8);
let e: Result<i8, &str> = Err("message");
assert_eq!(o.ok(), Some(8)); // Ok(v) ok = Some(v)
assert_eq!(e.ok(), None); // Err(v) ok = None
assert_eq!(o.err(), None); // Ok(v) err = None
assert_eq!(e.err(), Some("message")); // Err(v) err = Some(v)
}