Inspired by the tetris from scratch guide I saw in this week in rust I decided to do my own. To describe pieces I used bitfields and and associated it to my struct using const generics and to prevent users of my engine to construct it with any u8 I added a #[non_exhaustive] attribute to the Piece struct.
Is there a better way to do what I am trying to do ?
If const generics would support custom enum types yet, that might be an alternative. Currently, your approach seems like a decent one. An alternative could be to use no const generics at all, and instead do something like
pub mod piece {
use std::marker::PhantomData;
mod sealed {
// public-in-private trait to seal `BitPattern`
pub trait Sealed {}
}
pub trait BitPattern: sealed::Sealed {
fn inner() -> u8;
}
// private, just to save boilerplate
macro_rules! bit_patterns {
($($I:ident = $N:literal,)*) => {
$(
pub enum $I {}
impl sealed::Sealed for $I {}
impl BitPattern for $I {
fn inner() -> u8 {
$N
}
}
)*
}
}
bit_patterns! {
I = 0x0f, // 0000 1111
L = 0x17, // 0001 0111
J = 0x47, // 0100 0111
O = 0x33, // 0011 0011
S = 0x36, // 0011 0110
Z = 0x63, // 0110 0011
T = 0x27, // 0010 0111
}
pub enum Direction {
Up,
Left,
Down,
Right,
}
pub struct Piece<P: BitPattern> {
pub direction: Direction,
_marker: PhantomData<P>,
}
impl<P: BitPattern> Piece<P> {
pub fn new(direction: Direction) -> Self {
Self {
direction,
_marker: PhantomData,
}
}
pub fn inner(&self) -> u8 {
P::inner()
}
}
}
use piece::*;
#[allow(unused)]
fn use_case() {
let piece = Piece::<I>::new(Direction::Up);
}
I don't know whether this is something you'd prefer.
There isn't really any concept of "readonly fields" or "immutable types" in Rust. Instead, we enforce immutability by keeping things private and making sure all publicly accessible methods take &self instead of &mut self.
Copy types also tend to be de-facto immutable because passing them around by value creates copies. You also tend to structure their APIs such that that new instances are returned from an operation instead of mutating in place.
I've re-worked the example a bit to support const fn constructors, making all field public, and even making inner() a const fn. (Probably irrelevant, but so what.) Also it may be syntactically more pleasing to pass a zero-sized struct value like this instead of specifying a type parameter:
pub mod piece {
use paste::paste;
use std::marker::PhantomData;
mod sealed {
// public-in-private trait to seal `BitPattern`
pub trait Sealed {
const INNER: u8;
}
}
pub trait ValidBitPattern: sealed::Sealed {}
pub struct BitPattern<P>(PhantomData<P>);
// private macro, just to save boilerplate
macro_rules! bit_patterns {
($($I:ident = $N:literal,)*) => {
paste! {
$(
pub enum [< Inner $I >] {}
impl sealed::Sealed for BitPattern<[< Inner $I >]> {
const INNER: u8 = 0;
}
impl ValidBitPattern for BitPattern<[< Inner $I >]> {}
pub const $I: BitPattern<[< Inner $I >]> = BitPattern(PhantomData);
)*
}
}
}
bit_patterns! {
I = 0x0f, // 0000 1111
L = 0x17, // 0001 0111
J = 0x47, // 0100 0111
O = 0x33, // 0011 0011
S = 0x36, // 0011 0110
Z = 0x63, // 0110 0011
T = 0x27, // 0010 0111
}
pub enum Direction {
Up,
Left,
Down,
Right,
}
pub struct Piece<P>
where
BitPattern<P>: ValidBitPattern, // hack of using bound not on `P` directly, allows usage in `const fn`s
{
pub direction: Direction,
pub bit_pattern: BitPattern<P>,
}
impl<P> Piece<P>
where
BitPattern<P>: ValidBitPattern,
{
pub const fn new(bit_pattern: BitPattern<P>) -> Self {
Self {
direction: Direction::Up,
bit_pattern,
}
}
pub const fn inner(&self) -> u8 {
<BitPattern<P> as sealed::Sealed>::INNER
}
}
}
use piece::*;
#[allow(unused)]
fn use_case() {
let piece1 = Piece::new(I);
// I like this syntax:
let piece2 = Piece {
direction: Direction::Down,
bit_pattern: J,
};
}
I dont want user to be able to create one at all that's why I am using non_exhaustive. I want user to be able to use variants that I already constructed such as I,L,S...