Strain in Clojure: Complete Solution & Deep Dive Guide

a close up of a computer screen with code on it

Clojure Functional Filtering: The Definitive Guide to Keep and Discard

Implementing keep and discard in Clojure is a fundamental exercise in functional programming. It involves creating higher-order functions that accept a collection and a predicate, returning a new, filtered collection without mutating the original, perfectly illustrating Clojure's core principles of immutability and declarative data transformation.

Have you ever found yourself tangled in nested loops and complex if-else chains, just to separate elements in a list? It’s a common scenario in imperative programming that often leads to repetitive, error-prone, and hard-to-read code. You write a block to handle one condition, then copy-paste and invert it for the opposite case. This approach feels clunky and inefficient. What if you could express this logic in a single, elegant line? This is the promise of functional programming in Clojure, and mastering the "strain" pattern—implementing your own keep and discard functions—is your gateway to writing cleaner, more powerful code.


What Are Functional Filtering Operations Like Keep and Discard?

At their core, keep and discard are powerful data transformation tools. They are not just functions; they represent a fundamental concept in functional programming: treating functions as first-class citizens that can be passed as arguments to other functions. These are known as higher-order functions.

Let's break down the components:

  • The Collection: This is the input data you want to filter. In Clojure, this is typically a sequence like a list '(1 2 3), a vector [1 2 3], or even a map or set. The beauty of Clojure's sequence abstraction is that our functions will work on any of them.
  • The Predicate: This is a special type of function that takes a single element from your collection and returns a boolean value—true or false. It's the "rule" or "test" that determines whether an element belongs. Examples include built-in functions like even?, odd?, string?, or a custom anonymous function like #(> % 10) which checks if a number is greater than 10.

With these two inputs, the operations are straightforward:

  • keep: It iterates through the collection, applies the predicate to each element, and builds a new collection containing only the elements for which the predicate returned true.
  • discard: It does the opposite. It iterates through the collection, applies the predicate, and builds a new collection containing only the elements for which the predicate returned false.

The most critical aspect here is immutability. Neither keep nor discard changes the original collection. They always return a brand new collection, leaving the original data untouched. This is a cornerstone of Clojure that prevents a whole class of bugs related to side effects and shared mutable state, making code safer and easier to reason about.


Why is Mastering This Pattern Crucial for Clojure Developers?

Understanding how to build functions like keep and discard from the ground up is more than just an academic exercise. It unlocks a deeper understanding of the "Clojure way" of thinking about problems, which emphasizes data transformation pipelines over step-by-step imperative instructions.

Embracing Declarative Code

Instead of telling the computer how to loop, how to check a condition, and how to add to a new list (imperative style), you simply declare what you want: "keep the even numbers." This makes your code more readable, concise, and closer to the business logic it represents.


; Imperative-style thinking (conceptual)
let results = []
for each number in my_list:
  if is_even(number):
    add number to results
return results

; Declarative Clojure style
(keep even? my-list)

Building Reusable Tools

By separating the "what to do" (the iteration logic in keep) from the "rule" (the predicate), you create an incredibly reusable tool. The same keep function can be used to find active users, valid transactions, or prime numbers, simply by passing in a different predicate function. This is the power of higher-order functions in action.

Understanding Core Mechanics

While Clojure has built-in functions like filter (which is like keep) and remove (like discard), building them yourself forces you to engage with fundamental concepts like recursion, tail-call optimization with loop/recur, and the accumulator pattern. This knowledge is invaluable when you need to write more complex data processing functions later on.

Here is a conceptual flow of how the keep operation works. Think of the predicate as a gatekeeper that only lets certain elements pass through.

  ● Start with a Collection
  │ [1, 2, 3, 4, 5]
  │
  ▼
┌──────────────────┐
│  Apply Predicate │
│   `even?` to 1   │
└────────┬─────────┘
         │
         ▼
    ◆ is 1 even?
   ╱           ╲
  No            Yes
  │              │
  ▼              ▼
[Discard]    [Add to Result]
  │
  ▼
