Copartition two BlockPartitions objects.

        Args:
            axis: The axis to copartition.
            other: The other BlockPartitions object to copartition with.
            left_func: The function to apply to left. If None, just use the dimension
                of self (based on axis).
            right_func: The function to apply to right. If None, check the dimensions of
                other and use the identity function if splitting needs to happen.

        Returns:
            A tuple of BlockPartitions objects, left and right.

def copartition_datasets(self, axis, other, left_func, right_func):
        """Copartition two BlockPartitions objects.

        Args:
            axis: The axis to copartition.
            other: The other BlockPartitions object to copartition with.
            left_func: The function to apply to left. If None, just use the dimension
                of self (based on axis).
            right_func: The function to apply to right. If None, check the dimensions of
                other and use the identity function if splitting needs to happen.

        Returns:
            A tuple of BlockPartitions objects, left and right.
        """
        if left_func is None:
            new_self = self
        else:
            new_self = self.map_across_full_axis(axis, left_func)

        # This block of code will only shuffle if absolutely necessary. If we do need to
        # shuffle, we use the identity function and then reshuffle.
        if right_func is None:
            if axis == 0 and not np.array_equal(
                other.block_lengths, new_self.block_lengths
            ):
                new_other = other.manual_shuffle(
                    axis, lambda x: x, new_self.block_lengths
                )
            elif axis == 1 and not np.array_equal(
                other.block_widths, new_self.block_widths
            ):
                new_other = other.manual_shuffle(
                    axis, lambda x: x, new_self.block_widths
                )
            else:
                new_other = other
        # Most of the time, we will be given an operation to do. We perform that with
        # manual_shuffle.
        else:
            new_other = other.manual_shuffle(
                axis,
                right_func,
                new_self.block_lengths if axis == 0 else new_self.block_widths,
            )
        return new_self, new_other