Constants and unreachable patterns with Color

I have the following implementation of RGBA color codes, with specific const colors, and the option to create new colors from their RGBA values.

In the match statement for abbreviation(), I get several errors about unreachable patterns.

This makes sense as the underlying values in the const are identical, but the names are not.

I had tried to add the abbreviation value directly into the consts but ran into a nightmare of lifetime errors that I could not resolve.

I am not wedded to the approach below, so if there are other ways of accomplishing this, I am interested.

(I know I could use a 3rd party library for color handling, but each of my output formats have different requirements for color with different libraries in use, so I am using my own struct internally, and then converting when needed).

Thanks

pub struct Color {
    red: u8,
    green: u8,
    blue: u8,
    alpha: u8,
}

impl Color {
    // All constants match CSS Standard Colors as closely as possible.

    ///FF0000FF.
    pub const RED: Self = Self::from_rgba(0xFF, 0x0, 0x0, 0x0); 
    ///FFA500FF.
    pub const ORANGE: Self = Self::from_rgba(0xFF, 0xA5, 0x0, 0xFF);
    ///FFFF00FF.
    pub const YELLOW: Self = Self::from_rgba(0xFF, 0xFF, 0x0, 0xFF);
    ///00FF00FF.
    pub const GREEN: Self = Self::from_rgba(0x0, 0xFF, 0x0, 0xFF);
    ///OOOOFFFF.
    pub const BLUE: Self = Self::from_rgba(0x0, 0x0, 0xFF, 0xFF);
    ///800080FF.
    pub const PURPLE: Self = Self::from_rgba(0x80, 0x0, 0x80, 0xFF);
    ///EE82EEFF.
    pub const VIOLET: Self = Self::from_rgba(0xEE, 0x82, 0xEE, 0xFF);
    ///FFC0CBFF.
    pub const PINK: Self = Self::from_rgba(0xFF, 0xC0, 0xCB, 0xFF);
    ///FFC0CBFF.
    pub const ROSE: Self = Self::from_rgba(0xFF, 0xC0, 0xCB, 0xFF);
    ///FFC0CBFF.
    pub const MAGENTA: Self = Self::from_rgba(0xFF, 0x0, 0xFF, 0xFF);
    ///A52A2AFF.
    pub const BROWN: Self = Self::from_rgba(0xA5, 0x2A, 0x2A, 0xFF);
    ///852A2AFF.
    pub const DARK_BROWN: Self = Self::from_rgba(0x85, 0x2A, 0x2A, 0xFF);
    ///000000FF.
    pub const BLACK: Self = Self::from_rgba(0x0, 0x0, 0x0, 0xFF);
    ///FFFFFFFF.
    pub const WHITE: Self = Self::from_rgba(0xFF, 0xFF, 0xFF, 0xFF);
    ///808080FF.
    pub const GRAY: Self = Self::from_rgba(0x89, 0x80, 0x80, 0xFF);
    ///808080FF.
    pub const GREY: Self = Self::from_rgba(0x89, 0x80, 0x80, 0xFF);
    ///808080FF.
    pub const SLATE: Self = Self::from_rgba(0x89, 0x80, 0x80, 0xFF);
    ///FFFFFFFF.
    pub const CLEAR: Self = Self::from_rgba(0xFF, 0xFF, 0xFF, 0xFF);
    ///00FFFFFF.
    pub const CYAN: Self = Self::from_rgba(0x0, 0xFF, 0xFF, 0xFF);
    ///00FFFFFF.
    pub const AQUA: Self = Self::from_rgba(0x0, 0xFF, 0xFF, 0xFF);
    ///00000000.
    pub const TRANSPARENT: Self = Self::from_rgba(0x0, 0x0, 0x0, 0x0);

    /// Creates a `Color` value from separate red, green, blue and alpha values.
    #[inline]
    #[must_use]
    pub const fn from_rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
        Self { red, green, blue, alpha }
    }

    /// Returns a 3 character abbreviation for each color.
    #[must_use]
    #[inline]
    pub fn abbreviation(self) -> String {
        match self {
            Self::RED => "RED".to_owned(),
            Self::ORANGE => "ORN".to_owned(),
            Self::YELLOW => "YEL".to_owned(),
            Self::GREEN => "GRN".to_owned(),
            Self::BLUE => "BLU".to_owned(),
            Self::PURPLE => "PUR".to_owned(),
            Self::VIOLET => "VIO".to_owned(),
            Self::PINK => "PNK".to_owned(),
            Self::ROSE => "RSE".to_owned(),
            Self::MAGENTA => "MGA".to_owned(),
            Self::BROWN | Self::DARK_BROWN => "BRN".to_owned(),
            Self::BLACK => "BLK".to_owned(),
            Self::WHITE => "WHT".to_owned(),
            Self::GRAY | Self::GREY => "GRY".to_owned(),
            Self::SLATE => "SLT".to_owned(),
            Self::CLEAR => "CLR".to_owned(),
            Self::CYAN => "CYN".to_owned(),
            Self::AQUA => "AQA".to_owned(),
            _ => String::new(),
        }
    }
    /// Returns a 6 character hex code (RRGGBB) for each color.
    #[must_use]
    #[inline]
    pub fn hex_code(&self) -> String {
        format! {"{:02X}{:02X}{:02X}", self.red, self.green, self.blue}
    }
}


