Sum Of Multiples in Clojure: Complete Solution & Deep Dive Guide

a close up of a computer screen with code on it

Mastering Sum of Multiples in Clojure: A Functional Approach

Calculating the sum of unique multiples for a given set of factors under a specific limit is a classic programming challenge. In Clojure, this task is elegantly solved by leveraging its powerful functional constructs, primarily using functions like range, reduce, and some to create a concise, readable, and efficient solution without manual loops or mutable state.


The Quest for Energy Points: A Developer's Challenge

Imagine you're a developer for a massively popular online fantasy game. In this world, players complete perilous levels, and upon completion, they are awarded "energy points." This isn't just a simple reward; the calculation is intricate, tied to the magical items they discovered. Each item has a secret base value, and the final energy score is the sum of all unique multiples of these base values up to the level number the player just conquered.

You're tasked with building this calculation logic. A player finishes level 20, having found items with base values of 3 and 5. Your code must find all multiples of 3 below 20 (3, 6, 9, 12, 15, 18) and all multiples of 5 below 20 (5, 10, 15). The critical rule? A number that is a multiple of both, like 15, can only be counted once. The final sum should be 78.

This might seem straightforward, but it presents a common pain point for developers: how do you handle the "uniqueness" constraint efficiently? A naive approach with nested loops and a list to track numbers already added can quickly become complex, bug-prone, and slow. This is where the functional paradigm, and Clojure specifically, doesn't just offer a solution—it offers a more elegant and powerful way of thinking.


What is the Sum of Multiples Problem?

At its core, the "Sum of Multiples" problem is a mathematical and computational puzzle that asks for the sum of a set of unique numbers. These numbers are derived from a list of factors and must be below a given upper limit.

Let's formally break it down with the rules from our game scenario:

  • A Limit: A positive integer, which we'll call n. In our story, this is the level number. We only consider numbers less than n.
  • A Set of Factors: A list of one or more positive integers. In our story, these are the base values of the magical items. Let's call this list multiples.
  • The Goal: Find every number from 1 up to n-1 that is a multiple of at least one of the factors in the multiples list. Then, calculate the sum of these unique numbers.

A Concrete Example

To ensure we fully grasp the requirements, let's walk through our example again with precision.

  • Limit n: 20
  • Factors multiples: [3, 5]

Step 1: Find multiples of the first factor (3) below 20.
The numbers are: 3, 6, 9, 12, 15, 18.

Step 2: Find multiples of the second factor (5) below 20.
The numbers are: 5, 10, 15.

Step 3: Combine the lists and find the unique numbers.
Combined list: [3, 6, 9, 12, 15, 18, 5, 10, 15].
Unique set: {3, 5, 6, 9, 10, 12, 15, 18}. Notice how 15 appears only once.

Step 4: Sum the unique numbers.
3 + 5 + 6 + 9 + 10 + 12 + 15 + 18 = 78.

The challenge lies in implementing Step 3 efficiently. Simply generating all multiples and then filtering for duplicates can be memory-intensive if the limit n is very large. Clojure provides tools to solve this without creating large intermediate collections.


Why Clojure is Perfectly Suited for This Task

Clojure, a modern Lisp dialect that runs on the Java Virtual Machine (JVM), is built on principles that make it exceptionally good at solving problems like this. Its strength lies in its functional-first philosophy, which emphasizes data transformation over state mutation.

Key Clojure Concepts at Play

  • Immutability: In Clojure, data structures are immutable by default. When you "add" an item to a list, you're actually creating a new list. This prevents a whole class of bugs related to state being changed unexpectedly and makes concurrent programming vastly simpler.
  • Functions as First-Class Citizens: Functions can be passed as arguments to other functions, returned from functions, and stored in variables. This allows for powerful abstractions and composition.
  • Sequence Abstraction: Clojure has a powerful abstraction for sequences (anything you can iterate over). A rich library of functions like map, filter, and reduce operate on these sequences, allowing you to build data processing pipelines declaratively.
  • Declarative vs. Imperative: Instead of telling the computer how to do something with loops and counters (imperative), you describe what you want as a result (declarative). The "Sum of Multiples" solution in Clojure reads like a description of the problem itself.

By leveraging these features, we can build a solution that is not only correct but also concise, readable, and highly performant.


How to Implement the Solution: A Deep Code Walkthrough

Let's analyze the idiomatic Clojure solution provided in the kodikra.com exclusive curriculum. We will break it down piece by piece to understand the functional thinking behind it.

(ns sum-of-multiples)

