Const generics MVP достиг бета-версии

Перевод | Автор оригинала: The const generics project group

Спустя более 3 лет с момента принятия оригинального RFC для константных дженериков первая версия константных дженериков теперь доступна на бета-канале Rust! Он будет доступен в версии 1.51, которая, как ожидается, будет выпущена 25 марта 2021 года. Дженерики Const - одна из самых долгожданных функций, которые появятся в Rust, и мы рады, что люди начнут пользоваться преимуществами возросшей мощности. языка, следующего за этим дополнением.

Даже если вы не знаете, что такое константные дженерики (в этом случае, читайте дальше!), Вы, вероятно, извлекли из них пользу: константные дженерики уже используются в стандартной библиотеке Rust для улучшения эргономики массивов и диагностики; подробнее об этом ниже.

Когда дженерики const достигли бета-версии, давайте быстро рассмотрим, что на самом деле стабилизируется, что это означает на практике и что будет дальше.

Что такое дженерики const?

Универсальные константы - это общие аргументы, которые варьируются в пределах постоянных значений, а не типов или времени жизни. Это позволяет, например, параметризовать типы целыми числами. Фактически, с самого начала разработки Rust был один пример универсальных типов const: типы массивов [T; N], для некоторых типов T и N: usize. Однако ранее не было возможности абстрагироваться от массивов произвольного размера: если вы хотели реализовать трэйту для массивов любого размера, вам пришлось бы делать это вручную для каждого возможного значения. Долгое время из-за этой проблемы даже стандартные библиотечные методы для массивов были ограничены массивами длиной не более 32. Это ограничение было окончательно снято в Rust 1.47 - изменение, которое стало возможным благодаря дженерикам const.

Вот пример типа и реализации, использующих константные дженерики: тип, заключающий в себе пару массивов одинакового размера.

struct ArrayPair<T, const N: usize> {
    left: [T; N],
    right: [T; N],
}

impl<T: Debug, const N: usize> Debug for ArrayPair<T, N> {
    // ...
}

Текущие ограничения

Первая итерация константных дженериков была намеренно ограничена: другими словами, эта версия является MVP (минимально жизнеспособным продуктом) для константных дженериков. Это решение мотивировано как дополнительной сложностью общих константных дженериков (реализация общих константных дженериков еще не завершена, но мы чувствуем, что константные дженерики в 1.51 уже очень полезны), так и желанием постепенно вводить большую функцию , чтобы получить опыт работы с любыми потенциальными недостатками и трудностями. Мы намерены отменить их в будущих версиях Rust: посмотрим, что будет дальше.

Для константных дженериков разрешены только целочисленные типы

На данный момент единственными типами, которые могут использоваться в качестве типа универсального аргумента const, являются типы целых чисел (т.е. целые числа со знаком и без знака, включая isize и usize), а также char и bool. Это охватывает основной вариант использования const, а именно абстрагирование по массивам. В будущем это ограничение будет снято, чтобы разрешить использование более сложных типов, таких как &str и типы, определяемые пользователем.

В константных аргументах нет сложных универсальных выражений

В настоящее время константные параметры могут быть созданы только константными аргументами следующих форм:

Например:

fn foo<const N: usize>() {}

fn bar<T, const M: usize>() {
    foo::<M>(); // ok: `M` is a const parameter
    foo::<2021>(); // ok: `2021` is a literal
    foo::<{20 * 100 + 20 * 10 + 1}>(); // ok: const expression contains no generic parameters
    
    foo::<{ M + 1 }>(); // error: const expression contains the generic parameter `M`
    foo::<{ std::mem::size_of::<T>() }>(); // error: const expression contains the generic parameter `T`
    
    let _: [u8; M]; // ok: `M` is a const parameter
    let _: [u8; std::mem::size_of::<T>()]; // error: const expression contains the generic parameter `T`
}

Итератор массива по значению

В дополнение к изменениям языка, описанным выше, мы также начали добавлять методы в стандартную библиотеку, использующие преимущества дженериков const. Хотя большинство из них еще не готовы к стабилизации в этой версии, есть один метод, который был стабилизирован. array::IntoIter позволяет выполнять итерацию массивов по значению, а не по ссылке, что устраняет существенный недостаток. Продолжается обсуждение возможности реализации IntoIterator непосредственно для массивов, хотя есть проблемы с обратной совместимостью, которые все еще необходимо решить. IntoIter::new действует как временное решение, значительно упрощающее работу с массивами.

