Resistor Color Duo in Cairo: Complete Solution & Deep Dive Guide
Resistor Color Duo in Cairo: A Complete Guide to Enums and Pattern Matching
To solve the Resistor Color Duo challenge in Cairo, you must map electronic resistor color bands to their corresponding integer values. This is elegantly achieved by defining a Color enum for type safety and using a match expression to convert each color into a number, finally combining the first two values into a single two-digit integer.
Ever found yourself squinting at a tiny electronic component, a resistor perhaps, trying to decipher the microscopic text printed on it? If you've tinkered with a Raspberry Pi, Arduino, or any circuit board, you know this struggle well. The solution, devised decades ago, was brilliant: a system of color-coded bands that represent the resistor's value. But this analog solution presents a new challenge in the digital world: how do we teach a program to read these colors?
This is where the power of modern, safety-focused programming languages like Cairo comes into play. In this deep-dive guide, we will not only solve the "Resistor Color Duo" problem from the exclusive kodikra.com curriculum, but we'll also use it as a practical launchpad to master two of Cairo's most fundamental and powerful features: Enums and Pattern Matching. Prepare to transform abstract color names into concrete numerical data, building a robust and efficient solution ready for the world of verifiable computation.
What is the Resistor Color Duo Problem?
The core task is straightforward yet foundational. We are given a set of color bands, represented as a list or array, and we need to determine the resistor's two-digit value based on the first two bands. Each color corresponds to a specific number from 0 to 9.
Here's the standard color-to-value mapping:
- Black: 0
- Brown: 1
- Red: 2
- Orange: 3
- Yellow: 4
- Green: 5
- Blue: 6
- Violet: 7
- Grey: 8
- White: 9
The goal is to take the numerical values of the first two colors and concatenate them to form a two-digit number. For instance, if the first band is Brown (1) and the second band is Green (5), the resulting value is 15. If the bands are Blue (6) and Grey (8), the value is 68.
Our program must accept an array of colors and return a single integer representing this two-digit value. This simple problem is a perfect vehicle for exploring how to handle discrete, predefined sets of data in a type-safe manner.
Why Use Cairo for This Task?
While you could solve this problem in any language, using Cairo offers unique advantages that highlight its design philosophy, especially concerning security and correctness in the Starknet ecosystem. Cairo is not just another general-purpose language; it's built for provability, meaning code correctness can be mathematically verified.
For a problem like Resistor Color Duo, Cairo's features provide significant benefits:
- Unmatched Type Safety with Enums: Cairo's strong type system, particularly its implementation of enums, prevents a whole class of bugs. By defining a
Colorenum, we restrict the possible inputs to only the valid colors. This makes it impossible to accidentally pass an invalid color like "Purple" or misspell "Brown" as "brownn", errors that would crash a program relying on raw strings. - Exhaustive Pattern Matching: The
matchkeyword in Cairo forces you to handle every possible variant of an enum. The compiler will raise an error if you forget to handle a color, ensuring your logic is complete and preventing unexpected runtime behavior. This "compile-time correctness" is a cornerstone of secure smart contract development. - Clarity and Readability: The combination of enums and match statements makes the code's intent crystal clear. Anyone reading the code can immediately understand the mapping between colors and values without needing to look up comments or documentation. This self-documenting quality is invaluable for code audits and team collaboration.
By tackling this problem in Cairo, you're not just learning to code; you're learning to think in a way that prioritizes robustness, clarity, and verifiable correctness—skills that are non-negotiable for building on the decentralized web.
How to Implement the Solution in Cairo (The Deep Dive)
Let's break down the implementation into logical steps, building our Cairo module from the ground up. We'll be using Scarb, the official Cairo package manager, to structure our project.
Setting Up Your Scarb Project
First, open your terminal and create a new Scarb project. This command scaffolds a standard Cairo library structure for you.
$ scarb new resistor_color_duo
Created library `resistor_color_duo` package
Now, navigate into the newly created directory and open the src/lib.cairo file. This is where we will write all our logic.
$ cd resistor_color_duo
$ code . # Or your favorite editor
Step 1: Defining the Colors with an `enum`
Our first step is to represent the colors in a way that the Cairo compiler understands and can enforce. Instead of using strings ('black', 'brown'), which are prone to typos, we'll define an enum. An enum (enumeration) is a custom type that consists of a fixed set of named variants.
In src/lib.cairo, add the following code:
#[derive(Copy, Drop, PartialEq)]
pub enum Color {
Black,
Brown,
Red,
Orange,
Yellow,
Green,
Blue,
Violet,
Grey,
White,
}
Let's analyze this snippet:
pub enum Color: We declare a public enumeration namedColor.Black, Brown, ...: These are the "variants" of the enum. They are the only possible values a variable of typeColorcan hold.#[derive(Copy, Drop, PartialEq)]: This is an attribute that tells the compiler to automatically generate code (a "trait implementation") for certain behaviors.Dropallows values of this type to go out of scope,Copyallows them to be copied cheaply (like integers), andPartialEqallows us to compare twoColorvalues for equality (e.g.,color1 == Color::Black), which is useful for testing.
Step 2: Mapping Colors to Values with a `match` Expression
Now that we have our Color type, we need a way to convert each variant into its corresponding numeric value (0-9). The most idiomatic and safest way to do this in Cairo is with a match expression.
Let's create a function called color_code that takes a Color and returns a u8 (an 8-bit unsigned integer, perfect for numbers 0-255).
fn color_code(color: Color) -> u8 {
match color {
Color::Black => 0,
Color::Brown => 1,
Color::Red => 2,
Color::Orange => 3,
Color::Yellow => 4,
Color::Green => 5,
Color::Blue => 6,
Color::Violet => 7,
Color::Grey => 8,
Color::White => 9,
}
}
This function is the heart of our color-to-value logic. The match statement checks the value of the input color and executes the code on the right side of the => for the matching variant. If you were to remove a line, say Color::White => 9,, the Cairo compiler would produce an error, stating that the match is "non-exhaustive." This compile-time check is a powerful safety feature.
Here is an ASCII art diagram illustrating the flow of this function:
● Input: `Color` enum variant
│
▼
◆ Match `color`
├─ Case `Color::Black` ─────> Return 0
├─ Case `Color::Brown` ─────> Return 1
├─ Case `Color::Red` ───────> Return 2
├─ Case `Color::Orange` ────> Return 3
├─ Case `Color::Yellow` ────> Return 4
├─ Case `Color::Green` ─────> Return 5
├─ Case `Color::Blue` ──────> Return 6
├─ Case `Color::Violet` ────> Return 7
├─ Case `Color::Grey` ──────> Return 8
└─ Case `Color::White` ─────> Return 9
│
▼
● Output: `u8` value
Step 3: The Main Logic - Calculating the Duo Value
Finally, we need to write the main function, value, which takes an array of colors and performs the calculation.
pub fn value(colors: Array<Color>) -> u8 {
let val1 = color_code(*colors.at(0));
let val2 = color_code(*colors.at(1));
val1 * 10 + val2
}
Let's break this down line by line:
pub fn value(colors: Array<Color>) -> u8: We define a public functionvaluethat accepts one argument,colors, which is anArrayofColorenums. It returns au8.let val1 = color_code(*colors.at(0));:colors.at(0): This accesses the element at the first position (index 0) of the array. It returns a "snapshot" or read-only reference to the value.*: This is the dereference operator. It gets the actualColorvalue from the reference so we can pass it to ourcolor_codefunction.color_code(...): We call our helper function to get the numeric value for the first color.
let val2 = color_code(*colors.at(1));: We do the exact same thing for the second color at index 1.val1 * 10 + val2: This is the final calculation. Ifval1is 1 andval2is 5, this expression evaluates to1 * 10 + 5, which is 15. Since this is the last expression in the function, its result is automatically returned.
The following diagram illustrates the complete data flow for our main value function:
● Start: Input `Array<Color>`
│
▼
┌──────────────────────────┐
│ Get color at index 0 │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ Map to numeric value (val1)│
│ via `color_code` function │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ Get color at index 1 │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ Map to numeric value (val2)│
│ via `color_code` function │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ Calculate: (val1 * 10) + val2 │
└────────────┬─────────────┘
│
▼
● End: Return two-digit `u8`
Step 4: Putting It All Together and Adding Tests
A robust solution is incomplete without tests. Scarb makes testing incredibly easy. We can add tests directly in our src/lib.cairo file within a specially marked module.
Here is the complete code for src/lib.cairo, including the logic and a suite of tests:
#[derive(Copy, Drop, PartialEq, Debug)]
pub enum Color {
Black,
Brown,
Red,
Orange,
Yellow,
Green,
Blue,
Violet,
Grey,
White,
}
fn color_code(color: Color) -> u8 {
match color {
Color::Black => 0,
Color::Brown => 1,
Color::Red => 2,
Color::Orange => 3,
Color::Yellow => 4,
Color::Green => 5,
Color::Blue => 6,
Color::Violet => 7,
Color::Grey => 8,
Color::White => 9,
}
}
pub fn value(colors: Array<Color>) -> u8 {
// We assume the input array will always have at least two colors,
// as per the problem's constraints.
let val1 = color_code(*colors.at(0));
let val2 = color_code(*colors.at(1));
val1 * 10 + val2
}
#[cfg(test)]
mod tests {
use super::{Color, value};
#[test]
fn test_brown_and_black() {
let colors = array
![Color::Brown, Color::Black];
assert(value(colors)
== 10, 'Brown and Black should be 10');
}
#[test]
fn test_blue_and_grey() {
let colors = array
![Color::Blue, Color::Grey];
assert(value(colors)
== 68, 'Blue and Grey should be 68');
}
#[test]
fn test_yellow_and_violet() {
let colors = array
![Color::Yellow, Color::Violet];
assert(value(colors)
== 47, 'Yellow and Violet should be 47');
}
#[test]
fn test_orange_and_orange() {
let colors = array
![Color::Orange, Color::Orange];
assert(value(colors)
== 33, 'Orange and Orange should be 33');
}
#[test]
fn test_ignore_additional_colors() {
let colors = array
![Color::Green, Color::Brown, Color::Orange];
assert(value(colors)
== 51, 'Only first two colors matter');
}
}
Note: I added Debug to the derive attribute. This helps in printing the enum variants during testing and debugging, which is a good practice.
To run the tests, simply execute the following command in your terminal:
$ scarb test
Compiling resistor_color_duo v0.1.0 (...)
Finished release target(s) in ...
Running resistor_color_duo
[PASS] resistor_color_duo::tests::test_brown_and_black
[PASS] resistor_color_duo::tests::test_blue_and_grey
[PASS] resistor_color_duo::tests::test_yellow_and_violet
[PASS] resistor_color_duo::tests::test_orange_and_orange
[PASS] resistor_color_duo::tests::test_ignore_additional_colors
Tests: 5 passed, 0 failed, 0 skipped
A successful test run confirms our logic is correct and our implementation is solid!
Alternative Approaches and Considerations
While using an enum and match is the most idiomatic and safest approach in Cairo, it's worth exploring alternatives to understand the trade-offs.
Alternative: Using `Into` Trait
For a more direct conversion, we could implement the Into<u8> trait for our Color enum. This allows for a more seamless casting from Color to u8.
impl ColorIntoU8 of Into<Color, u8> {
fn into(self: Color) -> u8 {
match self {
Color::Black => 0,
// ... all other colors
Color::White => 9,
}
}
}
// The value function would then look like this:
pub fn value(colors: Array<Color>) -> u8 {
let val1: u8 = (*colors.at(0)).into();
let val2: u8 = (*colors.at(1)).into();
val1 * 10 + val2
}
This approach is slightly cleaner within the value function but centralizes the conversion logic in a trait implementation, which is excellent for reusability across a larger codebase.
Pros and Cons of `match` vs. Other Data Structures
One might wonder: why not use a hash map (like LegacyMap in Cairo) to store the color-to-value mappings? Let's compare.
| Aspect | match on enum |
LegacyMap<Color, u8> |
|---|---|---|
| Type Safety | Excellent. The compiler guarantees all enum variants are handled at compile time. Impossible to have a missing color. | Fair. The map must be populated at runtime. There's a risk of forgetting to insert a color-value pair, leading to a runtime error. |
| Performance | Highly Optimized. The compiler can often optimize a match expression into a simple jump table, which is extremely fast. | Good, but slower. Involves a hashing operation and memory lookup, which carries more overhead than a direct jump. |
| Readability | Excellent. The logic is self-contained, declarative, and easy for any developer to understand instantly. | Good. The mapping is clear, but the initialization logic is separate from the lookup logic, potentially splitting the context. |
| Use Case | Ideal for a fixed, known set of variants that won't change at runtime. Perfect for this problem. | Better for dynamic collections where key-value pairs can be added or removed during program execution. |
For the Resistor Color Duo problem, the set of colors is fixed and known ahead of time. Therefore, the enum and match pattern is unequivocally the superior solution, offering the best combination of safety, performance, and clarity.
Frequently Asked Questions (FAQ)
- What exactly is an `enum` in Cairo?
- An
enum(enumeration) is a custom data type that allows you to define a variable that can only be one of a predefined set of "variants". In our case, a variable of typeColorcan only beColor::Black,Color::Brown, etc., and nothing else. This prevents bugs from invalid data, like typos in strings. - How does pattern matching with `match` work?
- A
matchexpression takes a value (like ourcolorvariable) and compares it sequentially against a series of "patterns" (ourColor::Black,Color::Brown, etc.). When it finds the first pattern that matches the value, it executes the corresponding code and exits the block. Cairo's compiler ensures that you have a pattern for every possible value, making your code exhaustive and safe. - Why did you choose `u8` for the color codes?
- A
u8is an 8-bit unsigned integer, capable of storing values from 0 to 255. Since the resistor color codes are single digits from 0 to 9, au8is the smallest, most memory-efficient integer type that can comfortably hold these values. Using the most appropriately sized type is good practice in systems programming. - What does `*colors.at(0)` mean in the code?
- This expression is composed of two parts.
colors.at(0)accesses the element at index 0 of theArray, but for safety, it returns a read-only reference (a "snapshot") to it, not the value itself. The asterisk*is the dereference operator, which "follows" the reference to get the actualColorvalue stored at that location so we can use it. - Could I use strings instead of enums? Why is it not recommended?
- While technically possible, using strings is highly discouraged. Strings are prone to errors from typos (e.g., "blue" vs "bleu"), case sensitivity ("Brown" vs "brown"), and allow for invalid inputs ("purple"). Enums eliminate all these issues at the compile stage, ensuring that only valid, pre-defined colors can ever be processed by your logic, making the program far more robust.
- How do I test this Cairo code?
- The Cairo ecosystem uses the Scarb package manager, which has a built-in test runner. You write your tests inside a
#[cfg(test)] mod tests { ... }block in your source file, using the#[test]attribute for each test function. Then, you simply runscarb testfrom your terminal in the project's root directory. - What are some other real-world uses for `enums` in Starknet contracts?
- Enums are critical in smart contracts for managing state. For example, a contract could have a
Statusenum with variants likePending,Active, andCompleted. They are also used to define different types of assets in a multi-token contract or to represent different roles in an access control system (e.g.,Admin,User,Moderator).
Conclusion: From Colors to Clean Code
We've successfully journeyed from a real-world electronics problem to an elegant, safe, and efficient software solution using Cairo. By leveraging the power of enums for type safety and pattern matching for exhaustive logic, we built a program that is not just correct, but provably so. The compiler itself acts as our first line of defense, ensuring we've handled every possible color and preventing a wide range of potential bugs before the code is ever run.
These concepts are not just academic; they are the bedrock of secure and reliable Starknet smart contract development. Mastering enums and match statements will empower you to write clearer, more maintainable, and significantly safer code. The principles learned in this kodikra module are directly applicable to managing state, handling user roles, or processing different transaction types in complex decentralized applications.
Disclaimer: The code and concepts discussed in this article are based on Cairo v2.6+ and the Scarb package manager (v2.6+). The Cairo language and its tooling are in active development, so some syntax or commands may evolve in future versions.
Ready to continue your learning journey? Explore our complete Cairo Learning Roadmap to tackle more challenges and deepen your understanding. Or, if you want to dive deeper into Cairo's features, see our comprehensive Cairo language guide.
Published by Kodikra — Your trusted Cairo learning resource.
Post a Comment