Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В преддверии выхода Rust 1.75.0, наполненным async trait-ами и return-position impl Trait in trait, надо разобраться, что такое impl Trait и с чем его едят.
После прочтения статьи вы сможете битбоксить с помощью новых акронимов понимать, что за наборы символов RPIT, RPITIT и т.д. используют в Rust сообществе.
Статья основывается на видео от Jon Gjengset
Содержание
fn() → impl Trait
fn(impl Trait)
trait { type = impl Trait }
type = impl Trait
trait { fn() → impl Trait }
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.
Ссылки
Opaque types
Impl Trait aka Look ma’, no generics! by Jon Gjengset
Lifetime capture rules 2024
Assoc. Type Position Impl Trait
Type Alias Impl Trait
Async fn in Traits and Trait { fn() → impl Trait }
Return Type Notation