1
\$\begingroup\$

The Problem

As part of my doomed efforts to learn Rust, I decided to get to grips with how the language handles classes and objects. But, instead of taking on some well-known exercise, I thought it would be more fun to try to answer a question which has been nibbling at the back of my mind since I was eleven or twelve...

What is this question? The Wikipedia page for the song "Flowers in the Window" by Scottish rock band Travis describes the video like this:

The video features the band walking around a small town where all the residents are pregnant women. The band enter a clinic where women are staying and begin to perform the song. A chained-up man in distress appears at the end of the video.

The question which has fascinated me for two decades is: Would such a community actually be feasible, from a mathematical point of view? And, if so, what is the mathematical limit on its size? My answer to this involves simulating the scenario with

The Code

main.rs

/// This is the entry point to the program.

// Declare local modules.
mod female;
mod gynaeceum;
mod utils;

/// This is where the magic happens.
fn main() {
    let mut gynaeceum = gynaeceum::Gynaeceum::new();

    gynaeceum.run_sim();
}

female.rs

/// This code defines the Female class.

// Local imports.
use crate::utils;

// Local constants.
const PROB_FALLING_PREGNANT_PER_COPULATION: f64 = 0.05;
const PROB_FEMALE_OFFSPRING: f64 = 0.5;
const LENGTH_OF_PREGNANCY_IN_DAYS: i32 = 270;
const LENGTH_OF_YEAR_IN_DAYS: i32 = 365;
const MIN_FERTILE_AGE_IN_DAYS: i32 = 15*LENGTH_OF_YEAR_IN_DAYS;
const MAX_FERTILE_AGE_IN_DAYS: i32 = 45*LENGTH_OF_YEAR_IN_DAYS;

// Helper enum.
#[derive(PartialEq)]
pub enum FemaleStatusCode {
    Normal,
    GivenBirth,
    Infertile
}

/// Structure.
pub struct Female {
    id: i32,
    age_in_days: i32,
    is_pregnant: bool,
    days_of_pregnancy: i32
}

/// Implementation.
impl Female {
    pub fn new(id: i32) -> Female {
        let result =
            Female {
                id: id,
                age_in_days: 0,
                is_pregnant: false,
                days_of_pregnancy: 0
            };

        return result;
    }

    pub fn set_age(&mut self, new_age_in_days: i32) {
        self.age_in_days = new_age_in_days;
    }

    pub fn can_copulate(&mut self) -> bool {
        if
            self.is_pregnant ||
            self.age_in_days < MIN_FERTILE_AGE_IN_DAYS ||
            self.age_in_days > MAX_FERTILE_AGE_IN_DAYS
        {
            return false;
        }

        return true;
    }

    pub fn copulate(&mut self) -> bool {
        if self.is_pregnant {
            return false;
        }

        if utils::happens(PROB_FALLING_PREGNANT_PER_COPULATION) {
            self.is_pregnant = true;

            return true;
        }

        return false;
    }

    pub fn give_birth(&mut self) -> bool {
        self.is_pregnant = false;
        self.days_of_pregnancy = 0;

        if utils::happens(PROB_FEMALE_OFFSPRING) {
            return true; // I.e. has given birth to a girl.
        }

        return false; // I.e. has given birth to a boy.
    }

    pub fn tick(&mut self) -> FemaleStatusCode {
        self.age_in_days += 1;

        if self.is_pregnant {
            if self.days_of_pregnancy > LENGTH_OF_PREGNANCY_IN_DAYS {
                return FemaleStatusCode::GivenBirth;
            } else {
                self.days_of_pregnancy += 1;
            }
        } else if self.age_in_days > MAX_FERTILE_AGE_IN_DAYS {
            return FemaleStatusCode::Infertile;
        }

        return FemaleStatusCode::Normal;
    }

    pub fn get_id(&mut self) -> i32 {
        return self.id;
    }
}

gynaeceum.rs

/// This code defines a class which models "the Gynaeceum".

// Standard imports.
use std::collections::HashMap;

// Non-standard imports.
use rand::seq::SliceRandom;

// Local imports.
use crate::female;

// Local constants.
const SIM_LENGTH_IN_DAYS: i32 = 365*1000;
const SIM_INITIAL_FEMALE_COUNT: i32 = 4;
const FOUNDING_FEMALE_AGE_IN_DAYS: i32 = 365*20;

