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

d9031cd7bd2c03d22154e16c81021071.png

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

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

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

fn only_true(iter: I) -> 
  impl Future> /* Opaque type */
where I: Iterator 
{
	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 {}

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)

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

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;

fn ready(t: T) -> Ready {
    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. Пример:

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

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

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.

Ссылки

© Habrahabr.ru