Динамическая ссылка на типаж с дженериком без указания дженерика в Rust

Возможно ли иметь переменную, содержащую что-то, соответствующее признаку с универсальным признаком, без необходимости указания универсального типа?

Общей чертой интерфейсов и полиморфизма в других языках, таких как Java и JavaScript, является возможность обрабатывать экземпляр только на основе реализуемого им интерфейса. В следующем коде я создал признак Site и фабрику make_site для создания экземпляра, реализующего признак Site. С чертой Site связан общий шаблон, но код, который обрабатывает Site, может обрабатывать E непрозрачно — ему просто нужно отслеживать его и передавать обратно другим функциям того же экземпляра Site.

pub struct CrawlResult<E> {
    pub discovered_queue_entries: Vec<E>,
    pub result: Option<String>,
}

pub trait Site<E> {
    fn start_queue(&self) -> Vec<E>;
    fn crawl(&self, queue_entry: &E) -> CrawlResult<E>;
}

fn make_site<E>() -> Box<dyn Site<E>> {
    if rand::random() {
        Box::new(SiteA { add: 100 })
    } else {
        Box::new(SiteB { sub: 1 })
    }
}

fn main() {
    let site = make_site();

    process(&site);
}

Я опустил часть вспомогательного кода, чтобы попытаться выделить части, которые кажутся наиболее важными, но есть полностью воспроизводимый пример на игровой площадке Rust.

Приведенный выше код не может скомпилироваться в функции make_site.

error[E0277]: the trait bound `SiteA: Site<E>` is not satisfied
   --> src/main.rs:103:9
    |
