Пытаюсь реализовать абстракцию ResourceId в Rust так, чтобы:
Прежде чем что-либо компилировать, я придумал:
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?
добро пожаловать на ночную сторону Rust.
@eggyal добавил исходные ошибки компиляции, но я не очень уверен, что они пригодятся, поскольку шаблон, который я пытался реализовать, просто не представляется возможным в текущей последней версии Rust.
@eggyal кажется, что это способ сделать это, но это невозможно в стабильной версии, верно? Я бы даже не стал так беспокоиться об использовании ночной версии, но эти функции все еще помечены как экспериментальные. Похоже, что-то может измениться буквально за одну ночь
Согласованный. Я думал, вы пытаетесь использовать эти функции. Ваше последнее изменение разъясняет, что это не так, поэтому я удалил комментарий. Я думаю, что это все еще возможно на стабильной версии ржавчины, но это становится более сложным в зависимости от того, какая гибкость вам требуется от префикса. Можете ли вы, например, ограничить его фиксированным количеством байтов или символов?
Почему вы хотите хранить префикс для каждого идентификатора ресурса? Оно будет одинаковым для каждого ResourceId<T> определенного типа T, поэтому хранить его в памяти каждый раз кажется бессмысленным.
Очевидной альтернативой может быть фиксированный размер для каждого идентификатора ресурса, например. ResourceId<T>(usize). У вас не будет возможности использовать разные размеры идентификаторов ресурсов для разных типов, но действительно ли это необходимо? Если вам нужно строковое представление идентификатора ресурса, вы всегда можете добавить к ResourceId метод, который возвращает это строковое представление на основе постоянного префикса типа и байтов в значении usize.
@SvenMarnach Думаю, у тебя очень хорошая точка зрения. Идея этих идентификаторов ресурсов заключается в том, чтобы использовать их для взаимодействия с API, тогда как хранилища данных будут содержать только байты в качестве идентификаторов. В этом случае, вероятно, каждый ResourceKind имеет fn prefix() -> &'static str и использует его.

В общем, требование о том, чтобы разные типы не имели одного и того же свойства, может быть соблюдено на уровне типа путем проецирования этого свойства обратно на тип. В вашем случае можно определить новый признак, скажем 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;
}
)+}
}
Посмотрите все это вместе на детской площадке.
Ух ты, это потрясающий урок по системе типизации, и, похоже, это действительно единственный способ добиться этого без специального макроса процедуры или функций, о которых мы упоминали. Мне это нравится!
«Очевидно, меня встретил ряд ошибок, связанных с экспериментальной функцией
generic_const_exprs» — было бы полезно, если бы вы показали эти ошибки и код, который их вызвал.