┌──────────────────┐
│  Apply Predicate │
│   `even?` to 2   │
└────────┬─────────┘
         │
         ▼
    ◆ is 2 even?
   ╱           ╲
  No            Yes
  │              │
  ▼              ▼
[Discard]    [Add to Result]
  │              [2]
  ▼
  ... continue for all elements ...
  │
  ▼
  ● Final Result
    [2, 4]

How to Implement Keep and Discard from Scratch in Clojure?

Now, let's get our hands dirty and build these functions. We will follow idiomatic Clojure practices, using loop/recur to ensure our functions are efficient and don't consume the call stack, which prevents StackOverflowError on large collections.

This implementation is part of an exclusive learning module from kodikra.com, designed to build a strong foundation in functional programming.

The Complete Solution Code

Here is the full implementation within a `strain` namespace. We will break it down piece by piece afterward.


(ns strain)

(defn keep
  "Given a predicate and a collection, returns a new collection containing
  only the elements for which the predicate returns a truthy value."
  [pred coll]
  ;; Use loop/recur for tail-call optimization. This is the idiomatic
  ;; way to perform recursion in Clojure without blowing the stack.
  (loop [remaining coll
         result []]
    ;; Base Case: If the remaining collection is empty, we are done.
    (if (empty? remaining)
      ;; Return the accumulated result.
      result
      ;; Recursive Step:
      (let [item (first remaining)
            rest-coll (rest remaining)]
        ;; Apply the predicate to the current item.
        (if (pred item)
          ;; If predicate is true, recur with the item added to our result.
          (recur rest-coll (conj result item))
          ;; If predicate is false, recur without modifying the result.
          (recur rest-coll result))))))

(defn discard
  "Given a predicate and a collection, returns a new collection containing
  only the elements for which the predicate returns a falsy value."
  [pred coll]
  ;; This is the most elegant way to implement discard.
  ;; We reuse our `keep` function but pass it an inverted predicate.
  ;; `complement` is a built-in Clojure function that takes a function
  ;; and returns a new function that returns the opposite boolean value.
  (keep (complement pred) coll))

Detailed Code Walkthrough

1. The `keep` Function

The keep function is the heart of our implementation. It uses a common and powerful pattern in Clojure: the accumulator pattern with loop/recur.

  • Function Signature: (defn keep [pred coll])

    We define a function named keep that accepts two arguments: pred (the predicate function) and coll (the input collection).

  • The `loop` Binding: (loop [remaining coll result []])

    loop establishes a recursion point. It's not a traditional loop like in Java or Python. It initializes two local bindings: remaining is set to the initial collection, and result is an empty vector [] that will act as our accumulator, collecting the items that pass the test.

  • The Base Case: (if (empty? remaining) result ...)

    Every recursive process needs a stopping condition. Here, we check if the remaining collection is empty. If it is, we have processed all elements, and we simply return the final accumulated result.

  • The Recursive Step:

    If remaining is not empty, we proceed to the main logic. (let [item (first remaining) rest-coll (rest remaining)] ...) We use a let binding to grab the first element of the collection (item) and the rest of the collection (rest-coll).

    (if (pred item) ...)

    Here's where the magic happens. We call the predicate function pred with the current item. The if statement checks if the result is truthy (anything other than false or nil).

    • If `true`: (recur rest-coll (conj result item))

      The predicate passed! We call recur to jump back to the loop point. We update the bindings: remaining becomes rest-coll, and crucially, result becomes a new vector with item added to it using conj (conjoin).

    • If `false`: (recur rest-coll result)

      The predicate failed. We still call recur to continue the process with rest-coll, but we pass the result accumulator along unchanged.

2. The `discard` Function

We could have implemented `discard` by copying the code for `keep` and inverting the `if` condition: `(if (not (pred item)) ...)`. But in functional programming, we strive for composition and reuse.

(defn discard [pred coll] (keep (complement pred) coll))

This implementation is far more elegant and idiomatic.

  • complement: This is a built-in Clojure higher-order function. It takes a predicate function as an argument and returns a new function. This new function, when called, will call the original predicate and return its logical opposite.
  • Composition: So, (complement even?) creates a new function that is effectively odd?. By passing this new, inverted predicate to our existing keep function, we achieve the `discard` behavior without writing any new iteration logic. This is a perfect example of the power of functional composition.