(defn- divides?
  ([n] (fn [d] (divides? n d)))
  ([n d] (zero? (rem n d))))

(defn sum-of-multiples [multiples n]
  (reduce
    (fn [sum x]
      (cond-> sum
        (some (divides? x) multiples) (+ x)))
    0
    (range 1 n)))

Part 1: The Helper Function - divides?

The solution starts with a private helper function, divides?. The defn- macro makes it private to the current namespace.

(defn- divides?
  ([n] (fn [d] (divides? n d)))
  ([n d] (zero? (rem n d))))

This function is a brilliant example of Clojure's flexibility. It has two arities (versions with different numbers of arguments):

  • The 2-arity version ([n d]): This is the straightforward implementation. It takes a number n and a divisor d. It calculates the remainder of n divided by d using (rem n d) and checks if it's zero using zero?. It returns true if d divides n evenly, and false otherwise.
  • The 1-arity version ([n]): This is where the functional magic happens. This version takes only one argument, n. Instead of returning a boolean, it returns another function. This is a concept called currying or creating a higher-order function. The returned anonymous function (fn [d] ...) "remembers" the value of n and is waiting for the divisor d. When it finally receives d, it calls the 2-arity version of divides?.

Why is this useful? It allows us to create a specialized "divisibility checker" on the fly. For example, (divides? 10) returns a function that checks if its argument divides 10 evenly. We can then pass this specialized function to other functions like some, which is exactly what happens in the main solution.

Part 2: The Main Function - sum-of-multiples

This is the core of the logic. It uses reduce, one of the most fundamental functions in functional programming.

(defn sum-of-multiples [multiples n]
  (reduce
    (fn [sum x]
      (cond-> sum
        (some (divides? x) multiples) (+ x)))
    0
    (range 1 n)))

Let's dissect the reduce call:

  • (range 1 n): This generates a lazy sequence of numbers from 1 up to (but not including) n. For n=20, this is (1, 2, 3, ..., 19). This sequence is the collection we are "reducing".
  • 0: This is the initial value of our accumulator, which we've named sum in our reducing function.
  • (fn [sum x] ...): This is the reducing function. It's called for every number in the sequence. It takes two arguments: the accumulated value so far (sum) and the current item from the sequence (x). It must return the new accumulated value.

The Logic Inside the Reducing Function

This is where the problem is truly solved.

(cond-> sum
  (some (divides? x) multiples) (+ x))
  • cond->: This is a threading macro. It starts with an initial value (sum) and threads it through a series of clauses. If a clause's test expression is true, it applies the transformation to the value. If false, it passes the value through unchanged.
  • The Test Expression: (some (divides? x) multiples). This is the heart of the uniqueness check.
    • (divides? x): Here we use the 1-arity version of our helper! For a given number x from our range (e.g., 15), this creates a temporary function that checks if a number divides 15.
    • some: This function takes a predicate function and a collection. It applies the predicate to each item in the collection and stops, returning a truthy value as soon as the predicate returns true. If it gets through the whole collection without the predicate returning true, it returns nil (which is falsy).
    • Putting it together: (some (divides? x) multiples) asks the question: "Is there at least one number in our `multiples` list (e.g., [3, 5]) that divides `x` evenly?" This is a highly efficient way to check for divisibility against a set of factors.
  • The Transformation: (+ x). This is only applied if the test expression is true. It's a shorthand for (fn [current-sum] (+ current-sum x)). It takes the current value of sum and adds x to it.

Visualizing the Flow

The logic processes each number from 1 to n-1 one by one, deciding whether to add it to the running total. This avoids creating any large intermediate lists of multiples, making it very memory-efficient.

    ● Start with sum = 0, range = (1, 2, ..., n-1)
    │
    ▼
  ┌───────────────────┐
  │ Take next number `x`│
  │ from the range    │
  └─────────┬─────────┘
            │
            ▼
  ◆ Is `x` a multiple of ANY item in `[factors]`?
  │ (Uses `some (divides? x) multiples)`)
  │
  ├─────────────┐
  │             │
 Yes            No
  │             │
  ▼             ▼
┌─────────┐   ┌─────────┐
│ sum = sum + x │   │ sum remains │
│ (cond->)      │   │ unchanged │
└─────────┘   └─────────┘
  │             │
  └──────┬──────┘
         │
         ▼
  ◆ Any numbers left in range?
 ╱                         ╲
Yes (Loop back)          No
 │                         │
 ▼                         ▼
[Repeat with next number]   ● Return final `sum`

