Resistor Color Duo in Clojure: Complete Solution & Deep Dive Guide

graphical user interface, application

From Colors to Numbers: Master Clojure Maps with the Resistor Color Duo Challenge

To solve the Resistor Color Duo challenge in Clojure, you map color names to integer values using a hash map. Then, you extract the first two colors from an input vector, look up their corresponding numeric values, and concatenate them to form a two-digit integer.

You’ve been staring at the screen for an hour. The problem seems so simple: take two colors, find their corresponding numbers, and put them together. Yet, every approach you try in Clojure feels clunky, verbose, or just… wrong. You've tried `if-else` chains, maybe a `cond`, but it feels like you're fighting the language, not working with it. This is a common hurdle for developers transitioning to a functional, data-oriented language like Clojure.

This article is your breakthrough. We're going to dissect the Resistor Color Duo problem, a classic exercise from the kodikra.com exclusive curriculum. But we won't just give you the answer. We'll guide you through the elegant, idiomatic Clojure way of thinking, transforming this small challenge into a profound lesson on the power of data structures, particularly the hash map. By the end, you'll not only have a clean solution but a deeper understanding of Clojure's core philosophy.


What is the Resistor Color Duo Challenge?

Before diving into the code, let's establish a clear understanding of the problem. In electronics, resistors are tiny components that limit the flow of current. Because they're too small to print numbers on, they use a system of colored bands to represent their resistance value. Our task is to write a program that translates the first two of these color bands into a two-digit number.

The Core Requirements

The rules are straightforward. We are given a 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

Input: The program will receive a vector of strings, where each string is a color name (e.g., ["brown" "black" "green"]).

Output: The program must return a single integer representing the two-digit number formed by the values of the first two colors. For the input ["brown" "black" "green"], the first color is "brown" (1) and the second is "black" (0). Combined, they form the number 10.

This task, while simple on the surface, is a perfect vehicle for exploring fundamental data structures and functional composition in Clojure. It forces us to think about how to store lookup data efficiently and how to process a sequence of inputs in an immutable way.


Why This Challenge is a Perfect Gateway to Idiomatic Clojure

Many programming challenges can be solved with brute force, but the Resistor Color Duo problem gently pushes you toward a more elegant, data-centric solution. It's not about complex algorithms; it's about structuring your data correctly and then applying simple, powerful functions to it. This is the heart of Clojure programming.

Embracing Data-Oriented Programming

Instead of hiding the color-to-value mapping inside a complex function with a `case` or `cond` statement, Clojure encourages you to treat it as what it is: data. By defining this mapping in a primary data structure like a hash map, you separate the logic (what you do) from the data (what you do it to). This makes your code more readable, maintainable, and flexible.

The Power of Core Functions

This problem is a playground for some of Clojure's most essential functions. You'll see how a few well-chosen functions from the core library can be composed to create a solution that is both concise and highly expressive. We'll leverage functions for sequence manipulation (take), transformation (map), and data combination (apply, str), demonstrating the "small tools, combined" philosophy.

Thinking Immutably

In Clojure, data structures are immutable by default. This means you don't modify the input vector or the color map. Instead, you create new data structures based on them. This exercise provides a safe, simple context to get comfortable with this concept, which is crucial for writing safe, concurrent, and predictable code.


How to Build the Solution: A Step-by-Step Clojure Walkthrough

Let's construct our solution piece by piece, explaining the "why" behind each decision. Our goal is to write code that isn't just correct, but is also clear, concise, and idiomatic.

Step 1: The Foundation - Storing the Color Codes

The most critical part of our solution is the color-to-value mapping. What's the best way to store this key-value data? A hash map is the perfect tool for the job. In Clojure, a hash map is a collection of key-value pairs, providing incredibly fast lookups.

We'll define our map using a def to create a global, named constant. Using strings as keys directly matches our input format.


(def color-codes
  {"black"  0
   "brown"  1
   "red"    2
   "orange" 3
   "yellow" 4
   "green"  5
   "blue"   6
   "violet" 7
   "grey"   8
   "white"  9})

