В преддверии выхода Rust 1.75.0, наполненным async trait-ами и return-position impl Trait in trait, надо разобраться, что такое impl Trait и с чем его едят.

После прочтения статьи вы сможете битбоксить с помощью новых акронимов понимать, что за наборы символов RPIT, RPITIT и т.д. используют в Rust сообществе.

Статья основывается на видео от Jon Gjengset

Содержание

fn() → impl Trait

Feature: Return Position Impl Trait (RPIT)

Мотивация

Есть такой код:

fn only_true<I>(iter: I) -> /* ??? */ where I: Iterator<Item = bool> {
	iter.map(|x| foo(x)).filter(|&x| x)
}

Какой тип возвращает наша функция? Напишем полный тип std::iter::Filter<std::iter::Map<I, ???>, ???>. Что написать вместо вопросительных знаков? Ладно пропустим этот способ. Допустим мы хотим абстрагировать возвращаемый тип, чтобы метод возвращал тип, который реализует Iterator<Item = bool>. Вспоминаем, что можно сделать Box<dyn Iterator<Item=bool>>. Но тут не нужная аллокация памяти + динамическая диспетчеризация.

На помощь приходят экзистенциальные типы (Existential types). И теперь код выглядит следующим образом:

fn only_true<I>(iter: I) -> impl Iterator<Item = bool> /* Opaque type */
where I: Iterator<Item = bool> 
{
	iter.filter(|&x| x) /* Hidden type */
}

Тут появляются 2 новых термина:

Hidden Type - конкретный/настоящий тип объекта, который возвращается из функции.

Opaque type - интерфейс для работы с Hidden Type.

В итоге получаем следующие преимущества от impl Traits:

  • абстрагирование;

  • упрощение именования возвращаемого типа;

  • избавления от типов, которые нельзя наименовать;

  • избавление от аллокаций.

Особенности impl Traits

  1. Вложенность

Типы могут быть вложенными (правда возвращаемый тип выглядит громоздким):

fn only_true<I>(iter: I) -> 
  impl Future<Output=impl Iterator<Item = bool>> /* Opaque type */
where I: Iterator<Item = bool> 
{
	async move {
		iter.filter(|&x| x) /* Hidden type */
	}
}
  1. Авто-трейты

Для авто-трейтов компилятор может посмотреть Hidden Type из-за чего происходит "утечка" (leakage):

fn bar() -> impl Sized {
    ()
}

fn foo() -> impl Sized + Send + Unpin {
    bar()
}
  1. Отличие от Generic

fn f1<R: Trait>() -> R {}

fn f2() -> impl Trait {}

В f1 вызывающая сторона выбирает тип.

В f2 тело метода выбирает тип.

Возвращаясь к Return Position Impl Trait (RPIT), стоит упомянуть времена жизни. В настоящий момент нельзя абстрагироваться от времен жизни в RPIT. Пример:

// Ошибка компиляции
fn foo(t: &()) -> impl Sized {
    t
}

// Ok
fn foo<'a>(t: &'a ()) -> impl Sized + 'a {
    t
}

Также возникает проблема при использовании дженерик типов:

// Ошибка компиляции
fn bar<T>(t: T) -> impl Sized {
    ()
}

fn foo() -> impl Sized + 'static {
    let s = String::new();
    
    bar(&s)
}

В настоящий момент для 2021 редакции разработчики предлагают использовать такой трюк:

trait Captures<U> {}
impl<T: ?Sized, U> Captures<U> for T {}

// Для одного лайфтайма
fn foo<'a>(t: &'a ()) -> impl Sized + Captures<&'a ()> { t }

// Для нескольких
fn foo<'a, 'b>(x: &'a (), y: &'b ()) -> impl Sized + Captures<(&'a (), &'b ())> {
    (x, y)
}

К счастью, данную проблему пофиксили и можно будет абстрагировать от времен жизни в 2024 редакции.

