Triangle in Clojure: Complete Solution & Deep Dive Guide

Tabs labeled

The Complete Guide to Triangle Classification in Clojure

Learn to classify triangles in Clojure by checking side lengths. This guide covers creating equilateral?, isosceles?, and scalene? functions, validating the triangle inequality theorem, and handling invalid inputs using core Clojure concepts like sets, destructuring, and conditional logic for robust geometric calculations.

Remember that geometry problem from school? The one about triangles. Equilateral, isosceles, scalene—it all seemed so simple on paper. But when you sit down to translate that simple logic into code, the hidden complexities start to surface. How do you handle invalid inputs? What's the most efficient way to check the side lengths? It's a classic programming puzzle that can quickly turn into a messy web of nested if statements if you're not careful.

If you've ever felt that frustration, you're in the right place. This guide will walk you through building a robust, elegant, and idiomatic Clojure solution from the ground up. We'll move beyond basic conditionals and leverage the power of Clojure's data structures to write code that is not only correct but also beautifully concise and readable. By the end, you'll have mastered a fundamental problem and gained a deeper appreciation for the functional programming paradigm.


What is the Triangle Classification Problem?

At its core, the problem is to write a program that takes three numbers representing the lengths of a triangle's sides and determines its type. The classification is based on a few simple geometric rules that we must translate into code.

The Three Triangle Types

  • Equilateral Triangle: A triangle where all three sides are of equal length. For example, sides of (2, 2, 2).
  • Isosceles Triangle: A triangle where at least two sides are of equal length. This includes equilateral triangles by definition. For example, sides of (3, 3, 2) or (4, 4, 4).
  • Scalene Triangle: A triangle where all three sides have different lengths. For example, sides of (3, 4, 5).

The Two Golden Rules of Validity

Before you can even classify a triangle, you must first determine if the given side lengths can form a valid triangle at all. This is a critical step that many developers overlook, leading to bugs and incorrect results. Two fundamental rules govern a triangle's existence:

  1. The Rule of Positive Sides: All sides must have a length greater than zero. A side of length 0 is not a side; it's a point.
  2. The Triangle Inequality Theorem: The sum of the lengths of any two sides of a triangle must be greater than or equal to the length of the third side. For sides a, b, and c, this means all three of these conditions must be true: a + b ≥ c, a + c ≥ b, and b + c ≥ a. This rule prevents "degenerate" triangles that are just flat lines.

Only if a set of three side lengths satisfies both of these rules can we proceed to classify it. Our Clojure code must rigorously enforce these constraints first.


Why is Clojure a Great Fit for This Problem?

Clojure, a modern Lisp dialect running on the JVM, offers a unique and powerful toolset for solving logical problems like this one. Its functional nature encourages you to build solutions from small, pure, and composable functions, which leads to cleaner and more maintainable code.

  • Immutability by Default: Side lengths a, b, and c are passed as arguments and never change. This eliminates a whole class of bugs related to state mutation and makes the logic easier to reason about.
  • Expressive Data Structures: Clojure's built-in data structures are incredibly powerful. As we'll see, using a set to determine the number of unique side lengths provides an exceptionally elegant solution that is far cleaner than a series of nested if-else checks.
  • Rich Core Library: The language provides a wealth of functions for working with collections and sequences. Functions like every?, sort, and operators for creating collections make the code concise and declarative.
  • Predicate Functions: The convention of naming functions that return a boolean with a trailing question mark (e.g., equilateral?) makes the code's intent self-documenting and highly readable.

By leveraging these features, we can write a solution that is not just functional but also idiomatic, showcasing the "Clojure way" of thinking about problems.


How to Structure the Clojure Solution (The Core Logic)

We'll build our solution step-by-step, starting with validation and then moving to classification. This approach of breaking the problem into smaller, manageable functions is central to functional programming.

Step 1: Setting Up the Namespace

Every Clojure file starts with a namespace declaration. This organizes our code and prevents naming conflicts. We'll create a file named triangle.clj.

