While working on my iced chart implementation, I'm trying to make the structs holding the series more generic. I would like to have a series that will work with all values that implement Into<(f32, f32)> or with (f32, f32) itself. I can get the struct to work with either one or the other, but not for both. Is this even possible or do I always need to wrap (f32, f32) in a struct?
What that phrase even means? Any T is always From<T> and Into<T> which means one implementation should be enough. The one that receives Into<(f32, f32)>.
The thing that feels wrong here is that you're hard-coding too many uses of references. You should use general generic types and iterators, instead. Allow adaptation via iterator adapters. You will never be able to make From do everything you could possibly want, so make it easy to not need it.
struct PointSeries<I> {
data: I,
}
fn draw<I>(point_series: PointSeries<I>)
where
I: IntoIterator<Item: Into<(f32, f32)>>,
{
for p in point_series.data.into_iter() {
let (x, y) = p.into();
println!("x: {x}, y: {y}");
}
}
fn main() {
let data = ...;
let point_series = PointSeries { data: &data };
draw(point_series);
let data = vec![(0.0, 0.0), (1.0, 1.0)];
let point_series = PointSeries {
data: data.iter().copied(),
};
// or, consuming:
// let point_series = PointSeries {data};
draw(point_series);
}
@kpreid Thanks for your help. I ended up using the references, because my real code is a bit more complicated and has multiple parts, where I need to use &PointSeries instead of PointSeries.
I'd generally still recommend not using references for the items, but unfortunately that’s difficult because there aren’t IntoIterator implementations for references that provide flexibility. One unusual option would be to use Iterator + Clone:
fn draw<I>(point_series: &PointSeries<I>)
where
I: Iterator<Item: Into<(f32, f32)>> + Clone,
{
for p in point_series.data.clone() {
let (x, y) = p.into();
println!("x: {x}, y: {y}");
}
}
...
fn main() {
...
let point_series = PointSeries { data: data.iter() };
draw(&point_series);
let data = vec![(0.0, 0.0), (1.0, 1.0)];
let point_series = PointSeries {
data: data.iter().copied(),
};
draw(&point_series);
}
This way, you get to use arbitrary iterator adapters and have the PointSeries by reference. Cloning many iterators is cheap — the main exception would be an iterator like std::vec::IntoIter which owns the elements it’s returning, so cloning it clones all the elements.
Follow up question. In my real code I have a chart struct that needs to hold arbitrary series types which should be generic over their data. I guess that this is only possible with dynamic dispatch, but I can not figure out how to set the trait bounds for this. Maybe I'm totally on the wrong track here:
You have to design the trait so that it returns a type-erased[1] iterator itself, and then implement it to box the iterator.
trait SeriesData {
fn series_data_iter(&self) -> Box<dyn Iterator<Item = (f32, f32)>>;
}
impl<T> SeriesData for T
where
T: /* pick your bounds here */,
{
fn series_data_iter(&self) -> Box<dyn Iterator<Item = (f32, f32)>> {
Box::new(todo!("make an iterator here"))
}
}
Note that I’ve sketched a blanket implementation[2] here, but one way to solve your reference-vs-owner problem is to implement SeriesData explicitly for different types like Vec. This allows you to use .copied() or not in the implementations as needed.