This color-codes map is now our "single source of truth." If a new color were added, we would only need to update this one data structure. The logic that uses it remains unchanged.

Step 2: Processing the Input - Getting the First Two Colors

Our function will receive a vector of colors, but we only care about the first two. Clojure's core library has the perfect function for this: take. The expression (take 2 a-vector) returns a lazy sequence containing only the first two elements of a-vector.

For example:


(take 2 ["brown" "black" "green"])
;; Returns a lazy sequence equivalent to: '("brown" "black")

This is a declarative approach. We state what we want (the first two items), not how to get them (like using a loop with an index counter). This is a hallmark of functional programming.

Step 3: The Transformation Logic - From Colors to Digits

Now we have a sequence of two colors, like '("brown" "black"). We need to transform this into a sequence of their corresponding numbers, '(1 0). This is a classic use case for the map function.

The map function takes a function and one or more collections. It applies the function to each item of the collection(s) and returns a new lazy sequence of the results. Here, the function we want to apply is one that looks up a color in our color-codes map.

Conveniently, in Clojure, a hash map can be used as a function! When you call a map with a key, it returns the corresponding value.


(color-codes "brown") ;; Returns 1
(color-codes "black") ;; Returns 0

;; So, we can use the map directly with the `map` function:
(map color-codes '("brown" "black"))
;; Returns a lazy sequence equivalent to: '(1 0)

Step 4: The Final Combination - From Digits to a Number

We've successfully transformed our colors into a sequence of numbers: '(1 0). The final step is to combine them into the integer 10. A common and readable way to do this is to first convert them to strings, join them, and then parse the result back into an integer.

  1. Convert numbers to strings: The str function can take multiple arguments and concatenate them. (str 1 0) would produce the string "10".
  2. Apply str to our sequence: Our numbers are in a sequence '(1 0). The apply function is designed for this exact scenario. It takes a function and a sequence, and "applies" the function to the elements of the sequence as if they were passed as individual arguments.
  3. Parse the string: Once we have the string "10", we use Java interop to parse it into an integer with Integer/parseInt.

(def digits '(1 0))

;; Apply the str function to the elements of the digits sequence
(apply str digits)
;; Returns: "10"

;; Parse the resulting string into an integer
(Integer/parseInt "10")
;; Returns: 10

Logic Flow Diagram

Here is a visual representation of our entire process, from input to output.

    ● Start: Input Vector
    │ e.g., ["brown", "black", "green"]
    ▼
  ┌──────────────────┐
  │ take 2           │
  └─────────┬────────┘
            │
            ▼
    '("brown", "black")
            │
  ┌──────────────────┐
  │ map color-codes  │
  └─────────┬────────┘
            │
            ▼
        '(1, 0)
            │
  ┌──────────────────┐
  │ apply str        │
  └─────────┬────────┘
            │
            ▼
          "10"
            │
  ┌──────────────────┐
  │ Integer/parseInt │
  └─────────┬────────┘
            │
            ▼
    ● End: Output Integer (10)

The Complete Clojure Solution and Code Walkthrough

Now, let's assemble all the pieces into a complete, working function. We'll use a let block to bind intermediate results to descriptive names, enhancing readability.

Final Solution Code


(ns resistor-color-duo)

;; Define the color-to-value mapping as a constant hash map.
;; This separates our data from our logic.
(def color-codes
  {"black"  0
   "brown"  1
   "red"    2
   "orange" 3
   "yellow" 4
   "green"  5
   "blue"   6
   "violet" 7
   "grey"   8
   "white"  9})

(defn value
  "Takes a vector of color strings and returns a two-digit number
  based on the values of the first two colors."
  [colors]
  (let [two-colors   (take 2 colors)
        digits       (map color-codes two-colors)
        number-str   (apply str digits)]
    (Integer/parseInt number-str)))

;; Example Usage:
;; (value ["brown" "black"]) -> 10
;; (value ["blue" "grey" "green"]) -> 68

Detailed Code Walkthrough

  1. (def color-codes ...): We define our lookup table. This is evaluated once when the namespace is loaded. It's efficient and keeps the data separate from the function logic.
  2. (defn value [colors] ...): We define a function named value that accepts a single argument, which we'll call colors. This argument is expected to be a collection (like a vector) of color strings.
  3. (let [...] ...): The let block allows us to define local bindings. This is incredibly useful for breaking down a complex process into understandable steps without polluting the global namespace.
  4. two-colors (take 2 colors): Inside the let, we create our first local binding. two-colors is bound to the result of taking the first two items from the input colors vector.
  5. digits (map color-codes two-colors): Next, we create digits. We use the map function to apply our color-codes map (as a function) to each color in the two-colors sequence. This transforms the sequence of color strings into a sequence of numbers.
  6. number-str (apply str digits): We create number-str by using apply to pass the elements of the digits sequence as arguments to the str function. This concatenates them into a single string.
  7. (Integer/parseInt number-str): This is the body of the let block and the final expression in the function. Its result will be the return value of the function. We call the static parseInt method from Java's Integer class to convert our numeric string into an actual integer.

Alternative Approaches & Idiomatic Refinements

The solution above is clear and excellent for learning. However, Clojure often provides multiple ways to solve a problem, each with different trade-offs in conciseness and readability. Let's explore a few alternatives.

Alternative 1: Positional Destructuring

Clojure's destructuring capabilities are powerful. We can pull apart the input vector directly in the function's argument list. This can make the code more concise.


(defn value-destructuring
  "A more concise version using vector destructuring."
  [[color1 color2 & _]]
  (let [val1 (color-codes color1)
        val2 (color-codes color2)]
    (Integer/parseInt (str val1 val2))))

How it works:

  • [[color1 color2 & _]]: This pattern in the argument list tells Clojure to expect a vector. It binds the first element to the name color1, the second to color2, and the rest of the elements (if any) are ignored (& _).
  • This approach is very direct. It explicitly names the two things it cares about and discards the rest.

Alternative 2: The Mathematical Approach

Instead of converting to a string and back, we can use simple arithmetic to combine the two digits. The first digit represents the "tens" place, and the second represents the "ones" place.


(defn value-math
  "A version using arithmetic instead of string concatenation."
  [colors]
  (let [[val1 val2] (map color-codes (take 2 colors))]
    (+ (* 10 val1) val2)))

How it works:

  • (let [[val1 val2] ...]): Here we use destructuring inside a let block. The result of (map color-codes (take 2 colors)) is a sequence of two numbers. We bind the first number to val1 and the second to val2.
  • (+ (* 10 val1) val2): This is the core logic. If val1 is 6 and val2 is 8, this becomes (+ (* 10 6) 8) -> (+ 60 8) -> 68.
  • This method can be slightly more performant as it avoids the overhead of string creation and parsing.

Data Transformation Flow (Mathematical Approach)

    ● Input Vector
    │ ["blue", "grey"]
    ▼
  ┌──────────────────┐
  │ take 2           │
  └─────────┬────────┘
            │
            ▼
    '("blue", "grey")
            │
  ┌─────────────────────────────┐
  │ map color-codes             │
  └────────────┬────────────────┘
               │
               ▼
           '(6, 8)
               │
  ┌─────────────────────────────┐
  │ Destructure into val1, val2 │
  └────────────┬────────────────┘
               │
               ▼
    val1=6     val2=8
       ╲         ╱
        ╲       ╱
  ┌───────▼───────┐
  │ (+ (* 10 6) 8)│
  └───────┬───────┘
          │
          ▼
    ● Output Integer (68)

Pros and Cons of Different Approaches

Approach Pros Cons
Let-binding & String Concat Extremely readable for beginners; each step is named and clear. Slightly more verbose; minor performance overhead from string conversion.
Positional Destructuring Very concise and idiomatic; clearly states input requirements. Can be less readable for those unfamiliar with destructuring syntax.
Mathematical Approach Potentially higher performance; avoids string manipulation entirely. The intent (combining digits) might be slightly less obvious than direct string concatenation.

For this particular problem, all three approaches are excellent. The "best" one often comes down to team preference and the specific context. The first approach is fantastic for teaching, while the second and third are what you might see more often in experienced Clojure codebases.


Where These Concepts Apply in the Real World

Mastering this simple exercise from the kodikra learning path unlocks patterns used in complex, real-world applications.

  • Configuration Management: Imagine a configuration file where you map string keys (like "DATABASE_URL" or "API_TIMEOUT") to specific values. A hash map is the natural way to load and access this configuration data.
  • Data Transformation (ETL): In Extract, Transform, Load (ETL) pipelines, you constantly transform data from one format to another. The (map f collection) pattern is the bread and butter of such systems, for everything from cleaning data to enriching it with lookups.
  • Parsing and Compilers: When parsing a language or data format, you often need to map tokens or keywords to internal representations or functions. The resistor color map is a micro-version of a symbol table in a compiler.
  • Web Development: Handling JSON API responses often involves working with nested maps and vectors. The skills of destructuring, mapping, and sequence processing are directly transferable to manipulating complex data from web services.

Frequently Asked Questions (FAQ)

Why use a hash map instead of a `case` or `cond` statement?
A hash map treats the mapping as data. This is more flexible and scalable. If you add a new color, you only change the `color-codes` map. With `case` or `cond`, you'd have to modify the function's code, mixing data and logic. Data-driven approaches are almost always preferred in Clojure.
In the destructuring example `[color1 color2 & _]`, what does `& _` mean?
The ampersand (`&`) indicates that the next symbol will collect all *remaining* items in the sequence into a list. The underscore (`_`) is a conventional name in Clojure for a value that you are required to bind but intend to ignore. So, `& _` means "collect the rest of the items, but I don't care about them."
Is `Integer/parseInt` the only way to convert a string to a number in Clojure?
No, but it's the most common for integers. For more general-purpose number parsing (including decimals), you can use `clojure.edn/read-string`. However, for this specific case where you know the input is a valid integer representation, `Integer/parseInt` is direct and efficient.
How would this solution change if we needed the first three colors?
It's a minimal change, which highlights the beauty of this approach. You would simply change `(take 2 colors)` to `(take 3 colors)`. The rest of the logic (`map`, `apply str`, `parseInt`) would work exactly the same, producing a three-digit number. This demonstrates the flexibility of the solution.
Can I use keywords instead of strings for the colors (e.g., `:black` instead of `"black"`)?
Absolutely! Using keywords is often more idiomatic and performant in Clojure for map keys. If you controlled the input, you would define your map as `{:black 0, :brown 1, ...}`. Keywords are faster for lookups and use less memory than strings. We used strings here to match the problem's specified input format.
Why is immutability important for the `color-codes` map?
Because `color-codes` is immutable, we can safely share it across different parts of our program—even across different threads—without any fear that it will be changed unexpectedly. This eliminates a whole class of bugs common in other languages and is a cornerstone of Clojure's design for concurrency.
What is the difference between `map` and `for` in this context?
While both can be used for transformations, `map` is designed for a 1:1 transformation of a sequence. `for` is a list comprehension macro that is more powerful, allowing for filtering (`:when`), nested loops (`:let`), and more complex transformations. For simply applying one function to every item, `map` is the more direct and idiomatic choice.

Conclusion: More Than Just a Solution

We've thoroughly deconstructed the Resistor Color Duo challenge. We didn't just find an answer; we explored the Clojure way of solving problems. The key takeaway is the "data-oriented" mindset: structure your information in a suitable data structure (the hash map) and then use powerful, general-purpose functions (take, map, apply) to transform it. This approach leads to code that is not only concise but also remarkably flexible and easy to reason about.

By mastering these core concepts on a small scale, you are building the foundation needed to tackle much larger and more complex software systems in Clojure. The elegance you see here scales beautifully.

Disclaimer: All code examples are written for Clojure 1.11+ and rely on standard Java interoperability available in modern JVMs. The core concepts, however, are fundamental to the Clojure language and have been stable for many years.

Ready to tackle the next challenge? Continue your journey in the Kodikra Clojure learning path and build upon these foundational skills. To dive deeper into the language itself, explore our comprehensive Clojure guides.


Published by Kodikra — Your trusted Clojure learning resource.