Closure carnage

Introduction

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));
    }
}
1 Like

Yes, you can use traits to vastly simplify this.

I've already used the Fn, Copy and PartialOrd traits, I don't know how to push it further to reduce the ammount of closures.

1 Like

i think it would be best to start from what is the goal of this code, very likely that the solution is to rethink the architecture of the struct

1 Like

I was trying to write an outline for a trait based solution to get out of the 'closure carnage'. The main problem is that OP is duplicating the same logic in strange ways. It needs to be fundamentally 'rethought' and rewritten.

At one point the post I was writing became so convoluted that I thought posting it would not add anything of value to the thread, and thought someone else would take it up if they get some time.

My approach started with realizing the following, all 4 arrow keys share the same logic, differing only in:

  1. Which axis (Column vs Row) is "main"
  2. Which direction (positive vs negative) we move

We can encode these as zero-sized types and traits and let the compiler handle it.

I'm starting to think my current setup of a function for the logic and a macro for the boilerplate is the most optimal approach. Every other thing I've tried has failed. I'm gonna start to put the logic in functions and use macros to reduce the function calling repetition.