Closure carnage
⚓ Rust 📅 2025-12-31 👤 surdeus 👁️ 4Introduction
I know to not do with a macro something functions can.
My previous declarative macro didn't use any of the declarative macro special features so I decided to make it into a function.
impl MyApp {
/// Philosophy: shrink if possible - expand otherwise
fn selected_cell_range_arrow_key<MainAxis: Copy + PartialOrd<MainAxis>, OtherAxis>(
&mut self, selection_state: (Column, Row, &ColumnRangeInclusive, &RowRangeInclusive),
input: (&InputState, bool, Key), end_action: impl Fn(MainAxis) -> MainAxis, limit: MainAxis,
switch_axis: (
impl Fn(Column, Row) -> (MainAxis, OtherAxis),
impl for<'a> Fn(&'a ColumnRangeInclusive, &'a RowRangeInclusive) -> (&'a RangeInclusive<MainAxis>, &'a RangeInclusive<OtherAxis>),
impl for<'a> Fn(&'a RangeInclusive<MainAxis>, &'a RangeInclusive<OtherAxis>) -> (&'a ColumnRangeInclusive, &'a RowRangeInclusive),
impl Fn(Option<MainAxis>, Option<OtherAxis>) -> (Option<Column>, Option<Row>),
impl Fn(MainAxis, OtherAxis) -> (Column, Row),
),
switch_end: (
impl Fn(MainAxis, MainAxis) -> (MainAxis, MainAxis),
impl Fn((MainAxis, MainAxis), (MainAxis, MainAxis)) -> ((MainAxis, MainAxis), (MainAxis, MainAxis)),
),
) -> (Option<Column>, Option<Row>) {
let (column, row, column_range, row_range) = selection_state;
let (i, shift, arrow) = input;
let (
switch_axis_a, switch_axis_b,
switch_axis_c, switch_axis_d,
switch_axis_e,
) = switch_axis;
let (switch_end_a, switch_end_b) = switch_end;
// The axis refers to wether it's top-bottom or left-right.
// The end refers to wether it's positive(right-bottom) or negative(left-top).
if i.key_pressed(arrow) {
let (main_axis, _) = switch_axis_a(column, row);
let (no_shift_left, no_shift_right) = switch_end_a(main_axis, limit);
if shift {
let (main_axis_range, other_axis_range) = switch_axis_b(column_range, row_range);
// This is a pattern to decide wich end is mutated from a closure that could switch or not.
let (mut main_end, mut other_end) = switch_end_a(*main_axis_range.start(), *main_axis_range.end());
let ((shrink_left, expand_left), (shrink_right, expand_right)) =
switch_end_b((main_end, other_end), (main_axis, limit));
// The main end is the one that gets changed when shrinking
let changed_end = if shrink_left < shrink_right {
main_end = end_action(main_end);
main_end
// The other end is the one that gets changed when expanding
} else if expand_left < expand_right {
other_end = end_action(other_end);
other_end
} else {
// No column and no row has been changed
return (None, None)
};
// Now that the correct end has been mutated, the original order can be restored with another switch.
let (start, end) = switch_end_a(main_end, other_end);
let main_axis_range = range!(start,,;end);
let (column_range, row_range) = switch_axis_c(&main_axis_range, other_axis_range);
self.set_selection_range(column, row, column_range.clone(), row_range.clone());
return switch_axis_d(Some(changed_end), None)
} else if no_shift_left < no_shift_right {
// Same pattern
let (main_axis, other_axis) = switch_axis_a(column, row);
let main_axis = end_action(main_axis);
let (column, row) = switch_axis_e(main_axis, other_axis);
self.select_cell(column, row);
return (Some(column), Some(row))
}
}
(None, None)
}
}
Problem
As you could probably tell, what was originally only one expresssion in the macro has turned into many closures with the same job but different types in the function. I had to resort to another declarative macro to keep the code dry.
impl MyApp {
fn selected_cell_range_keyboard_navigation_got_input(
&mut self,
ctx: &Context,
cell: (Column, Row),
cell_range: (ColumnRangeInclusive, RowRangeInclusive),
total_cells: (Column, Row),
) -> (Option<Column>, Option<Row>) {
let (column, row) = cell;
let (column_range, row_range) = cell_range;
let (total_columns, total_rows) = total_cells;
ctx.input(|i| {
let shift = i.modifiers.shift;
macro_rules! selected_cell_range_arrow_key {
(
arrow = $arrow:ident, end_action = $end_action:expr, limit = $limit:expr;
switch_axis = $switch_axis:expr, switch_end = $switch_end:expr;
) => {
match self.selected_cell_range_arrow_key(
(column, row, &column_range, &row_range), (i, shift, Key::$arrow), $end_action, $limit,
($switch_axis, $switch_axis, $switch_axis, $switch_axis, $switch_axis), ($switch_end, $switch_end),
) {
(None, None) => {}
(column, row) => return (column, row),
}
};
}
selected_cell_range_arrow_key!(
arrow = ArrowUp, end_action = Row::previous, limit = Row::first();
switch_axis = |right, left| (left, right), switch_end = |right, left| (left, right);
);
selected_cell_range_arrow_key!(
arrow = ArrowDown, end_action = Row::next, limit = total_rows.previous();
switch_axis = |right, left| (left, right), switch_end = |left, right| (left, right);
);
selected_cell_range_arrow_key!(
arrow = ArrowLeft, end_action = Column::previous, limit = Column::first();
switch_axis = |left, right| (left, right), switch_end = |right, left| (left, right);
);
selected_cell_range_arrow_key!(
arrow = ArrowRight, end_action = Column::next, limit = total_columns.previous();
switch_axis = |left, right| (left, right), switch_end = |left, right| (left, right);
);
(None, None)
})
}
}
Question
Is there another way to reduce code repetition, macro use while avoiding dyn?
Here are the rest of the definitions:
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Column(usize);
impl Column {
pub const fn new(column_index: usize) -> Self {
Self(column_index)
}
pub const fn first() -> Self {
Self(0)
}
pub const fn index(self) -> usize {
self.0
}
pub const fn previous(self) -> Self {
Self(self.0 - 1)
}
pub const fn next(self) -> Self {
Self(self.0 + 1)
}
pub const fn mid(low: Self, high: Self) -> Self {
Self(low.0 + (high.0 - low.0) / 2)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Row(usize);
impl Row {
pub const fn new(row_index: usize) -> Self {
Self(row_index)
}
pub const fn first() -> Self {
Self(0)
}
pub const fn index(self) -> usize {
self.0
}
pub const fn previous(self) -> Self {
Self(self.0 - 1)
}
pub const fn next(self) -> Self {
Self(self.0 + 1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FillDirection {
Top(Row),
Right(Column),
Bottom(Row),
Left(Column),
}
#[macro_export]
macro_rules! range {
($start:expr,,;$end:expr) => {
RangeInclusive::from($start, $end)
};
}
#[derive(Clone, Default, PartialEq, Eq, Hash)]
pub struct RangeInclusive<Idx> {
start: Idx,
end: Idx,
exhausted: bool,
}
impl<Idx> RangeInclusive<Idx> {
#[inline]
pub const fn from(start: Idx, end: Idx) -> Self {
Self { start, end, exhausted: false }
}
#[inline]
pub const fn start(&self) -> &Idx {
&self.start
}
#[inline]
pub const fn end(&self) -> &Idx {
&self.end
}
}
pub type ColumnRangeInclusive = RangeInclusive<Column>;
pub type RowRangeInclusive = RangeInclusive<Row>;
#[derive(PartialEq, Eq)]
struct SelectionRange(ColumnRangeInclusive, RowRangeInclusive, Option<FillDirection>);
#[derive(PartialEq, Eq)]
struct SelectedCell(Column, Row, Option<SelectionRange>);
#[derive(PartialEq, Eq)]
enum CellState {
SelectedCell(SelectedCell)
}
pub struct MyApp {
cell_state: CellState,
}
#[derive(Clone)]
pub struct Context(InputState);
impl Context {
#[inline]
pub fn input<R>(&self, reader: impl FnOnce(&InputState) -> R) -> R {
reader(&self.0)
}
}
#[derive(Clone, Debug)]
pub struct InputState {
pub modifiers: Modifiers,
key_pressed: Key,
}
impl InputState {
pub fn key_pressed(&self, desired_key: Key) -> bool {
self.key_pressed == desired_key
}
}
#[derive(Clone, Debug, Copy, Default, Hash, PartialEq, Eq)]
pub struct Modifiers {
pub alt: bool,
pub ctrl: bool,
pub shift: bool,
pub command: bool,
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
pub enum Key {
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
}
impl MyApp {
pub const fn set_selection_range(&mut self, column: Column, row: Row,
column_range: ColumnRangeInclusive, row_range: RowRangeInclusive) {
self.cell_state = CellState::SelectedCell(SelectedCell(column, row, Some(
SelectionRange(column_range, row_range, None)
)));
}
pub const fn select_cell(&mut self, column: Column, row: Row) {
self.cell_state = CellState::SelectedCell(SelectedCell(column, row, None));
}
}
3 posts - 2 participants
🏷️ Rust_feed