rust-itertools / itertools

Extra iterator adaptors, iterator methods, free functions, and macros.
https://docs.rs/itertools/
Apache License 2.0
2.77k stars 309 forks source link

idea: TupleMap - alternating map calls #990

Closed joshka closed 2 months ago

joshka commented 2 months ago

Something that I want to do regularly in UI code is format a list of items with alternating style. Often this looks like*:

let items = ["a", "b", "c"]
let formatted = items.enumerate()
    .map(|(i, item)| if  i % 0 == 0 { format!("dark {item}") } else { format!("light {item}") })

I'd like to be able to write something like:

let formatted = items.tuple_map((
    |item| format!("dark {item}"),
    |item| format!("light {item}"),
));

Initially my need is for just 2 items, but sometimes it's handy to do this with 3 different things (various UI patterns need this).

I'm not sure if tuple map is the right name for this, and whether there are likely to be problems implementing this in a way that accepts multiple functions like this. Presenting as a request for feedback and sanity check before worrying about an implementation. And it's also entirely possible I've missed a simple approach here.

*the real code is applying Ratatui styles to table rows etc.

phimuemue commented 2 months ago

Hi, thanks for the idea.

I think ideally Rust would allow you to write one of these:

 std::iter::zip(
  items,
  [item| format!("light {item}"), item| format!("dark {item}")].iter().cycle(),
 )
 .map(|(item, formatter)| formatter(item))

But this breaks down due to the closures having different types.

Or - generalizing the function instead of the iterator's method:

items.map(make_alternating_fn((item| format!("light {item}"), item| format!("dark {item}"))))

I actually prefer the second version, because it decouples the "alternating function" from the iterator. (I had cases where I needed exactly such an "alternating function" in a non-iterator context.)

And I actually think one would be able to write such a make_alternating_fn.

joshka commented 2 months ago

This actually does work:

let alternate_styles = [|x| format!("dark {x}"), |x| format!("light {x}")];
let mapped = vec!["a", "b", "c"]
    .iter()
    .zip(alternate_styles.iter().cycle())
    .map(|(x, f)| f(*x))
    .collect::<Vec<_>>();

Which I think is concise enough for my use to not have to create an Itertools feature. Thanks for the pointer in the right direction.

Also, often the thing I'm alternating between is just some color / style, so the need for this to be closure based was probably unnecessary in many cases. Alternating like the following seems nice enough:

        let row_colors = [SLATE.c950, SLATE.c900];
        let rows = lines
            .map(|(key, line)| Row::new([key.name(), line]))
            .zip(row_colors.iter().cycle())
            .map(|(row, style)| row.bg(*style))
            .collect_vec();