Да кто такой этот ваш 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(iter: I) -> /* ??? */ where I: Iterator- {
iter.map(|x| foo(x)).filter(|&x| x)
}
Какой тип возвращает наша функция? Напишем полный тип std::iter::Filter
. Что написать вместо вопросительных знаков? Ладно пропустим этот способ. Допустим мы хотим абстрагировать возвращаемый тип, чтобы метод возвращал тип, который реализует Iterator
. Вспоминаем, что можно сделать Box
. Но тут не нужная аллокация памяти + динамическая диспетчеризация.
На помощь приходят экзистенциальные типы (Existential types). И теперь код выглядит следующим образом:
fn only_true(iter: I) -> impl Iterator- /* Opaque type */
where I: Iterator
-
{
iter.filter(|&x| x) /* Hidden type */
}
Тут появляются 2 новых термина:
Hidden Type — конкретный/настоящий тип объекта, который возвращается из функции.
Opaque type — интерфейс для работы с Hidden Type.
В итоге получаем следующие преимущества от impl Traits:
абстрагирование;
упрощение именования возвращаемого типа;
избавления от типов, которые нельзя наименовать;
избавление от аллокаций.
Особенности impl Traits
Вложенность
Типы могут быть вложенными (правда возвращаемый тип выглядит громоздким):
fn only_true(iter: I) ->
impl Future
Авто-трейты
Для авто-трейтов компилятор может посмотреть Hidden Type из-за чего происходит «утечка» (leakage):
fn bar() -> impl Sized {
()
}
fn foo() -> impl Sized + Send + Unpin {
bar()
}
Отличие от Generic
fn f1() -> 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) -> impl Sized {
()
}
fn foo() -> impl Sized + 'static {
let s = String::new();
bar(&s)
}
В настоящий момент для 2021 редакции разработчики предлагают использовать такой трюк:
trait Captures {}
impl Captures 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)
Самый простой случай, который является полу-сахаром для
:
fn f1(display: D) where D: std::fmt::Display { /* … */ }
fn f2(display: impl std::fmt::Display) { /* … */ }
Отличия при только при вызове, нельзя выбирать дженерик типы:
f1::(1); // Ok
f2(1) // Ok
f2::(1); // Ошибка
trait { type = impl Trait }
Feature: Assoc. Type Position Impl Trait (ATPIT) (Пока что в nightly)
Мотивация такая же как и у RPIT. Пример:
struct Odd;
impl IntoIterator for Odd {
type IntoIter = impl Iterator- ;
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 = impl std::future::Future
Времена жизни также захватываются автоматически.
trait { fn () → impl Trait }
Feature: Return position impl Trait in Trait (RPITIT)
То, что появится в версии 1.75.0. С помощью ATPIT мы можем определить возвращаемый тип для каждой функции, которая возвращает impl Trait, но это не практично, поэтому появилась эта фича. С помощью этой фичи как раз и возможны асинхронные функции в трейтах, так как async fn () -> ret
равносильна fn () -> impl Future
. Пример:
impl MyAsyncTrait for MyStruct {
fn foo(&mut self) -> impl Future
Но возникает потребность добавлять ограничения на возвращаемые типы.
trait It {
fn iter(&self) -> impl Iterator- ;
}
fn twice
(i: I) where ???: Clone
{
let it = i.iter();
it.clone().chain(it);
}
Одним из решений является Return Type Notation (в разработке). Примерный код выглядит так:
fn twice>(i: I) {
let it = i.iter();
it.clone().chain(it);
}
Времена жизни захватываются автоматически.
Заключение
На самом деле impl Trait полезная вещь для абстрагирования и для уменьшения оверхеда в некоторых случаях. Асинхронные функции и RPITIT разрабатывались долго, но они появились, а значит в будущем появятся стабильные ATPIT и TAIT.