(ns triangle)

Step 2: The Crucial Validation Logic

Before anything else, we need a robust function to check if three sides can form a triangle. Let's create a helper function, valid-triangle?. A common convention in Clojure is to mark private helper functions with defn-.

This function needs to check our two golden rules: all sides are positive, and the triangle inequality theorem holds. A simple way to check the inequality is to sort the sides first. If we have sorted sides s1, s2, s3 (where s1 is the smallest and s3 is the largest), we only need to check if s1 + s2 ≥ s3. If the sum of the two smaller sides is greater than the largest, the other two conditions are automatically met.

Here is the logic flow for our validation:

    ● Start with sides [a, b, c]
    │
    ▼
  ┌──────────────────┐
  │ Check All Sides > 0 │
  └─────────┬────────┘
            │
            ▼
    ◆ Are they all positive?
   ╱                       ╲
  Yes (Continue)          No (Invalid) ⟶ ● End (false)
  │
  ▼
  ┌──────────────────┐
  │ Sort the sides    │
  │ [s1, s2, s3]      │
  └─────────┬────────┘
            │
            ▼
  ┌──────────────────┐
  │ Check s1 + s2 ≥ s3 │
  └─────────┬────────┘
            │
            ▼
    ◆ Is the inequality true?
   ╱                       ╲
  Yes (Valid)             No (Invalid)
  │                          │
  ▼                          ▼
 ● End (true)             ● End (false)

And here's how that logic translates into Clojure code:

(ns triangle)

(defn- valid-triangle? [a b c]
  (let [[s1 s2 s3] (sort [a b c])]
    (and (> s1 0)
         (>= (+ s1 s2) s3))))

In this snippet, let binds the sorted list of sides to [s1 s2 s3] using destructuring. Then, the and macro ensures both conditions—all sides are positive (checked by seeing if the smallest, s1, is > 0) and the inequality holds—are true.

Step 3: The Elegant Classification Logic with Sets

Now for the fun part. Once we know we have a valid triangle, how do we classify it? We could use a chain of if statements, but there's a more elegant, data-driven way in Clojure.

Consider the number of unique side lengths:

  • If there is only 1 unique side length, it means all three sides are the same. This is an equilateral triangle.
  • If there are 2 unique side lengths, it means two sides are the same and one is different. This is an isosceles triangle.
  • If there are 3 unique side lengths, it means all three sides are different. This is a scalene triangle.

What's the best way to count unique items in a collection in Clojure? A set! A set is a data structure that only stores unique values. If we put our three side lengths into a set, the size of the resulting set will tell us everything we need to know.

Here's the classification flow based on this insight:

    ● Start with valid sides [a, b, c]
    │
    ▼
  ┌───────────────────┐
  │ Create a set from  │
  │ the sides: #{a b c} │
  └──────────┬──────────┘
             │
             ▼
  ┌───────────────────┐
  │ Count unique items │
  │ in the set         │
  └──────────┬──────────┘
             │
             ▼
    ◆ How many unique sides?
   ╱          │           ╲
  1           2            3
  │           │            │
  ▼           ▼            ▼
[Equilateral] [Isosceles] [Scalene]
  │           │            │
  └─────┬─────┴────────────┘
        │
        ▼
     ● End

Step 4: Putting It All Together (The Final Code)

Now we can combine our validation and classification logic to create the final public functions. Each classification function will first call valid-triangle?. If that returns true, it will then apply the set-based logic.

Here is the complete, well-commented code for our triangle.clj file, which is part of the exclusive kodikra.com learning path.

;; Defines the namespace for our triangle classification logic.
(ns triangle)

;; Private helper function to determine if the given sides can form a valid triangle.
;; It checks two conditions:
;; 1. All sides must have a length > 0.
;; 2. The sum of the lengths of any two sides must be greater than or equal to the third side (Triangle Inequality Theorem).
(defn- valid-triangle?
  "Checks if the sides [a, b, c] can form a valid triangle."
  [a b c]
  (let [[s1 s2 s3] (sort [a b c])] ; Sort sides to simplify the inequality check.
    (and
      (> s1 0)                    ; Rule 1: Smallest side must be positive.
      (>= (+ s1 s2) s3))))        ; Rule 2: Sum of two smaller sides must be >= largest side.

