Как «пометить» функциональный признак для параллельного выполнения?

Контекст

У меня есть предикатная черта. Он принимает некоторый тип T и возвращает для него логическое значение.

trait Predicate<T> {
  fn evaluate(&self, t: &T) -> bool;
}

У меня также есть оценщик, который оценивает один и тот же предикат для каждого заданного T.

trait Evaluator<T> {
  fn evaluate(&self, is: &[T]) -> Vec<bool>;
}

Evaluator реализован для Predicate. Он оценивает Predicate для каждого T.

impl<T, P: Predicate<T>> Evaluator<T> for P {
  fn evaluate(&self, is: &[T]) -> Vec<bool> {
    is.iter().map(|i| self.evaluate(i)).collect()
  }
}

У меня также реализован Predicate<T> для замыканий типа Fn(&T) -> bool, что позволяет мне предоставлять такое замыкание всякий раз, когда я хочу Evaluator, вместо того, чтобы создавать структуру и реализовывать для нее признак Evaluator каждый раз, когда я хочу применить какую-то другую логику.

Теперь я могу создать замыкание и передать его всему, что требует Evaluator.

  let p = |i: &i32| *i > 0;
  wants_evaluator(&p);

Проблема

До сих пор этот API был довольно кратким, но недавно у меня возникла необходимость параллельно оценивать предикаты для каждого решения. Сама логика не является проблемой для вискозы — par_iter() делает именно то, что мне нужно. Проблема в том, что я не хочу терять удобное автоматическое преобразование замыкания в оценщик, поскольку я во многом от него завишу. В идеале я хочу добиться чего-то вроде этого:

  let p = |i: &i32| *i > 0;
  let p_par = p.into_par(); // will be evaluated in parallel
  wants_evaluator(&p_par);

Параллельная оценка требуется не так часто, потому что для большинства простых предикатов распараллеливание только снижает производительность, однако когда оно мне нужно, оно мне нужно.

Неудачные решения

Я пытался использовать структуру-обертку ParPredicate

struct ParPredicate<T, P: Predicate<T>>(P, PhantomData<T>);

impl<T, P> Evaluator<T> for ParPredicate<T, P>
where
  T: Sync,
  P: Predicate<T> + Sync,
{
  fn evaluate(&self, is: &[T]) -> Vec<bool> {
    is.par_iter().map(|i| self.0.evaluate(i)).collect()
  }
}