I would recommend that you should, first, separate your problem into two questions:

  1. What is the correct behavior?
  2. What is the best expression of that behavior as code?

It’s not clear what the correct behavior is. Do you want it to be the case that the user can enter a color name, and then get (an abbreviated form of) that same name back, for all recognized names? Or do you not care about that, and merely want to ensure that all named colors map to some abbreviation?

If the names should be preserved, then I would recommend that you define an

enum NamedColor {
    Red,
    Orange,
    Yellow,
    ...
}

Then you can have a function to convert that to RGBA, but you would only do that when necessary, since it is a lossy conversion (a non-injective function).

Constants of custom types can only be used as match patterns if the type derives PartialEq (a hand-written impl doesn't work - has to be the derive). That's why you got the non-structural type error.

#[derive(PartialEq, Eq)]
pub struct Color { /* ... */ }

With the derive, each arm becomes a == check. The match compiles, but if you leave in the duplicate-value constants (PINK/ROSE, GRAY/GREY/SLATE, CYAN/AQUA, WHITE/CLEAR) you'll get unreachable_patterns warnings on the later arms, since they have the same RGBA as an earlier one. You can shut those up with #[allow(unreachable_patterns)].

something like:

#[allow(unreachable_patterns)]
pub fn abbreviation(self) -> String {
    match self {
        Self::PINK => "PNK".to_owned(),
        Self::ROSE => "RSE".to_owned(), // same RGBA as PINK, never reached
        // ...
        _ => String::new(),
    }
}

I am aware I can get rid of the unreachable patterns error with allow/expect, but I actually want to be able to reach those patterns.

But you can't reach those patterns. If a user calls abbreviation() on Color::from_rgba(0xFF, 0xC0, 0xCB, 0xFF), there is no way to know whether that color "is" pink or rose or something else; as far as Rust is concerned, PINK and ROSE are two names for the same value, and those names only exist at compile time, not at runtime when the code actually does its thing.

What I wanted originally, was to have something like the following:

pub struct Color {
    red: u8,
    green: u8,
    blue: u8,
    alpha: u8,
    abbreviation: String
}

And have the ability to define associated constants of that value.

(I am aware I can't have a const String), and if I use const &str I also get errors around lifetimes, etc.

I originally utilized an Enum like you suggested, but that made matching a bit trickier, especially if I need to access the RGBA values within the match. It also meant that I need to call a function to get the RGBA, abbreviation and any other static data that is associated with the constant, which means that data isn't actually constant within the program.

@jwodder I understand where the error is coming from, and why. The current implementation is not the desired end state. See the beginning of this message.

Here is a minimal impl on the playground, and the errors I get.

If I instead change String to &str in the struct, I wind up with all sorts of lifetime issues, as I need to store these constants in various owned structs outside of this module of the code.

   Compiling playground v0.0.1 (/playground)
error[E0015]: cannot call non-const method `<str as ToString>::to_string` in constants
    --> src/lib.rs:11:99
     |
  11 |     pub const RED: Self = Self {red: 0xFF, green: 0x0, blue: 0x0, alpha: 0x0, abbreviation: "RED".to_string()};
     |                                                                                                   ^^^^^^^^^^^
     |
note: method `to_string` is not const because trait `ToString` is not const
    --> /playground/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/string.rs:2866:1
     |
2866 | pub trait ToString {
     | ^^^^^^^^^^^^^^^^^^ this trait is not const
...
2880 |     fn to_string(&self) -> String;
     |     ------------------------------ this method is not const
     = help: const traits are not yet supported on stable Rust
     = note: calls in constants are limited to constant functions, tuple structs and tuple variants

For more information about this error, try `rustc --explain E0015`.
error: could not compile `playground` (lib) due to 1 previous error

Here's one approach that should get you what you want[1], based on how the http crate implements its HeaderName constants:

  • Define a ColorAbbrev enum with fieldless Red, Blue, Yellow, etc. variants for each constant color you want to define, plus a Custom(String) variant
  • Give ColorAbbrev an as_str(&self) -> &str method that maps ColorAbbrev::Red to "RED", etc., and maps Custom(s) to &s
  • Give ColorAbbrev a std::str::FromStr impl that maps "RED" to ColorName::Red, and so on for the other constants, and maps any input that's not a known constant to Custom(s.to_owned())
  • Change the type of Color.abbreviation to ColorAbbrev
  • Define pub const RED = Color {red: 0xFF, green: 0x00, blue: 0x00, alpha: 0xFF, abbreviation: ColorAbbrev::Red} and likewise for the other constants

  1. As far as I understand what you want, which may not be very much ↩︎

Use &'static str instead of String - works in const, no lifetime parameter needed: playground.

The type to use for a string that appears only in constants is &'static str; this will not have “lifetime issues”.

But in this case, you should probably not store constants at all, but define an enum with methods:

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NamedColor {
    Red,
    Orange,
    Yellow,
    ...
}

impl NamedColor {
    pub fn abbreviation(self) -> &'static str {
        match self {
            Self::Red => "RED",
            ...
        }
    }

    pub fn rgba(self) -> [u8; 4] {
        match self {
            Self::Red => [0xFF, 0x0, 0x0, 0xFF],
            ...
        }
    }
}

This has the key advantage, unlike putting both RGBA and abbreviation in a struct, that the abbreviation and RGBA value can never be inconsistent with each other (unlike the struct which could be constructed with an inconsistent set of values). It also is efficient to store, taking up only one byte per named color value.

I am confused by your statement here:

It seems to me, like having the methods on the enum, and defining info there, and defining a list of consts with the same info, is the same amount of effort, and using methods just splits up where the info is defined which could result in confusion.

Not trying to be combative, just trying to understand.

Having a list of enum variants instead of a list of consts should indeed be roughly an equal amount of effort, it just rearranges where the data is stored and written. Using methods, though, separates state from the data cheaply computed from that state, which has the advantages that @kpreid described.

If splitting the logic across multiple methods is confusing, you could have a single private method which returns a tuple (or a struct) of all the relevant information, and public methods which simply call the private method and return each of the components individually.

Looks like your playground link broke, but I was able to get things working.

I swear I had tested &'static str before, but I guess I didn't.

Here is some working code based on the &'static str method. (Obviously trimmed down a bit)

#[derive(Debug)]
pub struct Color {
    red: u8,
    green: u8,
    blue: u8,
    alpha: u8,
    abbreviation: &'static str
}

impl Color {
    
    pub const RED: Self = Self {red: 0xFF, green: 0x0, blue: 0x0, alpha: 0x0, abbreviation: "RED"};
    
    fn from_rgba_abbr(red: u8, green: u8, blue: u8, alpha: u8, abbreviation: &'static str) -> Self {
        Self {
            red, green, blue, alpha, abbreviation
        }
        
    }
}

fn main() {
    
    let red = Color::RED;
    
    let grey = Color::from_rgba_abbr(0x88, 0x88, 0x88, 0x0, "GRY");
    
    println!("{red:#?}");
    
    println!("{grey:#?}");
    
}

If the data is truly static, what is the advantage of methods vs consts with the data inside?

Seems like a very minor trade off between memory usage, and reducing function calls?

Also worth noting that for a “plain old data” struct whose constructor performs no validation, it’s normal to just mark all the fields of the struct pub.

That was just an oversight. Thanks!

Your struct allows:

Color {
    red: 0,
    green: 0xFF,
    blue: 0,
    alpha: 0xFF,
    abbreviation: "RED",
}

to be made in addition to the correct RED and GREEN structs. Defining the enum instead makes this invalid data impossible — not because of a check, but because the nonsensical value simply cannot exist.

That makes sense! Appreciate the explanation.

The data of Color::RED may be static, but if you have a Vec<Color> which you fill with Color::RED, the RGB data is not static; you store one copy of the RGB data per color you push into the Vec.

With methods, the associated RGB data seems more static to me; even when you have a Vec<Color>, the method approach wouldn’t make a bunch of copies of the RGB data.

Note that function calls, especially of small functions, can be optimized out. However, Vec<Color> can’t be automatically optimized to use a more efficient memory layout.

Interesting. I didn't think about the multiple copies aspect of it.

Much appreciated!