103 |         Box::new(SiteA { add: 100 })
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Site<E>` is not implemented for `SiteA`
    |
    = note: required for the cast from `Box<SiteA>` to `Box<(dyn Site<E> + 'static)>`
help: consider introducing a `where` clause, but there might be an alternative better way to express this requirement
    |
101 | fn make_site<E>() -> Box<dyn Site<E>> where SiteA: Site<E> {
    |                                       ++++++++++++++++++++

error[E0277]: the trait bound `SiteB: Site<E>` is not satisfied
   --> src/main.rs:105:9
    |
105 |         Box::new(SiteB { sub: 1 })
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Site<E>` is not implemented for `SiteB`
    |
    = note: required for the cast from `Box<SiteB>` to `Box<(dyn Site<E> + 'static)>`
help: consider introducing a `where` clause, but there might be an alternative better way to express this requirement
    |
101 | fn make_site<E>() -> Box<dyn Site<E>> where SiteB: Site<E> {
    |                                       ++++++++++++++++++++

error[E0277]: the trait bound `Box<dyn Site<_>>: Site<_>` is not satisfied
   --> src/main.rs:112:13
    |
112 |     process(&site);
    |             ^^^^^ the trait `Site<_>` is not implemented for `Box<dyn Site<_>>`
    |
    = help: the following other types implement trait `Site<E>`:
              <SiteA as Site<SiteAEntry>>
              <SiteB as Site<SiteBEntry>>
    = note: required for the cast from `&Box<dyn Site<_>>` to `&dyn Site<_>`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` (bin "playground") due to 3 previous errors

Этот код предназначен для поддержки веб-сканера, и идея состоит в том, что у каждого сайта будет своя стратегия сканирования. E обычно представляет собой тип перечисления, который описывает запись в очереди сканирования и может отличаться в зависимости от сканируемого сайта. Кажется, что большинство проблем возникает из-за универсального типа записи сканирования, но было бы просто невозможно определить реализацию Site, если бы у нас не было этого универсального типа.

Можно ли перевести эти устоявшиеся шаблоны на Rust или их просто невозможно реализовать в Rust?

Даже в Java нельзя считать, что первый интерфейс с объявленной функцией, возвращающей список целых чисел, и второй интерфейс с той же объявленной функцией, но возвращающий список строк, эквивалентны. Если бы это было так, что можно было бы ожидать при вызове такой функции? целые числа или строки?

prog-fh 24.03.2024 10:19

Это правда, но реализовать этот шаблон в Java также может быть невозможно. В последнее время мой опыт больше связан с динамическими языками, где такой контракт, как «этот универсальный код будет согласован при работе с этим API», гарантируется программистом, а не компилятором. Нечто подобное было бы тривиально реализовать на JavaScript или Python.

Kevin Johnson 24.03.2024 18:53
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
0
2
54
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Возможно ли иметь переменную, содержащую что-то, соответствующее признаку с универсальным признаком, без необходимости указания универсального типа?

Только если тип реализует какую-то особенность, которую вы можете указать, и все ветки используют один и тот же тип. В вашем случае ни то, ни другое не соответствует действительности.

fn make_site<E>() -> Box<dyn Site<E>>

Эта подпись позволяет вызывающему абоненту решить, что такое E. Что будет, если позвонят make_site::<String>()? Вам нужен способ, чтобы сама функция могла сказать, что такое E. Вы можете сделать это без указания внутреннего типа, вернув Box<dyn Site<impl SiteEntry>> и убедившись, что универсальный тип реализует SiteEntry. Вы также можете удалить общий тип E из функции.

Обратите внимание, что это ограничит то, что вызывающая сторона может делать с типом, только тем, что предоставляет SiteEntry. Давайте попробуем это с пустым признаком, чтобы посмотреть, работает ли это:

trait SiteEntry {}

impl SiteEntry for SiteAEntry {}
impl SiteEntry for SiteBEntry {}

fn make_site() -> Box<dyn Site<impl SiteEntry>> {
    // ...

Мы обменяли одну ошибку на другую. Теперь вместо этого мы получаем следующее:

error[E0277]: the trait bound `SiteB: Site<SiteAEntry>` is not satisfied
   --> src/main.rs:109:9
    |
109 |         Box::new(SiteB { sub: 1 })
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Site<SiteAEntry>` is not implemented for `SiteB`
    |
    = help: the trait `Site<SiteBEntry>` is implemented for `SiteB`
    = help: for that trait implementation, expected `SiteBEntry`, found `SiteAEntry`
    = note: required for the cast from `Box<SiteB>` to `Box<(dyn Site<SiteAEntry> + 'static)>`

Теперь мы сталкиваемся со вторым ограничением, о котором я упоминал выше: все ветки должны возвращать один и тот же тип. Здесь у нас есть первая ветвь, возвращающая Box<dyn Site<SiteAEntry>>, и это совершенно нормально, но затем компилятор делает вывод, что другая ветвь также должна вернуть Box<dyn Site<SiteAEntry>>, и справедливо жалуется, что SiteB не реализует Site<SiteAEntry>, поэтому приведение невозможно.

Ваши возможности здесь несколько ограничены. Одним из вариантов было бы создать версию Site со стертым типом, которая не имеет универсального аргумента и работает исключительно с объектами типажей:

trait ErasedSite {
    fn start_queue(&self) -> Vec<Box<dyn SiteEntry>>;
    fn crawl(&self, queue_entry: &dyn SiteEntry) -> CrawlResult<Box<dyn SiteEntry>>;
}

Тогда ваша функция make_site сможет вернуть Box<dyn ErasedSite> (при условии, что вы создадите соответствующий новый тип с реализацией ErasedSite).

Этот подход потребует SiteEntry реализации метода, позволяющего определить тип вашей записи, а также, возможно, других методов.

То, что вы ищете, возможно в Rust, но вполне вероятно, что этот конкретный подход на самом деле может оказаться не тем, что вам нужно. Например, Site, вероятно, не должен быть универсальным, а вместо этого должен позволять реализации определять тип записи, используя связанные типы:

trait Site {
    type Entry;

    fn start_queue(&self) -> Vec<Self::Entry>;
    fn crawl(&self, queue_entry: &Self::Entry) -> CrawlResult<Self::Entry>;
}

Обычно это предпочтительнее, если только вы не предполагаете, что один тип реализует Site<E> несколько раз с разными типами для E.

Обратите внимание, что это в конечном итоге не решает проблему, с которой вы столкнулись, а просто перемещает ее - когда вы говорите dyn SomeTrait, вам необходимо включить связанные типы SomeTrait, поэтому вам нужно будет сказать Box<dyn Site<Entry = impl SiteEntry>>, и тогда вы вернетесь туда, где вы начал.

В конечном итоге у вас действительно есть два варианта:

  • Введите сотрите все и поместите все возможности, которые должен иметь тип записи сайта, за такую ​​черту, как SiteEntry. (Это динамическая диспетчеризация во время выполнения.)
  • Используйте перечисление для хранения различных типов сайтов. Это может сработать, а может и не сработать, в зависимости от того, что именно вы пытаетесь сделать. (Это динамическая диспетчеризация во время компиляции.)

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

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