(defn- unique-side-count
  "Calculates the number of unique side lengths."
  [a b c]
  (->> [a b c]   ; Start with the list of sides
       (into #{}) ; Convert the list into a set to get unique values
       (count)))  ; Count the number of items in the set

(defn equilateral?
  "Returns true if the triangle with sides [a, b, c] is equilateral."
  [a b c]
  (and
    (valid-triangle? a b c)
    (= 1 (unique-side-count a b c)))) ; An equilateral triangle has 1 unique side length.

(defn isosceles?
  "Returns true if the triangle with sides [a, b, c] is isosceles."
  [a b c]
  (and
    (valid-triangle? a b c)
    (<= (unique-side-count a b c) 2))) ; An isosceles triangle has 1 or 2 unique side lengths.

(defn scalene?
  "Returns true if the triangle with sides [a, b, c] is scalene."
  [a b c]
  (and
    (valid-triangle? a b c)
    (= 3 (unique-side-count a b c)))) ; A scalene triangle has 3 unique side lengths.

Running the Code

You can test this code using the Clojure command-line interface (CLI). Save the code as triangle.clj in a `src` directory. Then, from the root of your project, you can run:

# Test for an equilateral triangle
clj -M -e "(require '[triangle]) (println (triangle/equilateral? 7 7 7))"
# Expected output: true

# Test for an isosceles triangle
clj -M -e "(require '[triangle]) (println (triangle/isosceles? 5 5 3))"
# Expected output: true

# Test for a scalene triangle
clj -M -e "(require '[triangle]) (println (triangle/scalene? 3 4 5))"
# Expected output: true

# Test for an invalid triangle (violates inequality)
clj -M -e "(require '[triangle]) (println (triangle/scalene? 1 1 3))"
# Expected output: false

# Test for an invalid triangle (side is zero)
clj -M -e "(require '[triangle]) (println (triangle/isosceles? 2 2 0))"
# Expected output: false

Where and When to Apply This Logic

Where: Real-World Applications

While a simple exercise, triangle classification and the underlying geometric principles are fundamental in many advanced domains:

  • Computer Graphics & Rendering: 3D models are composed of meshes, which are vast collections of triangles (polygons). Efficiently analyzing these triangles is key to rendering, shading, and collision detection.
  • Game Development: Physics engines constantly calculate interactions between objects, often simplified as triangular meshes. Determining properties of these triangles is essential for realistic physics.
  • CAD and Engineering Software: Computer-Aided Design (CAD) tools for architecture and engineering rely heavily on geometric calculations for structural analysis and design validation.
  • GIS and Mapping: Geographic Information Systems use Triangulated Irregular Networks (TINs) to represent terrain surfaces.
  • Computational Geometry: This entire field of computer science is dedicated to algorithms for geometric shapes, with the triangle being the most basic building block.

When: Considering Alternative Approaches

The set-based approach is elegant and idiomatic in Clojure. However, in some contexts, a more traditional approach using cond might be considered. Let's compare them.

An alternative implementation might look like this:

(defn equilateral-cond? [a b c]
  (and (valid-triangle? a b c)
       (and (= a b) (= b c))))

(defn isosceles-cond? [a b c]
  (and (valid-triangle? a b c)
       (or (= a b) (= b c) (= a c))))

(defn scalene-cond? [a b c]
  (and (valid-triangle? a b c)
       (and (not= a b) (not= b c) (not= a c))))

This works perfectly well, but it's more verbose and arguably less declarative. The logic is spread across multiple equality checks. Let's analyze the trade-offs.

Pros & Cons: Set-Based vs. Conditional Logic

Aspect Set-Based Approach (Idiomatic) Conditional `cond`/`and` Approach (Traditional)
Readability High. The intent of `(count (set [a b c]))` is very clear: "count the unique sides". It's declarative. Moderate. The logic is explicit but requires reading multiple comparisons. Can become noisy.
Conciseness Very concise. The core logic is a single, expressive line. More verbose. Requires multiple `and`/`or` clauses and direct comparisons.
Performance Negligible difference for 3 elements. Creating a set has a small overhead, but it's trivial in this case. Potentially slightly faster as it avoids creating a new data structure, but this is micro-optimization.
Extensibility More extensible. If you needed to find shapes with 4 unique sides from 4 inputs, the pattern holds. Less extensible. The number of comparisons grows quadratically, making it complex for more sides.
Idiomatic Style This is a classic example of "thinking in Clojure" — transforming data to get the answer. More akin to an imperative style, translating a direct "if this then that" thought process.

For this problem in Clojure, the set-based approach is clearly superior. It aligns better with the language's philosophy of data transformation and results in cleaner, more maintainable code.


Detailed Code Walkthrough

Let's dissect the final, idiomatic solution to understand exactly what each piece of code is doing and who is responsible for what.

The `valid-triangle?` Helper Function

(defn- valid-triangle? [a b c]
  (let [[s1 s2 s3] (sort [a b c])]
    (and (> s1 0)
         (>= (+ s1 s2) s3))))
  • (defn- valid-triangle? [a b c]): We define a private function named valid-triangle? that accepts three arguments: a, b, and c. The - in defn- signifies it's intended for internal use within this namespace only.
  • (let [[s1 s2 s3] (sort [a b c])] ...): This is the heart of the validation.
    • [a b c] creates a vector (a list-like collection) from the three side lengths.
    • (sort [...]) takes this vector and returns a new, sorted vector, e.g., [5 2 4] becomes [2 4 5].
    • [s1 s2 s3] is a destructuring form. It plucks the elements out of the sorted vector and binds them to the local names s1, s2, and s3. So, s1 will always be the smallest side and s3 the largest.
  • (and (...) (...)): The and macro evaluates its arguments from left to right. It stops and returns false as soon as it encounters a falsey value. If all arguments are truthy, it returns the value of the last argument.
  • (> s1 0): This is our first check. It ensures the smallest side is greater than 0. If this is true, all sides must be positive.
  • (>= (+ s1 s2) s3): This is our second check, the Triangle Inequality Theorem. It adds the two smallest sides and checks if their sum is greater than or equal to the largest side.

The `equilateral?` Function (and its peers)

(defn equilateral? [a b c]
  (and
    (valid-triangle? a b c)
    (= 1 (unique-side-count a b c))))
  • (defn equilateral? [a b c]): Defines our public function. The trailing ? is a Clojure convention indicating that the function returns a boolean value (a predicate).
  • (and (valid-triangle? a b c) ...): The first and most important step is to call our validator. If (valid-triangle? a b c) returns false, the and macro short-circuits and immediately returns false without even evaluating the second part. This is a powerful and clean way to chain validation.
  • (= 1 (unique-side-count a b c)): This is the classification logic.
    • (unique-side-count a b c) calls our other helper, which calculates the number of unique sides.
    • (= 1 ...) checks if the result of that count is exactly 1. If it is, and the triangle is valid, the function returns true.

The isosceles? and scalene? functions follow the exact same pattern, simply changing the number they compare against (<= 2 for isosceles because it can have 1 or 2 unique sides, and = 3 for scalene).

This structure is a perfect example of building a robust system from small, focused, and reusable parts, a core tenet of good software design and functional programming. To further your understanding, you can explore more modules in the complete Kodikra Clojure guide.


Frequently Asked Questions (FAQ)

1. What is the Triangle Inequality Theorem again?

The Triangle Inequality Theorem is a fundamental rule in geometry stating that the sum of the lengths of any two sides of a triangle must be greater than or equal to the length of the third side. For a triangle with sides a, b, and c, it means a + b ≥ c, a + c ≥ b, and b + c ≥ a must all be true. Our code simplifies this by sorting the sides first, so we only need to check if the sum of the two shortest sides is greater than or equal to the longest side.

2. Why is using a `set` to count unique sides considered idiomatic in Clojure?

It's considered idiomatic because it aligns with Clojure's data-oriented philosophy. Instead of writing a series of imperative instructions (if a equals b, etc.), you perform a data transformation. You transform the input list of sides `[a b c]` into a different data structure, a `set`, which by its very nature discards duplicates. Then, you simply query this new data structure for its property (`count`). This declarative, data-transformation approach is at the core of effective Clojure programming.

3. How would this code handle floating-point numbers for side lengths?

The current code handles floating-point numbers perfectly without any changes. Clojure's arithmetic functions (+, >, >=) and data structures (vector, set) are polymorphic and work with integers, floats, and other number types seamlessly. However, when working with floating-point numbers, one should always be mindful of potential precision issues, though it's unlikely to be a problem for this specific logic.

4. What does the `?` at the end of function names like `equilateral?` mean in Clojure?

In Clojure, the question mark ? at the end of a function name is a community convention, not a syntactic requirement. It signals to other developers that the function is a "predicate"—meaning it is expected to return a boolean value (true or false). This makes the code more self-documenting and easier to read, as you immediately know the function's purpose is to ask a true/false question about its arguments.

5. Is it better to have one function that returns a keyword (e.g., `:equilateral`) or separate predicate functions?

Both approaches are valid and depend on the use case. The approach in this guide (separate predicate functions) is excellent when you need to ask specific true/false questions, like `(if (triangle/isosceles? ...))`. A single function like `(classify-triangle a b c)` that returns `:equilateral`, `:isosceles`, `:scalene`, or `:invalid` is better when you need to act differently based on the type, such as in a `case` or `cond` statement. The current problem in the kodikra learning module asks for separate predicates.

6. How can I test this code using `clojure.test`?

You would create a corresponding test file, e.g., `test/triangle_test.clj`, and use the `clojure.test` framework. Here's a small example:

(ns triangle-test
  (:require [clojure.test :refer :all]
            [triangle :refer :all]))

(deftest equilateral-triangle-test
  (is (equilateral? 2 2 2))
  (is (not (equilateral? 2 3 2))))

(deftest isosceles-triangle-test
  (is (isosceles? 3 3 2))
  (is (isosceles? 4 4 4)) ; Equilateral is also isosceles
  (is (not (isosceles? 2 3 4))))
7. What does the `->>` (thread-last) macro in `unique-side-count` do?

The `->>` macro, often called the "thread-last" macro, is syntactic sugar for restructuring nested function calls. It takes the first argument, `[a b c]`, and "threads" it as the last argument to the subsequent forms. So, the code:

(->> [a b c]
       (into #{})
       (count))

is automatically rewritten by Clojure to be this:

(count (into #{} [a b c]))

This allows you to write a sequence of data transformations in a clear, top-to-bottom pipeline, which is often much easier to read than deeply nested parentheses.


Conclusion: From Geometry to Elegant Code

We've successfully journeyed from basic geometric principles to a complete, idiomatic Clojure solution. The key takeaways from this exercise extend far beyond triangles. We've seen the paramount importance of validating inputs before processing them, a practice that prevents countless bugs. More importantly, we've witnessed the power of Clojure's data-oriented approach, where transforming data into a more suitable representation (a list into a set) can dramatically simplify logic and improve code clarity.

By breaking the problem into small, pure functions like valid-triangle? and unique-side-count, we created a solution that is not only robust and testable but also a pleasure to read. This is the essence of functional programming and the strength you'll continue to build as you explore more challenges in the Kodikra Clojure roadmap.

Technology Disclaimer: The code and concepts discussed in this article are based on Clojure 1.11+. While the core logic is fundamental and unlikely to change, always consult the official documentation for the latest language features and best practices.


Published by Kodikra — Your trusted Clojure learning resource.