Использование const, определенного в общих типах с привязкой к признакам в Rust

Пытаюсь реализовать абстракцию ResourceId в Rust так, чтобы:

  • для данного типа ресурса префикс идентификатора ограничивается во время компиляции
  • для данного типа ресурса размер этого идентификатора в байтах ограничивается во время компиляции.
  • Идентификатор ресурса для типа ресурса A несовместим с идентификатором ресурса для типа B, который также применяется во время компиляции.

Прежде чем что-либо компилировать, я придумал:

pub struct ResourceId<T: ResourceKind>([u8; T::SIZE], PhantomData<T>);

trait ResourceKind {
    const SIZE: usize;
    const PREFIX: &'static str;
}

impl<T: ResourceKind> Display for ResourceId<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let encoded_bytes = /* encode self.0 to a string */
        write!(f, "{}_{}", T::PREFIX, encoded_bytes)
    }
}

Теперь давайте реализуем идентификатор ресурса Foo:

pub struct Foo;
impl ResourceKind for Foo {
    const SIZE: usize = 8;
    const PREFIX: &'static str = "foo";
}

Теперь в моем коде, где бы я ни хотел работать с ресурсом Foo, я хочу принять ResourceId<Foo>, гарантирующий, что идентификатор ресурса с другим префиксом, чем «foo», или с другим размером байта, чем «8», не будет компилироваться.

Меня явно встретил ряд ошибок, которые потенциально можно было бы устранить с помощью экспериментальной функции generic_const_exprs.

Ошибки, которые я получал:

error: generic parameters may not be used in const operations
 --> packages/core_resource/lib.rs:9:45
  |
9 | pub struct ResourceId<T: ResourceKind>([u8; T::SIZE], PhantomData<T>);
  |                                             ^^^^^^^ cannot perform const operation using `T`
  |
  = note: type parameters may not be used in const expressions

error: constant expression depends on a generic parameter
  --> packages/core_resource/lib.rs:18:33
   |
18 |         let mut id = Self([0u8; T::SIZE], PhantomData);
   |                                 ^^^^^^^
   |
   = note: this may fail depending on what value the parameter takes

error: aborting due to 2 previous errors

Другие альтернативы, о которых я подумал:

  • измените ResourceId на ResourceId<T: ResourceKind, const SIZE: usize>, но это фактически позволит определить идентификатор для ресурса R с несколькими допустимыми размерами в байтах, поскольку они просто станут независимыми
  • реализовать макрос атрибута и использовать его для создания отдельных, а не универсальных типов, таких как #[resource_id(prefix = "foo", size = 8) pub struct FooId;; кажется достойной альтернативой, поэтому буду признателен за отзывы.

Каким был бы приемлемый и (надеюсь) идиоматический дизайн кода для этого, не полагаясь на ночной компилятор и generic_const_exprs?

«Очевидно, меня встретил ряд ошибок, связанных с экспериментальной функцией generic_const_exprs» — было бы полезно, если бы вы показали эти ошибки и код, который их вызвал.

eggyal 23.05.2024 09:47

добро пожаловать на ночную сторону Rust.

Stargateur 23.05.2024 09:52

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

Victor 23.05.2024 10:42

@eggyal кажется, что это способ сделать это, но это невозможно в стабильной версии, верно? Я бы даже не стал так беспокоиться об использовании ночной версии, но эти функции все еще помечены как экспериментальные. Похоже, что-то может измениться буквально за одну ночь

Victor 23.05.2024 10:44

Согласованный. Я думал, вы пытаетесь использовать эти функции. Ваше последнее изменение разъясняет, что это не так, поэтому я удалил комментарий. Я думаю, что это все еще возможно на стабильной версии ржавчины, но это становится более сложным в зависимости от того, какая гибкость вам требуется от префикса. Можете ли вы, например, ограничить его фиксированным количеством байтов или символов?

eggyal 23.05.2024 10:54

Почему вы хотите хранить префикс для каждого идентификатора ресурса? Оно будет одинаковым для каждого ResourceId<T> определенного типа T, поэтому хранить его в памяти каждый раз кажется бессмысленным.

Sven Marnach 23.05.2024 13:33

Очевидной альтернативой может быть фиксированный размер для каждого идентификатора ресурса, например. ResourceId<T>(usize). У вас не будет возможности использовать разные размеры идентификаторов ресурсов для разных типов, но действительно ли это необходимо? Если вам нужно строковое представление идентификатора ресурса, вы всегда можете добавить к ResourceId метод, который возвращает это строковое представление на основе постоянного префикса типа и байтов в значении usize.

