Rust Errors - Monomorphism & Option
Thomas J. Kennedy
1 Overview
What does monomorphic equivalance actually mean? Let us start with the first word.
Example 1: Definition - monomorphicmonomorphic (adjective) having but a single form, structural pattern, or genotype.
Retrieved from Merriam-Webster on 11 January 2025.
That leads to monomorphic equivalance as describing two or more things that have the same form. In our case Result
and Option
.
2 Huh?
In most cases… functions need to return
-
a value if everything went okay
-
a specific error if something went wrong.
These returns correspond to… Result::Ok
and Result::Err
, respectively.
Sometimes… a function may return some value if everything went okay and nothing if there was an error. In this case…
-
Result::Ok
is monomorphically equivalent toOption::Some
-
Result::Err
is monomorphically equivalent toOption::None
Hooray! We now know a fancy programming term! We should probably use the term in conversation at every given opportunity.
3 Why?
The Rust book uses a division by zero example for when Option
might be a good idea. While good for a mechanical demonstration… division by zero should almost certainly be handled by using a Result
.
Let us examine another do-not-worry-this-is-from-later-in-the-course example.
const MIN_LINEAR_DIM: f64 = 0.0_f64;
const MIN_COST: f64 = 0.01_f64;
pub fn read_house_from_str(room_data: &str) -> Option<House> {
let parsed_rooms: Vec<Room> = room_data
.lines()
.filter(|line| !line.is_empty())
.filter(|line| line.contains(";"))
.map(|line| {
let line = line.split(";").collect::<Vec<&str>>();
// Grab the name first
let name = line[0];
// Split everything else by whitespace
let the_rest: Vec<&str> = line[1].split_whitespace().collect();
(name, the_rest)
})
.filter(|(_, the_rest)| the_rest.len() >= 4)
.map(|(name, the_rest)| {
let nums: Vec<f64> = the_rest[0..3]
.iter()
.flat_map(|token| token.parse())
.collect();
// The flooring name might contain spaces.
// Combine the remainder of the line.
let flooring_name = the_rest.into_iter().skip(3).join(" ");
(name, nums, flooring_name)
})
.filter(|(_, nums, _)| nums.len() == 3)
.map(|(name, nums, flooring_name)| (name, nums[0], nums[1], flooring_name, nums[2]))
.filter(|(_, length, width, _, _)| *length > MIN_LINEAR_DIM && *width > MIN_LINEAR_DIM)
.filter(|(_, _, _, _, unit_cost)| *unit_cost >= MIN_COST)
.map(|(name, length, width, flooring_name, unit_cost)| {
Room::builder()
.with_name(name)
.with_dimensions(length, width)
.unwrap()
.with_flooring(
Flooring::builder()
.type_name(flooring_name)
.unit_cost(unit_cost)
.build(),
)
.build()
})
.collect();
match House::builder().with_rooms(parsed_rooms) {
Ok(builder) => {
let house = builder.build();
Some(house)
}
Err(_) => None,
}
}
Now… focus on the filter
lines. Each of these lines filters out data that does not meet specified criteria. It is possible that we end up filtering out every line of a file. That means that…
let parsed_rooms: Vec<Room> = room_data
// ...
// ...
.collect()
…might result in a Vec
that contains no (i.e., zero rooms).
match House::builder().with_rooms(parsed_rooms) {
Ok(builder) => {
let house = builder.build();
Some(house)
}
Err(_) => None,
}
The HouseBuilder::with_rooms
method returns a Result::Err
if parsed_rooms
is empty. We do not need to reason about the specific error… just return None
. If we have at least one (1) room in parsed_rooms
… we with Some(house)
.
This design only works because the specification for the read_house_from_str
function requires that all valid lines be processed and invalid lines be skipped.