Is there a better way to define an immutable field on a struct?

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 ?

const INNER_I: u8 = 0x0f; // 0000 1111
const INNER_L: u8 = 0x17; // 0001 0111
const INNER_J: u8 = 0x47; // 0100 0111
const INNER_O: u8 = 0x33; // 0011 0011
const INNER_S: u8 = 0x36; // 0011 0110
const INNER_Z: u8 = 0x63; // 0110 0011
const INNER_T: u8 = 0x27; // 0010 0111

pub const I: Piece<INNER_I> = Piece::new();
pub const L: Piece<INNER_L> = Piece::new();
pub const J: Piece<INNER_J> = Piece::new();
pub const O: Piece<INNER_O> = Piece::new();
pub const S: Piece<INNER_S> = Piece::new();
pub const Z: Piece<INNER_Z> = Piece::new();
pub const T: Piece<INNER_T> = Piece::new();

pub enum Direction
{
	Up,
	Left,
	Down,
	Right,
}
#[non_exhaustive]
pub struct Piece<const P: u8>
{
	pub direction: Direction,
}
impl<const P: u8> Piece<P>
{
	const fn new() -> Self
	{
		Self {
			direction: Direction::Up,
		}
	}
	pub fn inner(&self) -> u8
	{
		P
	}
}

If I understand correctly, you want to make it so that Piece can't be created by anything other than Piece::new, yes?

If so, you can make direction not pub and create a getter for it that returns a reference:

pub struct Piece<const P: u8> {
    direction: Direction,
}
impl<const P: u8> Piece<P> {
    // .. other functions omitted ..

    pub fn direction(&self) -> &Direction {
        // create a reference to self.direction
        &self.direction
    }
}

or a copy if Direction implements Copy:

#[derive(Copy, Clone)] // new
pub enum Direction {
    // .. variants omitted ..
}

pub struct Piece<const P: u8> {
    direction: Direction,
}
impl<const P: u8> Piece<P> {
    // .. other functions omitted ..

    pub fn direction(&self) -> Direction {
        // this will copy the value
        self.direction
    }
}

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.

2 Likes

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,
    };
}
1 Like

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...

thank you @everyone I guess there's no "correct" way of doing this and among all the suggestions seems my way is the shortest.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.