Running the Code from the Terminal

Assuming you have a project set up with Leiningen or `deps.edn`, you can test this code. For example, in a test file:


(ns strain-test
  (:require [clojure.test :refer [deftest is]]
            [strain :refer [keep discard]]))

(deftest keep-test
  (is (= [2 4 6] (keep even? [1 2 3 4 5 6]))))

(deftest discard-test
  (is (= [1 3 5] (discard even? [1 2 3 4 5 6]))))

You can run the tests using your build tool:


# Using Leiningen
lein test

# Using Clojure CLI tools
clj -M:test

Where Can You Apply This Filtering Pattern? (Real-World Examples)

This pattern of filtering collections with predicates is ubiquitous in software development. Here are a few practical scenarios where keep and discard shine.

1. User Data Processing

Imagine you have a list of user maps and you want to find all the active administrators.


(def users
  [{:name "Alice" :role :admin :active? true}
   {:name "Bob" :role :user :active? true}
   {:name "Charlie" :role :admin :active? false}
   {:name "David" :role :user :active? true}])

(defn admin? [user] (= (:role user) :admin))
(defn active? [user] (:active? user))

;; Get all active admins
(keep #(and (admin? %) (active? %)) users)
;;=> [{:name "Alice", :role :admin, :active? true}]

;; Get all non-admin users
(discard admin? users)
;;=> [{:name "Bob", :role :user, :active? true} {:name "David", :role :user, :active? true}]

2. Sanitizing Input Data

When processing data from external sources like APIs or user forms, you often need to clean it up by removing invalid entries.


(def raw-data ["value1" nil "value2" "" "value3" nil])

;; Discard all nil or empty string values
(discard #(or (nil? %) (.isEmpty %)) raw-data)
;;=> ["value1" "value2" "value3"]

3. Financial Transaction Analysis

Suppose you have a list of transactions and you need to separate them into income and expenses.


(def transactions
  [{:id 1 :amount 100.00}
   {:id 2 :amount -25.50}
   {:id 3 :amount 300.00}
   {:id 4 :amount -15.00}])

(defn income? [tx] (pos? (:amount tx)))

;; Keep only the income transactions
(keep income? transactions)
;;=> [{:id 1, :amount 100.0} {:id 3, :amount 300.0}]

;; Discard income transactions to get expenses
(discard income? transactions)
;;=> [{:id 2, :amount -25.5} {:id 4, :amount -15.0}]

When to Use Built-in Functions vs. Your Custom Implementation?

As mentioned earlier, Clojure's core library already provides highly optimized functions for this exact purpose: filter (equivalent to our keep) and remove (equivalent to our discard). So, when should you use one over the other?

The primary reason to build them yourself, as we've done in this kodikra learning path module, is for learning. It solidifies your understanding of recursion, higher-order functions, and immutability. In professional, day-to-day Clojure development, you should almost always prefer the built-in functions.

Here’s a breakdown of the pros and cons:

Aspect Custom keep/discard (This Article) Built-in filter/remove
Learning Value Excellent. Forces you to understand the underlying mechanics. Minimal. Abstracts away the implementation details.
Performance Good, but eager. It builds the entire result collection in memory immediately. Excellent and Lazy. Returns a "lazy sequence" that computes elements only as they are needed. This is far more memory-efficient for large or infinite sequences.
Idiomatic Code Not idiomatic for production code. It's a learning implementation. Highly idiomatic. This is the standard, expected way to filter collections in Clojure.
Conciseness More verbose as you need to include the function definitions. Extremely concise. Part of clojure.core, always available.
Flexibility Works on standard collections. Works on the full range of Clojure's sequence abstraction, including transient collections for performance-critical code.

The Power of Laziness

The most significant difference is laziness. Our `keep` function is eager. If you give it a list of a million items, it will process all one million items and build a new list in memory before returning.

Clojure's `filter` is lazy. When you call `(filter even? large-collection)`, it returns immediately with a lazy sequence object. No computation happens until you actually try to access an element from that sequence (e.g., by calling `first` or `take`). This allows you to compose complex data transformations on massive (or even infinite!) datasets with minimal memory overhead.

The logic of using `complement` remains a powerful pattern even with built-in functions. `(remove pred coll)` is functionally equivalent to `(filter (complement pred) coll)`.

This diagram illustrates how our `discard` function elegantly composes `complement` and `keep`.

    ● Start with Predicate
    │ `even?`
    │
    ▼
  ┌─────────────┐
  │ `complement`  │
  │  (Wrapper)    │
  └──────┬──────┘
         │
         ▼
    ● Inverted Predicate
    │ `(complement even?)`
    │  which behaves like `odd?`
    │
    ▼
  ┌─────────────┐
  │ Pass to `keep`│
  └──────┬──────┘
         │
         ▼
    ● Final Result
      (Elements where original
       predicate was false)

Frequently Asked Questions (FAQ)

What exactly is a predicate in Clojure?
A predicate is any function that is intended to be used in a boolean context. It takes one or more arguments and returns a value that is interpreted as either "truthy" (anything except `nil` and `false`) or "falsy" (`nil` or `false`). By convention, predicate functions in Clojure often end with a question mark, like even?, nil?, or empty?.
Why use loop/recur instead of a standard recursive function call?
Standard recursion consumes stack space for each call. If you have a collection with thousands of items, a standard recursive function would likely cause a java.lang.StackOverflowError. recur is a special form in Clojure that performs a jump back to a loop or function start without consuming new stack space. This is called tail-call optimization (TCO), and it allows your recursive functions to run in constant stack space, just like a standard loop.
What is the main difference between the built-in filter and our custom keep function?
The primary difference is that our custom keep is "eager," meaning it processes the entire collection and builds the full result immediately. Clojure's built-in filter is "lazy." It returns a lazy sequence, and elements are only computed as they are requested. This makes filter much more memory-efficient for large datasets.
How does the complement function work?
complement is a higher-order function. It takes another function `f` as input and returns a new anonymous function. This new function will take the same arguments as `f`, call `f` with them, and then return the logical `not` of `f`'s result. It's a concise way to invert the logic of any predicate.
Can I use this keep/discard pattern on data structures other than lists and vectors?
Absolutely. The code we wrote works on any data structure that can be treated as a sequence in Clojure. This includes lists, vectors, sets, and even the key-value pairs of a map. This is thanks to Clojure's powerful sequence abstraction, which provides a uniform way to interact with different collection types.
Is this eager approach ever better than the lazy one?
In some specific, performance-critical scenarios where you know the collection is small and you will need every single element of the result immediately, an eager approach can sometimes be slightly faster by avoiding the overhead of lazy sequence creation. However, these cases are rare, and the safety and memory efficiency of laziness make it the better default choice.
What are higher-order functions?
A higher-order function is a function that either takes one or more functions as arguments, returns a function as its result, or both. Our `keep` function is a higher-order function because it takes the `pred` function as an argument. Clojure's `complement` is also a higher-order function because it takes a function and returns a new one.

Conclusion: From Instructions to Intuition

Building your own keep and discard functions is a rite of passage for any developer learning Clojure. It moves you from simply following imperative instructions to thinking in terms of declarative data transformations. You've seen how to implement efficient, stack-safe recursion with loop/recur, how to leverage higher-order functions by passing predicates as arguments, and how to achieve elegance and code reuse through functional composition with complement.

While in your professional projects you will and should use the built-in, lazy, and highly optimized filter and remove functions, the deep understanding gained from this exercise is invaluable. You now know exactly what is happening under the hood, empowering you to write more expressive, robust, and idiomatic Clojure code.

Disclaimer: The code and concepts in this article are based on the latest stable version of Clojure (1.11+). The principles of functional programming discussed are timeless and will remain relevant in future versions.

Ready to tackle the next challenge? Continue your journey on the kodikra Clojure learning path and deepen your functional programming skills. Or, if you're ready for more, explore more advanced Clojure topics on our platform.


Published by Kodikra — Your trusted Clojure learning resource.