Да кто такой этот ваш impl Trait

от автора

В преддверии выхода 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.

Ссылки


ссылка на оригинал статьи https://habr.com/ru/articles/783266/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *