Толчок к стабилизации GAT

Перевод | Автор оригинала: Jack Huey

Толчок к стабилизации GAT

С чего начать, с чего начать ...

Начнем с того, что скажем: это очень интересный пост. Некоторые люди, читающие это, будут чрезвычайно взволнованы; некоторые не будут знать, что такое GAT (общие связанные типы); другие могут быть в недоумении. RFC для этой функции был открыт в апреле 2016 года (и объединился примерно через полтора года). Фактически, этот RFC даже предшествует дженерикам const (MVP которого недавно был стабилизирован). Не позволяйте этому обмануть вас: это мощная функция; и реакция на проблему отслеживания на Github, возможно, должна дать вам представление о ее популярности (это самая популярная проблема в репозитории Rust): реакции GAT

Если вы не знакомы с GAT, они позволяют вам определять обобщенные типы типа, времени жизни или константы для связанных типов. Вот так:

trait Foo {
    type Bar<'a>;
}

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

Но пока: что именно происходит? Что ж, спустя почти четыре года после того, как его RFC был объединен, функция generic_associated_types больше не является «неполной».

crickets chirping

Подожди ... вот и все ?? Ну да! Позже в этом сообщении в блоге я расскажу немного подробнее о том, почему это так важно. Но, короче говоря, в компилятор пришлось внести много изменений, чтобы заставить GAT работать. И хотя остается еще несколько небольших проблем с диагностикой, эта функция наконец-то находится в той области, в которой мы чувствуем себя комфортно, делая ее более не «неполной».

Так что это значит? Что ж, все это на самом деле означает, что когда вы используете эту функцию по ночам, вы больше не будете получать предупреждение «generic_associated_types is incomplete». Однако настоящая причина этого важна: мы хотим стабилизировать эту функцию. Но нам нужна ваша помощь. Нам нужно, чтобы вы протестировали эту функцию, сообщали о проблемах, связанных с обнаруженными вами ошибками, или о возможных улучшениях диагностики. Кроме того, мы хотели бы, чтобы вы просто рассказали нам о некоторых интересных паттернах, которые GAT поддерживает в Zulip!

Не давая обещаний, которые мы не уверены на 100%, мы очень надеемся, что сможем стабилизировать эту функцию в течение следующих нескольких месяцев. Но мы хотим убедиться, что не упускаем явно очевидных ошибок или недостатков. Мы хотим, чтобы стабилизация была плавной.

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

Так что же такое GAT?

__ Примечание: это будет лишь краткий обзор. RFC содержит гораздо больше деталей .__

GAT (общие связанные типы) были первоначально предложены в RFC 1598. Как было сказано ранее, они позволяют вам определять общие типы, время жизни или константы для связанных типов. Если вы знакомы с языками, в которых есть «типы более высокого порядка», тогда вы можете вызывать конструкторы типов GAT для свойств. Возможно, самый простой способ понять, как можно использовать GAT, - это перейти к примеру.

Вот популярный вариант использования: LendingIterator (ранее известный как StreamingIterator):

trait LendingIterator {
    type Item<'a> where Self: 'a;

    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

Давайте рассмотрим одну реализацию этого, гипотетический <[T]>::windows_mut, который позволяет выполнять итерацию через перекрывающиеся изменяемые окна на срезе. Если бы вы сегодня попытались реализовать это с помощью Iterator, например

struct WindowsMut<'t, T> {
    slice: &'t mut [T],
    start: usize,
    window_size: usize,
}

impl<'t, T> Iterator for WindowsMut<'t, T> {
    type Item = &'t mut [T];

    fn next<'a>(&'a mut self) -> Option<Self::Item> {
        let retval = self.slice[self.start..].get_mut(..self.window_size)?;
        self.start += 1;
        Some(retval)
    }
}

тогда вы получите ошибку.

error[E0495]: cannot infer an appropriate lifetime for lifetime parameter in function call due to conflicting requirements
  --> src/lib.rs:9:22
   |
9  |         let retval = self.slice[self.start..].get_mut(..self.window_size)?;
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^
   |
note: first, the lifetime cannot outlive the lifetime `'a` as defined on the method body at 8:13...
  --> src/lib.rs:8:13
   |