Sven Marnach 23.05.2024 13:37

@SvenMarnach Думаю, у тебя очень хорошая точка зрения. Идея этих идентификаторов ресурсов заключается в том, чтобы использовать их для взаимодействия с API, тогда как хранилища данных будут содержать только байты в качестве идентификаторов. В этом случае, вероятно, каждый ResourceKind имеет fn prefix() -> &'static str и использует его.

Victor 23.05.2024 15:26
Как создавать пользовательские общие типы в Python (50/100 дней Python)
Как создавать пользовательские общие типы в Python (50/100 дней Python)
Помимо встроенных типов, модуль типизации в Python предоставляет возможность определения общих типов, что позволяет вам определять типы, которые могут...
0
8
85
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

pub trait ResourceKindOf {
    type Kind: ResourceKind;
}

pub trait ResourceKind {
    type Id: ResourceKindOf<Kind = Self>;
    type Prefix: ResourceKindOf<Kind = Self>;
}

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

Тип, который следует использовать для Id, довольно очевиден: [u8; Z], где Z — предыдущая константа SIZE. Мы можем обеспечить разрешение только типов, соответствующих такому шаблону, добавив ограничение PermissableId, где PermissableId — запечатанный признак, который мы реализуем только для [u8; Z]:

mod sealed {
    pub trait PermissableId {}
}
use sealed::PermissableId;

impl<const Z: usize> PermissableId for [u8; Z] {}

pub trait ResourceKind {
    type Id: PermissableId + ResourceKind<Kind = Self>;
    // etc
}

С префиксом немного сложнее, поскольку не существует такой очевидной версии произвольной строки на уровне типа. Лучшее, что я могу придумать, это использовать список минусов символов:

struct Nil;
struct Cons<T, const C: char>(PhantomData<T>);

Тогда, например, мы можем иметь уровень типа "foo" как Cons<Cons<Cons<Nil, 'o'>, 'o'>, 'f'>. Однако писать это довольно ужасно, поэтому давайте (немного) упростим задачу с помощью макроса:

macro_rules! cons {
    () => {Nil};
    ($c:literal $($rest:tt)*) => {$crate::Cons<cons!($($rest)*), $c>};
}

Теперь мы можем вместо этого написать cons!('f''o''o'), чтобы получить строку уровня типа, представляющую "foo". Раздражает то, что нам нужно цитировать каждый символ индивидуально, но я не могу придумать лучшего способа (во всяком случае, без макроса proc).

Затем мы можем добавить такое же ограничение, как и раньше, чтобы гарантировать, что Prefix всегда является строкой уровня типа. Однако в данном случае мы также хотим иметь возможность конвертировать cons-список обратно в отображаемую строку во время выполнения, поэтому давайте добавим метод fmt к нашему запечатанному признаку:

mod sealed {
    use std::fmt;

    pub trait PermissablePrefix {
        fn fmt(f: &mut fmt::Formatter<'_>) -> fmt::Result;
    }
}
use sealed::PermissablePrefix;

impl PermissablePrefix for Nil {
    fn fmt(_: &mut fmt::Formatter<'_>) -> fmt::Result {
        Ok(())
    }
}

impl<T: PermissablePrefix, const C: char> PermissablePrefix for Cons<T, C> {
    fn fmt(f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_char(C)?;
        T::fmt(f)
    }
}

pub trait ResourceKind {
    type Prefix: PermissablePrefix + ResourceKind<Kind = Self>;
    // etc
}

Наконец, давайте упростим использование всего этого, создав макрос для определения типов ресурсов и связанных с ними реализаций признаков:

macro_rules! resource_kinds {
    ($($kind:ident{$size:literal, $($prefix:tt)*})+) => {$(
        pub struct $kind;
        impl $crate::ResourceKind for $kind {
            type Id = [u8; $size];
            type Prefix = $crate::cons!($($prefix)*);
        }
        impl $crate::ResourceKindOf for [u8; $size] {
            type Kind = $kind;
        }
        impl $crate::ResourceKindOf for $crate::cons!($($prefix)*) {
            type Kind = $kind;
        }
    )+}
}

Посмотрите все это вместе на детской площадке.

Ух ты, это потрясающий урок по системе типизации, и, похоже, это действительно единственный способ добиться этого без специального макроса процедуры или функций, о которых мы упоминали. Мне это нравится!

Victor 23.05.2024 12:27

Другие вопросы по теме