use std::array;
fn needs_vec(v: Vec<i32>) {
    // ...
}

let arr = [vec![0, 1], vec![1, 2, 3], vec![3]];
for elem in array::IntoIter::new(arr) {
    needs_vec(elem);
}

Что дальше?

Общие константы и аргументы по умолчанию

Общие параметры в настоящее время должны располагаться в определенном порядке: время жизни, типы, константы. Однако это вызывает трудности, когда кто-то пытается использовать аргументы по умолчанию вместе с константными параметрами. Чтобы компилятор знал, какой общий аргумент является каким, любые аргументы по умолчанию должны быть помещены последними. Эти два ограничения - «типы предшествуют константам» и «значения по умолчанию идут последними» - конфликтуют друг с другом для определений, которые имеют аргументы типа по умолчанию и параметры констант.

Решение этой проблемы - ослабить ограничение порядка, чтобы параметры const могли предшествовать аргументам типа. Однако при реализации этого изменения, оказывается, есть свои тонкости, потому что компилятор Rust в настоящее время делает предположения о порядке параметров, которые требуют некоторой деликатности для удаления.

В свете аналогичных вопросов проектирования по умолчанию для аргументов const, они также в настоящее время не поддерживаются в версии 1.51. Однако исправление проблем с упорядочением параметров выше также разблокирует значения по умолчанию const.

Константные универсальные шаблоны для пользовательских типов

Чтобы тип был допустимым, теоретически, как тип константного параметра, мы должны иметь возможность сравнивать значения этого типа во время компиляции. Кроме того, равенство значений должно быть правильным (а именно, оно должно быть детерминированным, рефлексивным, симметричным и транзитивным). Чтобы гарантировать эти свойства, концепция структурного равенства была введена в RFC-обобщениях констант: по сути, это включает любой тип с #[derive(PartialEq, Eq)], члены которого также удовлетворяют структурному равенству.

Есть еще несколько вопросов, касающихся того, как именно должно вести себя структурное равенство, и предпосылок для его реализации. Примитивные типы значительно проще, что позволило нам стабилизировать константные дженерики для этих типов перед более общими типами.

Константные обобщения со сложными выражениями

Есть несколько сложностей, связанных с поддержкой сложных выражений. Флаг функции, feature (const_evaluatable_checked), доступен в канале Nightly, который включает версию поддержки сложных выражений для константных обобщений.

Одна из трудностей заключается в необходимости иметь какой-то способ сравнения неоцененных констант, поскольку компилятор автоматически не знает, что два синтаксически идентичных выражения фактически равны. Это включает в себя своего рода символические рассуждения о выражениях, что в целом является сложной проблемой.

// The two expressions `N + 1` and `N + 1` are distinct
// entities in the compiler, so we need a way to check
// if they should be considered equal.
fn foo<const N: usize>() -> [u8; N + 1] {
    [0; N + 1]
}

Нам также нужен способ борьбы с потенциальными ошибками при оценке общих операций.

fn split_first<T, const N: usize>(arr: [T; N]) -> (T, [T; N - 1]) {
    // ...
}

fn generic_function<const M: usize>(arr: [i32; M]) {
    // ...
    let (head, tail) = split_first(arr);
    // ...
}

Без возможности ограничить возможные значения M здесь, вызов generic_function::<0>() вызовет ошибку при оценке 0 - 1, которая не обнаруживается во время объявления и поэтому может неожиданно выйти из строя для нижестоящих пользователей.

Существуют вопросы проектирования о том, как точно выразить эти виды границ, которые необходимо решить, прежде чем стабилизировать сложные константные аргументы.

Резюме

С такой важной новой функцией, вероятно, будет несколько шероховатостей. Если у вас возникнут какие-либо проблемы, даже если они настолько незначительны, как запутанное сообщение об ошибке, пожалуйста, откройте проблему! Мы хотим, чтобы пользовательский опыт был как можно лучше - и любые проблемы, которые сейчас возникают, вероятно, станут еще более важными для следующих итераций константных дженериков.