8  |     fn next<'a>(&'a mut self) -> Option<Self::Item> {
   |             ^^
note: ...so that reference does not outlive borrowed content
  --> src/lib.rs:9:22
   |
9  |         let retval = self.slice[self.start..].get_mut(..self.window_size)?;
   |                      ^^^^^^^^^^
note: but, the lifetime must be valid for the lifetime `'t` as defined on the impl at 6:6...
  --> src/lib.rs:6:6
   |
6  | impl<'t, T: 't> Iterator for WindowsMut<'t, T> {
   |      ^^

Короче говоря, эта ошибка, по сути, говорит нам, что для того, чтобы мы могли вернуть ссылку на self.slice, он должен существовать до тех пор, пока 'a, что потребует привязки' a: 't (что мы можем' ') т предоставить). Без этого мы могли бы вызвать next, уже удерживая ссылку на срез, создавая перекрывающиеся изменяемые ссылки. Однако он отлично компилируется, если вы реализовали это с помощью трейта LendingIterator из предыдущего:

impl<'t, T> LendingIterator for WindowsMut<'t, T> {
    type Item<'a> where Self: 'a = &'a mut [T];

    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>> {
        let retval = self.slice[self.start..].get_mut(..self.window_size)?;
        self.start += 1;
        Some(retval)
    }
}

В стороне, есть одна вещь, которую следует отметить по поводу этой трэйты и подразумевает, что вам может быть интересно узнать: пункт where Self: 'в Item. Вкратце, это позволяет нам использовать &'a mut [T]; без этого предложения where кто-то может попытаться вернуть Self::Item <'static> и продлить время жизни среза. Мы понимаем, что это иногда вызывает путаницу, и рассматриваем потенциальные альтернативы, например, всегда предполаGATь эту границу или подразумевать ее на основе использования в трейте (см. Этот вопрос). Мы определенно хотели бы услышать здесь о ваших вариантах использования, особенно если предположить, что это ограничение будет помехой.

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

trait PointerFamily {
    type Pointer<T>: Deref<Target = T>;

    fn new<T>(value: T) -> Self::Pointer<T>;
}

struct ArcFamily;
struct RcFamily;

impl PointerFamily for ArcFamily {
    type Pointer<T> = Arc<T>;
    ...
}
impl PointerFamily for RcFamily {
    type Pointer<T> = Rc<T>;
    ...
}

struct MyStruct<P: PointerFamily> {
    pointer: P::Pointer<String>,
}

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

Эти два примера лишь поверхностно относятся к шаблонам, поддерживаемым GAT. Если вы найдете что-то особенно интересное или умное, мы будем рады услышать о них на Zulip!

Почему на это потребовалось так много времени?

Так что же заставило нас потратить почти четыре года, чтобы достичь того, чем мы являемся сейчас? Что ж, трудно выразить словами, насколько существующий решатель трэйтов пришлось изменить и адаптировать; но подумайте вот о чем: какое-то время считалось, что для поддержки GAT нам придется перевести rustc на использование Chalk, потенциального будущего решателя трэйтов, который использует логические предикаты для решения целей трэйтов (хотя, хотя некоторый прогресс был достигнут, это все еще очень экспериментально даже сейчас).

Для справки, вот несколько различных дополнений и изменений в реализации, которые так или иначе способствовали поддержке GAT:

И чтобы еще больше подчеркнуть работу, приведенную выше: многие из этих PR являются крупными и требуют значительных проектных работ. Есть также несколько более мелких PR. Но мы сделали это. И я просто хочу поздравить всех, кто так или иначе приложил усилия. Ты жжешь.

Какие ограничения существуют в настоящее время?

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

fn takes_iter(_: &mut dyn for<'a> LendingIterator<Item<'a> = &'a i32>) {}

Самая большая причина для этого решения заключается в том, что еще предстоит немного поработать над дизайном и реализацией, чтобы сделать это действительно пригодным для использования. И хотя это хорошая функция, добавление ее в будущем будет обратно совместимым изменением. Мы считаем, что лучше стабилизировать большинство GAT, а затем вернуться и попытаться решить эту проблему позже, чем блокировать GAT еще дольше. Кроме того, GAT без обеспечения безопасности объектов по-прежнему очень эффективны, поэтому мы мало что теряем, откладывая это.

Как упоминалось ранее в этом посте, остается еще пара проблем с диагностикой. Если вы все же найдете ошибки, пожалуйста, сообщите о проблемах!