Rust Errors - Monomorphism & Option

Thomas J. Kennedy

Contents:

1 Overview

What does monomorphic equivalance actually mean? Let us start with the first word.

Example 1: Definition - monomorphic

monomorphic (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

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…

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.