An Alternative Approach: Generate, Unify, and Sum

While the reduce solution is elegant and memory-efficient, Clojure's rich standard library offers other ways to think about the problem. An alternative strategy is to generate all possible multiples first, use a set to automatically handle uniqueness, and then sum the result.

This approach is often more direct and can be easier to read for those new to functional programming. It explicitly follows the steps we outlined earlier: generate, unify, sum.

The Code: Using mapcat and set

(defn sum-of-multiples-set-based [multiples n]
  (if (empty? multiples)
    0
    (->> multiples
         (mapcat (fn [m] (when (pos? m) (range m n m))))
         (into #{})
         (reduce +))))

Code Walkthrough for the Set-Based Solution

This version uses the thread-last macro ->> to create a clean data transformation pipeline. Let's follow the data:

  1. multiples: The initial data is our list of factors, e.g., [3, 5]. The outer if handles the edge case of an empty list of multiples.
  2. (mapcat (fn [m] ...)): mapcat is a combination of map and concat. It applies a function to each item in a collection and concatenates the resulting sequences into a single, flat sequence.
    • (when (pos? m) (range m n m)): For each factor m, we first check if it's positive to avoid errors with range. Then we use the 3-arity version of range: (range start end step). So, (range 3 20 3) generates (3, 6, 9, 12, 15, 18). This is a direct way to generate all multiples of a number up to a limit.
    • After mapcat, we have a single lazy sequence of all multiples, including duplicates: (3, 6, 9, 12, 15, 18, 5, 10, 15).
  3. (into #{}): This is the key to uniqueness. into pours the elements of our sequence into the provided collection. By providing an empty set literal #{}, we collect all the numbers into a set. Sets, by their mathematical definition, cannot contain duplicates. The result is #{3 5 6 9 10 12 15 18}.
  4. (reduce +): Finally, we use the simplest form of reduce. When given just a function (like +) and a collection, it uses the function to sum all the elements. This calculates our final result of 78.

Visualizing the Set-Based Flow

This approach feels more like a factory assembly line, where data is transformed at each station.

    ● Start with factors = [3, 5], limit = 20
    │
    ▼
  ┌──────────────────────────────┐
  │ `mapcat` over `[3, 5]`       │
  └──────────────┬───────────────┘
                 │
  ╭──────────────┴──────────────╮
  │                             │
  ▼                             ▼
┌───────────────┐           ┌──────────────────┐
│ Generate for 3:       │           │ Generate for 5:    │
│ (3, 6, 9, 12, 15, 18) │           │ (5, 10, 15)      │
└───────────────┘           └──────────────────┘
  │                             │
  ╰──────────────┬──────────────╯
                 │
                 ▼
┌───────────────────────────────────┐
│ Concatenated Sequence:            │
│ (3, 6, ..., 18, 5, 10, 15)        │
└─────────────────┬─────────────────┘
                  │
                  ▼
┌───────────────────────────────────┐
│ `(into #{})` ⟶ Convert to Set    │
│ Result: #{3 5 6 9 10 12 15 18}    │
└─────────────────┬─────────────────┘
                  │
                  ▼
┌───────────────────────────────────┐
│ `(reduce +)` ⟶ Sum the elements  │
│ Result: 78                        │
└─────────────────┬─────────────────┘
                  │
                  ▼
               ● End

Where and When to Apply These Patterns

The "Sum of Multiples" logic, while presented in a game context, is a fundamental pattern in data processing that appears in numerous real-world scenarios.

Real-World Applications

  • Data Analytics & Reporting: Imagine filtering a massive dataset of user activities. You might need to find all users whose engagement score is a multiple of 7 or 11 to include them in a special cohort for A/B testing.
  • Financial Systems: Calculating fees, taxes, or interest that apply on specific intervals. For example, flagging all transactions that are exact multiples of $100 for fraud review.
  • System Scheduling: In cron jobs or system schedulers, you might have tasks that need to run every 3 hours and other tasks that run every 5 hours. This logic helps determine the unique hours when at least one task will run.
  • Signal Processing: Identifying frequencies in a signal that are harmonics (multiples) of a fundamental frequency.

Pros and Cons: Choosing the Right Approach

Both solutions are correct and idiomatic, but they have different performance characteristics. Choosing between them depends on the constraints of your specific problem.

Aspect reduce / some Approach mapcat / set Approach
Memory Usage Excellent (Low). Processes one number at a time (from range). No large intermediate collections are created. Ideal for extremely large limits (n). Potentially High. Creates an intermediate sequence of all multiples (which can be large) before creating the final set. Can cause memory issues if n is huge.
CPU Usage Potentially higher. It iterates through every number from 1 to n-1 and performs a divisibility check for each. Potentially lower. It directly generates only the numbers that are multiples, avoiding checks on numbers like 2, 4, 7, etc. The number of operations is tied to the number of multiples, not the limit n.
Readability More abstract. Requires understanding of reduce, currying, and higher-order functions. Highly expressive for experienced functional programmers. More direct and declarative. The pipeline `multiples -> all multiples -> unique multiples -> sum` is very clear and easy to follow.
Best For Scenarios with a very large limit n and a relatively small number of factors, where memory is the primary concern. Scenarios where n is reasonably sized and readability is paramount. Often faster if the factors are large, as fewer numbers need to be generated and checked.

For most typical use cases, the performance difference will be negligible. The choice often comes down to which style you and your team find more readable and maintainable.


Frequently Asked Questions (FAQ)

What happens if the list of factors is empty?
The original reduce solution gracefully handles this. (range 1 n) will be the collection, but the test (some (divides? x) []) will always be false, so nothing is ever added to the initial sum of 0. The result is correctly 0. Our set-based solution includes an explicit check for this case.
How do the solutions handle a factor of 0 in the input list?
This is an important edge case. The reduce/some solution's (rem x 0) would throw an ArithmeticException. The set-based solution's (range 0 n 0) would also throw an exception. A robust, production-ready version should filter out non-positive factors from the input list first, as we did in our refined set-based example with (when (pos? m) ...).
What if the input list of factors contains duplicates, like [3, 5, 3]?
Both solutions handle this perfectly. The some function will simply find the first 3 and short-circuit, so the duplicate has no effect. The mapcat/set approach will generate multiples of 3 twice, but the (into #{}) step will eliminate all duplicates anyway.
Are these solutions "lazy"?
Yes, Clojure's sequence functions are generally lazy. In both solutions, range creates a lazy sequence, meaning it doesn't generate all the numbers at once. In the reduce solution, numbers are realized one by one. In the mapcat solution, the intermediate sequence of all multiples is also lazy. The first point where the entire sequence must be realized is at the (into #{}) step, which needs all values to build the set.
Which solution is more "idiomatic" Clojure?
Both are highly idiomatic. The reduce approach showcases the power of higher-order functions and accumulators, a core FP pattern. The mapcat/set approach showcases the power of lazy sequence transformations and pipelines, another cornerstone of Clojure development. The choice often reflects a preference for a specific style of functional composition.
Could this be parallelized for better performance on huge numbers?
Absolutely. For extremely large computations, you could explore Clojure's reducers library or use parallel processing constructs like pmap. The problem can be broken down by splitting the range of numbers to check (e.g., one thread checks 1-1,000,000, another checks 1,000,001-2,000,000) and then summing the results, though care must be taken to handle uniqueness across threads.
What is the time complexity of these solutions?
Let N be the limit and k be the number of factors. The `reduce`/`some` solution has a time complexity of roughly O(N * k) because for each of the `N` numbers, it might check up to `k` factors. The `mapcat`/`set` solution's complexity is harder to pin down but is related to the sum of N/m for each factor `m` in the list (the number of multiples generated), plus the cost of building the set. For small `k`, the set-based approach is often faster.

Conclusion: The Functional Advantage

We've journeyed from a simple game mechanic to a deep exploration of functional programming in Clojure. The Sum of Multiples problem, while seemingly simple, serves as a perfect canvas to illustrate the power and elegance of thinking in terms of data transformations rather than manual, stateful loops.

You've learned how to leverage higher-order functions like reduce and some for memory-efficient processing, and how to build clean, readable data pipelines with mapcat and sets. Understanding the trade-offs between these approaches—memory vs. CPU, directness vs. abstraction—is a key skill that marks the transition from a novice to an expert developer.

The beauty of the Clojure way is that the resulting code is not just a solution; it's a clear, declarative statement of the problem itself. This leads to software that is easier to reason about, maintain, and extend.

Disclaimer: The code examples in this article are written using Clojure 1.11.x and are compatible with modern Java 21+ environments. The core functional principles discussed are fundamental and will remain relevant across future versions of Clojure and other functional languages.

Ready to apply these concepts to more complex challenges? Continue your journey by exploring the next module in our Clojure Learning Path.

For a broader look at the language's capabilities, be sure to visit our complete Clojure programming guide.


Published by Kodikra — Your trusted Clojure learning resource.