/// Structure.
pub struct Gynaeceum {
    duration_in_days: i32,
    next_ticket: i32,
    females: HashMap<i32, female::Female>
}

/// Implementation.
impl Gynaeceum {
    pub fn new() -> Gynaeceum {
        let result = 
            Gynaeceum {
                duration_in_days: 0,
                next_ticket: 1,
                females: HashMap::new()
            };

        return result;
    }

    fn get_ticket(&mut self) -> i32 {
        let result = self.next_ticket;

        self.next_ticket += 1;

        return result;
    }

    fn add_female(&mut self) {
        let mut new_female = female::Female::new(self.get_ticket());

        self.females.insert(new_female.get_id(), new_female);
    }

    fn process_retirees(&mut self, retiree_ids: Vec<i32>) {
        for id in retiree_ids {
            self.females.remove(&id);
        }
    }

    fn service(&mut self, available: Vec<i32>) -> bool {
        if available.len() == 0 {
            return false;
        }

        let selected_id = available.choose(&mut rand::thread_rng());
        let selected = self.females.get_mut(selected_id.unwrap()).unwrap();

        selected.copulate();

        return true;
    }

    fn tick(&mut self) {
        let mut available = Vec::new();
        let mut retiree_ids = Vec::new();
        let mut birth_count = 0;

        for (id, female) in self.females.iter_mut() {
            let status_code = female.tick();

            if status_code == female::FemaleStatusCode::GivenBirth {
                if female.give_birth() {
                    birth_count += 1;
                }
            } else if status_code == female::FemaleStatusCode::Infertile {
                retiree_ids.push(*id);
            } else if status_code == female::FemaleStatusCode::Normal {
                if female.can_copulate() {
                    available.push(*id);
                }
            }
        }

        for _ in 0..birth_count {
            self.add_female();
        }

        self.process_retirees(retiree_ids);
        self.service(available);
        self.duration_in_days += 1;
    }

    pub fn run_sim(&mut self) {
        for _ in 0..SIM_INITIAL_FEMALE_COUNT {
            self.add_female();
        }

        for (_, female) in self.females.iter_mut() {
            female.set_age(FOUNDING_FEMALE_AGE_IN_DAYS);
        }

        while self.duration_in_days <= SIM_LENGTH_IN_DAYS {
            self.tick();
        }

        println!("Female count: {}", self.females.len());
    }
}

utils.rs

/// This code defines some utility functions.

// Standard import.
use rand::Rng;

/// Decide randomly whether an event with a given probability happens.
pub fn happens(event_probability: f64) -> bool {
    if rand::thread_rng().gen_range(0.0..1.0) < event_probability {
        return true;
    }

    return false;
}

The Results

  • After sufficiently many days, the population seems to level off at around 400.
  • If one tinkers with the constant PROB_FALLING_PREGNANT_PER_COPULATION, any increase or decrease will result in a directly proportional change in the population limit.
  • Tinkering with the other constants did not result in any appreciable change in the population limit.
\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

Not taking the bait on you choice of examples. The two problems with the code itself that jumped out at me first are:

If all you’re doing is giving each person a unique numeric identifier, you should be storing them in a Vec, not a HashMap, which has much higher overhead. The key should be the index within the Vec.

You don’t need to create a separate crate for each struct. The name of the struct is a namespace. Here, there’s no point, because you have a data structure that’s only used in one block of code, which uses its internals and has no name conflicts. So it’s an internal implementation detail of the same crate, not some separate library you’ll want to re-use in other projects. If you do want to group several things together in the same namespace within a crate, declare a module.

\$\endgroup\$
3
  • \$\begingroup\$ Forgive me for a very very late reply to your insightful answer. I have a couple of follow-up questions, if that's okay with you? \$\endgroup\$
    – Tom Hosker
    Aug 14 at 17:59
  • \$\begingroup\$ (1) I used a HashMap rather than a Vec to dodge any mind-bending bugs with adding and removing specific items within for loops. Do you still think I made the wrong choice? If you know a better way, I'm genuinely eager to hear it. \$\endgroup\$
    – Tom Hosker
    Aug 14 at 17:59
  • \$\begingroup\$ (2) So I declared a crate in the code whenever I wanted to import from another file in the same folder. I couldn't get any other syntax that did the same thing to compile. If you of a better way, could you flesh it out with a bit more of a snippet? \$\endgroup\$
    – Tom Hosker
    Aug 14 at 17:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.