В преддверии выхода 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
Вложенность
Типы могут быть вложенными (правда возвращаемый тип выглядит громоздким):
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 */ } }
Авто-трейты
Для авто-трейтов компилятор может посмотреть Hidden Type из-за чего происходит "утечка" (leakage):
fn bar() -> impl Sized { () } fn foo() -> impl Sized + Send + Unpin { bar() }
Отличие от 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.