Однако Rust жалуется, что эта реализация конфликтует с impl<T, P: Predicate<T>> Evaluator<T> for P { ...

conflicting implementations of trait `Evaluator<_>` for type `ParPredicate<_, _>`
downstream crates may implement trait `Predicate<_>` for type `ParPredicate<_, _>`

Если я использую какой-то конкретный тип вместо T в Predicate, то эта ошибка не появляется. Это также портит весь смысл Predicate, поскольку он должен быть общим.

Я также попытался использовать какой-то шаблон состояния типа с Predicate:

trait ExecutionStrategy {}

enum Sequential {}
impl ExecutionStrategy for Sequential {}

enum Parallel {}
impl ExecutionStrategy for Parallel {}

trait Predicate<ExecutionStrategy, T> {
  fn evaluate(&self, i: &T) -> bool;
}

trait Evaluator<ExecutionStrategy, T> {
  fn evaluate(&self, is: &[T]) -> Vec<bool>;
}

impl<T, P> Evaluator<Sequential, T> for P
where
  P: Predicate<Sequential, T>,
{
  fn evaluate(&self, is: &[T]) -> Vec<bool> {
    is.iter().map(|i| self.evaluate(i)).collect()
  }
}

impl<T, P> Evaluator<Parallel, T> for P
where
  T: Sync,
  P: Predicate<Parallel, T> + Sync,
{
  fn evaluate(&self, is: &[T]) -> Vec<bool> {
    is.par_iter().map(|i| self.evaluate(i)).collect()
  }
}

Проблема здесь в том, что мне нужно каким-то образом преобразовать Predicate<Sequential, T> в Predicate<Parallel, T>, и я понятия не имею, как это сделать, хотя по сути у них один и тот же контакт.

trait IntoPar<T> {
  fn into_par(self) -> impl Predicate<Parallel, T>;
}

impl<T, P: Predicate<Sequential, T>> IntoPar<T> for P {
  fn into_par(self) -> impl Predicate<Parallel, T> {
    // now what?
  }
}

Все, что я хочу, это прикрепить к моему Predicate какой-то маркер, который позволил бы Evaluator реализовать другую логику для Predicate на основе его маркера. Вся эта информация собирается во время компиляции, и я не понимаю, почему я теоретически не мог этого добиться. Но как мне это сделать, сохранив плавное преобразование замыкания в Evaluator?

Я всего лишь хочу изучить ограничения системы типов Rust. Этот код будет использоваться только в моем личном проекте, а не в производстве.

Если у вас есть решение, отличное от существующих ответов, опубликуйте сообщение с ответом. Решения не принадлежат вопросу.

cafce25 30.08.2024 23:02
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
2
1
61
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы на правильном пути. Чтобы решить проблему преобразования в Parallel, вам нужно избавиться от ExecutionStrategy в Predicate (но оставить его в Evaluator):

trait Predicate<T> {
    fn evaluate(&self, i: &T) -> bool;
}

trait Evaluator<ExecutionStrategy, T> {
    fn evaluate(&self, is: &[T]) -> Vec<bool>;
}

impl<T, P> Evaluator<Sequential, T> for P
where
    P: Predicate<T>,
{
    fn evaluate(&self, is: &[T]) -> Vec<bool> {
        is.iter().map(|i| self.evaluate(i)).collect()
    }
}

impl<T, P> Evaluator<Parallel, T> for P
where
    T: Sync,
    P: Predicate<T> + Sync,
{
    fn evaluate(&self, is: &[T]) -> Vec<bool> {
        is.par_iter().map(|i| self.evaluate(i)).collect()
    }
}

fn wants_evaluator<S, E: Evaluator<S, i32>>(evaluator: E) { ... }

Однако это будет иметь нежелательные последствия: все существующие вызовы wants_evaluator() перестанут работать с «необходимой аннотацией типа».

Это можно решить с помощью типа маркера:

enum Sequential {}
enum Parallel {}

trait Predicate<T> {
    fn evaluate(&self, i: &T) -> bool;
}

trait Evaluator<ExecutionStrategy, T> {
    fn evaluate(&self, is: &[T]) -> Vec<bool>;
}

pub struct ParallelPredicate<T, P>(P, PhantomData<T>);

impl<T, P> Evaluator<Parallel, T> for ParallelPredicate<T, P>
where
    T: Sync,
    P: Predicate<T> + Sync,
{
    fn evaluate(&self, is: &[T]) -> Vec<bool> {
        is.par_iter().map(|i| self.0.evaluate(i)).collect()
    }
}

fn into_par<T, P: Predicate<T>>(predicate: P) -> ParallelPredicate<T, P> {
    ParallelPredicate(predicate, PhantomData)
}

impl<T, P> Evaluator<Sequential, T> for P
where
    P: Predicate<T>,
{
    fn evaluate(&self, is: &[T]) -> Vec<bool> {
        is.iter().map(|i| self.evaluate(i)).collect()
    }
}

fn wants_evaluator<S, E: Evaluator<S, i32>>(evaluator: E) {
    evaluator.evaluate(&[1, 2, 3]);
}

fn main() {
    wants_evaluator(|v: &i32| true);
    wants_evaluator(into_par(|v: &i32| true));
}

Это действительно работает. Не могли бы вы объяснить, почему ваше решение работает, а мое нет? Как это вообще работает? Например, что такое ExecutionStrategy в этом контексте? Это какой-то шаблон типа состояния типа для структур?

Sun of A beach 30.08.2024 16:56

Другие вопросы по теме