3

I sumbled upon a problem while trying out Rust, that might point to a bigger non understanding of its concepts on my part.

My goal is to programm a variant of Conway's Game of Life. I want the values when cells are created or stay alive not to be hard coded, but within a struct. My first attempt was to create a struct

use std::ops::Range;

struct Rules {
    be_born: Range<usize>,
    stay_alive: Range<usize>,
}

impl Rules {
  pub fn new(be_born: Range<usize>, stay_alive: Range<usize>) -> Rules {
    Rules { be_born, stay_alive }
  }
}

let rules = Rules::new(2..4, 3..6);

This object is later used within the algorithm that iterates over all the cells. It works fine, until I also want to allow other kind of Ranges during creation like for example RangeTo (2..=3).

I know I could rewrite the struct Rules to be generic.

use std::ops::RangeBounds;

struct Rules<BR: RangeBounds<usize>, AR: RangeBounds<usize>> {
    be_born: BR,
    stay_alive: AR,
}

This in turn would force me to make all the algorithms I use to be generic as well. This seems to be quite a lot of overhead just to include two simple ranges.

On the other hand none of my attempts to include variables of type RangeBounds directly into my struct succeeded. I tried &dyn RangeBounds<usize> or Box<&dyn RangeBounds<usize>>, just to always get the error E0038 that I cannot make this trait into an object.

Is there any other way to get this done, or is there some other feasable way I do not see?

Thank you in advance for all your hints.

  • Instead of using RangeBounds as a type, try using it as a bound: `struct Rules {be_born: T, stay_alive: T}` – Ivan C Dec 31 '20 at 11:31
  • @IvanC You can't. It needs to have a generic type parameter, either as another generic parameter of struct `Rules` -which involves in using `PhantomData`- or as an const parameter like `usize`. – Ekrem Dinçel Dec 31 '20 at 11:42

1 Answers1

1

For a broad answer, we need to know how exactly you are iterating over be_born and stay_alive fields. But for solving the problem you point out that you want to use different kind of ranges, the easiest way is specifying that be_born and stay_alive fields are both an Iterable that returns usize when iterated over, aka Iterator<Item=usize>:

struct Rules<T, U> 
where
    T: Iterator<Item=usize>,
    U: Iterator<Item=usize>
{
    be_born: T,
    stay_alive: U,
}

impl<T, U> Rules<T, U>
where
    T: Iterator<Item=usize>,
    U: Iterator<Item=usize>
{
    pub fn new(be_born: T, stay_alive: U) -> Self {
        Self { be_born, stay_alive }
    }
}

fn main() {
    let rule = Rules::new(2..4, 3..6);
    let other_rule = Rules::new(2..=4, 3..=6);
    let another_rule = Rules::new(2..=4, 3..6);

    for x in rule.be_born {
        println!("born: {}", x);
    }
    for y in rule.stay_alive {
        println!("alive: {}", y);
    }
}

This will also allow you to assign non-range but iterable types to be_born and stay_alive fields. If you want to restrict yourself to range types only, you can replace every Iterator<Item=usize> in the code with Iterator<Item=usize> + RangeBounds<usize>, which means "a type which both implements Iterator<Item=usize> and RangeBounds<usize>".

For further usage of Iterator trait, see the book.

Ekrem Dinçel
  • 1,053
  • 6
  • 17
  • Thank you for your answer. It was not quite what I was looking for, but implictly you gave the answer I needed: I was on the total wrong track that I could store the trait object while avoiding Generics. For my actual problem I needed to rethink how I structure the program. So thank you for putting me back on the right track. – Rüdiger Ludwig Jan 02 '21 at 07:59
  • @RüdigerLudwig You are welcome. Use dynamic trait objects only when you can't do what you want using generics, generics are much easier to deal with and often provide a faster runtime. – Ekrem Dinçel Jan 02 '21 at 14:10