Почитать про времена жизни в impl Traits можно здесь.

fn(impl Trait)

Feature: Argument Position Impl Trait (APIT)

Самый простой случай, который является полу-сахаром для <T: Trait>(t: T):

fn f1<D>(display: D) where D: std::fmt::Display { /* … */ }

fn f2(display: impl std::fmt::Display) { /* … */ }

Отличия только при вызове, нельзя выбирать дженерик типы:

f1::<u32>(1); // Ok
f2(1) // Ok
f2::<u32>(1); // Ошибка

trait { type = impl Trait }

Feature: Assoc. Type Position Impl Trait (ATPIT) (Пока что в nightly)

Мотивация такая же как и у RPIT. Пример:

struct Odd;
impl IntoIterator for Odd {
    type IntoIter = impl Iterator<Item = u32>;
    fn into_iter(self) -> Self::IntoIter {
        (0u32..).filter(|x| x % 2 != 0)
    } 
}

Используются новые правила для захвата времен жизни и дженерик типов, поэтому всё захватывается автоматически:

impl<'a, T> Trait1 for Type {
    type Assoc<'b, U> = impl Trait2; // Ok
}

type = impl Trait

Feature: Type Alias Impl Trait (TAIT) (Пока что в nightly)

Позволяет использовать псевдоним impl Trait-а в различных местах, кроме структур. Пример:

type Ready<T> = impl std::future::Future<Output = T>;

fn ready<T>(t: T) -> Ready<T> {
    async move { t }
}

Времена жизни также захватываются автоматически.

trait { fn() → impl Trait }

Feature: Return position impl Trait in Trait (RPITIT)

То, что появится в версии 1.75.0. С помощью ATPIT мы можем определить возвращаемый тип для каждой функции, которая возвращает impl Trait, но это не практично, поэтому появилась эта фича. С помощью этой фичи как раз и возможны асинхронные функции в трейтах, так как async fn () -> ret равносильна fn () -> impl Future<Output = ret>. Пример:

impl MyAsyncTrait for MyStruct {
    fn foo(&mut self) -> impl Future<Output = u32> {
        async { 1 }
    } 
}

Но возникает потребность добавлять ограничения на возвращаемые типы.

trait It {
    fn iter(&self) -> impl Iterator<Item = u32>;
}

fn twice<I: It>(i: I) where ???: Clone
{
    let it = i.iter();
    it.clone().chain(it);
}

Одним из решений является Return Type Notation (в разработке). Примерный код выглядит так:

fn twice<I: It<iter(): Clone>>(i: I) {
    let it = i.iter();
    it.clone().chain(it);
}

Времена жизни захватываются автоматически.

Заключение

На самом деле impl Trait полезная вещь для абстрагирования и для уменьшения оверхеда в некоторых случаях. Асинхронные функции и RPITIT разрабатывались долго, но они появились, а значит в будущем появятся стабильные ATPIT и TAIT.

Ссылки

Комментарии (6)


  1. itmind
    26.12.2023 22:48

    Для авто-трейтов компилятор может посмотреть Hidden Type из-за чего происходит "утечка" (leakage)

    "утечка" памяти? Почему она происходит в данном примере?


    1. mayorovp
      26.12.2023 22:48
      +4

      Не памяти, а информации о типе.

      Хотя в заголовке функции bar() заявлено только то, что тип является Sized, компилятор при компиляции foo() знает, что он является ещё и Send + Unpin.

      Что плохо, поскольку чревато потерей обратной совместимости в библиотеках.


  1. orekh
    26.12.2023 22:48

    Ради async в Rust'е столько всего сломано, что страшно от того чем становится язык.


    1. Weselton
      26.12.2023 22:48
      +4

      Например, что поломали?


      1. orekh
        26.12.2023 22:48
        +2

        Моё понимание как язык работает поломали :)


        1. Dominux
          26.12.2023 22:48
          +1

          Тут едва ли что-то связано конкретно с async