# Tooling and Ecosystem Standards for Clojure
This document outlines coding standards specifically related to tooling and ecosystem practices for Clojure development. Following these guidelines will promote consistency, maintainability, and efficiency within Clojure projects. The aim is to equip developers and AI coding assistants with context for generating optimal Clojure code adhering to the latest best practices.
## 1. Project Setup and Dependencies
### 1.1. Project Structure and "deps.edn"
**Standard:** Use "deps.edn" for dependency management and project configuration. Adopt a consistent project structure.
**Do This:**
* Structure your project following established conventions (e.g., "src/", "test/", "resources/").
* Use "deps.edn" to declare dependencies and aliases.
* Keep "deps.edn" well-formatted and organized.
* Define namespaces consistent with the project structure. For instance, a file under "src/my_app/core.clj" should have the namespace "my-app.core".
**Don't Do This:**
* Use "project.clj" (Leiningen) for new projects. "deps.edn" is the standard and recommended approach.
* Mix dependency management approaches (e.g., using "deps.edn" for some dependencies and manual additions for others).
* Create deeply nested or unstructured project layouts.
**Why:** "deps.edn" is Clojure's built-in dependency management tool; it is simpler, configuration-driven, and avoids the heavy plugin ecosystem associated with Leiningen. A consistent structure enables maintainability & tooling support.
**Example "deps.edn":**
"""clojure
{:paths ["src"]
:deps {org.clojure/clojure {:mvn/version "1.11.2"}
org.clojure/core.async {:mvn/version "1.6.681"}
;; Add other dependencies here
}
:aliases {:test {:extra-paths ["test"]
:extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}}
:main-opts ["-m" "clojure.test runner"]}
:repl {:extra-deps {nrepl/nrepl {:mvn/version "1.0.0"}
cider/cider-nrepl {:mvn/version "0.36.0"}
;; other REPL tools such as refactor-nrepl
refactor-nrepl/refactor-nrepl {:mvn/version "3.4.0"}}
:main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]"]}}}
"""
### 1.2. Dependency Versioning
**Standard:** Explicitly declare dependency versions.
**Do This:**
* Always specify concrete versions for all dependencies in "deps.edn".
* Regularly review and update dependency versions.
**Don't Do This:**
* Omit dependency versions or rely on "latest.release" (or similar dynamic versioning).
**Why:** Specifying versions ensures reproducible builds and reduces the risk of unexpected behavior changes due to transitive dependency updates.
**Example:**
"""clojure
{:deps {org.clojure/data.json {:mvn/version "2.4.0"}}}
"""
### 1.3. Developing against SNAPSHOT dependencies
**Standard:** Use SNAPSHOT dependencies when working on libraries that depend on each other.
**Why:** Speeds up the workflow developing multiple Clojure libraries simultaneously, allowing for incremental changes without deploying each library to a Maven repository on every change.
**Example:**
Assume you are working on "library-a" and "library-b". "library-b" depends on "library-a".
In "library-b/deps.edn":
"""clojure
{:deps {com.example/library-a {:mvn/version "1.0.0-SNAPSHOT"}}}
"""
Before working on "library-b", install "library-a" locally with "clojure -T:build install". Make iterative changes to "library-a" and reinstall again. The SNAPSHOT dependency will use the latest locally installed version.
## 2. REPL-Driven Development
### 2.1. REPL Workflow
**Standard:** Embrace REPL-driven development as the primary workflow.
**Do This:**
* Use a REPL (CIDER, REPL-y, etc.) for interactive development.
* Load code incrementally into the REPL and test changes immediately.
* Use "cider-jack-in" or similar commands to start a REPL connected to your editor.
* Use tools like "clojure.tools.namespace" to reload code.
* Use "clojure.repl/doc", "clojure.repl/source", and "clojure.repl/dir" in the REPL to explore code
**Don't Do This:**
* Rely solely on static analysis or compilation cycles without leveraging the REPL.
* Restart the entire application after making small code changes.
**Why:** REPL-driven development dramatically increases development speed and facilitates experimentation. It empowers immediate feedback & iterative design refinement.
**Example:**
"""clojure
;; In the REPL
(require '[clojure.tools.namespace.repl :as repl])
(repl/refresh) ; Reload changed namespaces
(my-app.core/my-function 1 2 3) ; Execute the function
"""
### 2.2. Hot Code Reloading
**Standard:** Use hot code reloading, using the REPL, to update running applications with new code changes without restarting the entire system.
**Do This:**
* Require "clojure.tools.namespace.repl" in your development environment or use a library that provides reloading such as "integrant".
* When changing source code, call "(repl/refresh)" to reload changed namespaces and ensure the changes are applied to your REPL environment.
**Don't Do This:**
* Manually "require" or "refer" namespaces after making changes, as this can lead to inconsistencies.
* Avoid reloading when making changes because you are uncertain about the implications. Understanding how namespace reloading works is crucial for productive Clojure development.
**Why:** Hot code reloading vastly improves developer turnaround, allowing for quick iterative design and problem-solving.
**Example:**
"""clojure
;; In the REPL, after modifying src/my_app/core.clj:
(require 'clojure.tools.namespace.repl :reload)
(clojure.tools.namespace.repl/refresh)
;; Now test the modified function
(my_app.core/my-modified-function)
"""
### 2.3 REPL history and persistence
**Standard:** Store REPL history and persist the REPL session between restarts.
**Do This:**
* Configure your REPL environment (CIDER, etc.) to save history to a file.
* Use a REPL persistence library such as "reply" which saves the state of the REPL to a database.
* Use "portal" as a REPL enhancement for inspecting data structures.
**Don't Do This:**
* Start from scratch every time you open a REPL connection.
**Why:** Persisting REPL history enables you to recall previous commands and experiments, saving time and effort. Keeping session persistence means you don't need to redefine functions on every restart.
## 3. Linting and Static Analysis
### 3.1. Using "clj-kondo"
**Standard:** Use "clj-kondo" for linting and static analysis.
**Do This:**
* Integrate "clj-kondo" into your development workflow (editor integration or command-line tool).
* Configure "clj-kondo" to enforce project-specific coding standards.
* Address "clj-kondo" warnings and errors promptly.
* Leverage ".clj-kondo/config.edn" for custom linting rules.
* Use the ":lint-as" feature for advanced type hinting or behavior modification.
**Don't Do This:**
* Ignore "clj-kondo" warnings or treat them as inconsequential.
* Disable relevant linting rules without a valid justification.
* Fail to share a common ".clj-kondo/config.edn" across the development team.
**Why:** "clj-kondo" helps identify code smells, potential errors, and style inconsistencies early in the development process. This promotes code quality and reduces debugging efforts. Static analysis catches a multitude of issues before runtime.
**Example ".clj-kondo/config.edn":**
"""clojure
{:linters {:unresolved-symbol {:level :warning} ; treat unresolved symbols as warnings
:unresolved-namespace {:level :error} ; treat unresolved namespaces as errors
:unused-binding {:level :off} ; disable unused binding warnings
:deprecated-vars {:level :warning}}
:config-paths ["/path/to/shared/config"] ; Shared configuration
:lint-as {my-project.macros/defservice clojure.core/defn} ; Treat defservice like defn for linting purposes
}
"""
### 3.2. Custom Linting
**Standard:** Develop custom linting configurations for project-specific checks using clj-kondo.
**Do This:**
* Create a ".clj-kondo/config.edn" file at the root of your project.
* Define custom ":hooks" within the configuration to analyze code structure and semantics specific to your libraries or domain logic.
* Use the "clj-kondo --lint" command to run the linter and identify violations.
**Don't Do This:**
* Rely solely on default linting rules without tailoring them to your project's specific needs.
* Introduce overly complex or performance-intensive hooks that significantly slow down the linting process.
* Neglect to document the purpose and behavior of your custom linting rules.
**Why:** Custom linting allows teams to enforce project-specific coding standards, catch domain-specific errors earlier, and maintain a consistent codebase.
**Example:**
"""clojure
;; .clj-kondo/config.edn
{:hooks [[:find-require-refer
{:lint-ns 'my-project.core
:require 'my-project.deprecated-lib
:message "Referring to deprecated library"
:level :error}]]}
"""
This example creates a custom hook, "find-require-refer", that checks if the namespace "my-project.core" refers to "my-project.deprecated-lib". If it does, "clj-kondo" will report an error.
## 4. Testing
### 4.1. Test Frameworks
**Standard:** Use "clojure.test" as the primary testing framework, supplemented by data-driven testing libraries like "clojure.test.check".
**Do This:**
* Write unit tests using "clojure.test"'s "deftest" and "is" macros.
* Organize tests into namespaces mirroring the source code structure.
* Use "clojure.test.check" for property-based (generative) testing.
* Run tests frequently and automatically (e.g., with "kaocha").
**Don't Do This:**
* Avoid writing tests or rely solely on manual testing.
* Write overly complex or brittle tests.
* Fail to cover edge cases or boundary conditions in your tests.
**Why:** Automated testing is essential for code quality, reliability, and maintainability. "clojure.test" is the built-in standard, and "clojure.test.check" adds a powerful dimension of generative testing.
**Example "clojure.test":**
"""clojure
(ns my-app.core-test
(:require [clojure.test :refer [deftest is testing]]
[my-app.core :as core]))
(deftest addition-test
(testing "Should add two numbers correctly"
(is (= 3 (core/add 1 2)))))
"""
**Example "clojure.test.check":**
"""clojure
(ns my-app.core-test
(:require [clojure.test :refer [deftest]]
[clojure.test.check.clojure-test :refer [defspec]]
[clojure.test.check.properties :as prop]
[clojure.test.check.generators :as gen]
[my-app.core :as core]))
(defspec add-associative 100
(prop/for-all [a gen/int
b gen/int
c gen/int]
(= (core/add a (core/add b c))
(core/add (core/add a b) c))))
"""
### 4.2. Test Runners
**Standard:** Utilize a dedicated test runner like Kaocha to manage and execute tests efficiently.
**Do This:**
* Configure Kaocha with a "tests.edn" file in your project root.
* Specify test namespaces, source paths, and other test-related configurations.
* Use the "kaocha" command to run tests from the command line or integrate it into your build process.
* Customize Kaocha's behavior with plugins, such as code coverage or JUnit XML reporting.
**Don't Do This:**
* Run tests directly from the REPL without using a test runner for larger projects.
* Neglect to configure Kaocha for your specific project needs, such as excluding certain tests or setting up custom test environments.
**Why:** Kaocha provides a convenient way to discover, execute, and manage tests in Clojure projects, offering features like parallel test execution, code coverage reporting, and JUnit XML output.
**Example "tests.edn":**
"""clojure
{:source-paths ["src"]
:test-paths ["test"]
:dependencies [[org.clojure/clojure "1.11.2"]
[org.clojure/test.check "1.1.1"]]
:namespace-prefixes ["my-app"]
:plugins [[kaocha.plugin/spec-discover]]}
"""
### 4.3 Mocking and Stubbing
**Standard:** Use appropriate mocking libraries when unit testing functions with external dependencies; use sparingly, favoring integration tests over excessive mocking.
**Do This:**
* If mocking is required (i.e. for external DB connection), use a library like "clojure.test.check.clojure-test/with-redefs" to temporarily redefine functions during testing.
* Focus on testing the core logic of your functions, and avoid mocking unnecessarily.
* Consider using integration tests to verify the interaction between different components of your system.
**Don't Do This:**
* Over-mock your code, which leads to brittle tests that don't reflect real-world usage.
* Mock functions that are part of your own application's core logic.
Why: Mocking allows you to isolate units of code for testing without relying on external dependencies, but over-mocking can make tests less reliable and more difficult to maintain. It is key to strike a balance between isolating units of code and verifying interactions between external dependencies.
**Example:**
"""clojure
(ns my-app.payment-processor-test
(:require [clojure.test :refer [deftest is with-redefs]]
[my-app.payment-processor :as processor]))
(deftest process-payment-test
(with-redefs [processor/charge-credit-card (fn [amount card-number]
{:status :success
:transaction-id "fake-transaction-id"})]
(let [result (processor/process-payment 100 "1234-5678-9012-3456")]
(is (= :success (:status result)))
(is (= "fake-transaction-id" (:transaction-id result))))))
"""
## 5. Logging
### 5.1. Logging Libraries
**Standard:** Use a dedicated logging library, preferably "taoensso.timbre".
**Do This:**
* Configure "taoensso.timbre" with appropriate log levels and output destinations.
* Use macros like "(timbre/info "Message")" for logging.
* Include relevant context information in log messages (e.g., user ID, request ID).
**Don't Do This:**
* Use "println" or "prn" for logging in production code.
* Log sensitive information (e.g., passwords, credit card numbers).
* Rely on default logging configurations without customization.
**Why:** Dedicated logging libraries offer features like log levels, formatting, and configurable output, which are essential for debugging and monitoring applications.
**Example:**
"""clojure
(ns my-app.core
(:require [taoensso.timbre :as log]))
(log/set-config! {:level :info}) ; Set log level to info
(defn my-function [x]
(log/debug "my-function called with x:" x)
(log/info "Processing value:" x)
(if (> x 10)
(log/warn "Value x is greater than 10")
(log/trace "Value x is less than or equal to 10"))
(+ x 1))
"""
### 5.2 Structured Logging
**Standard:** Favor structured logging over unstructured logging, using logging features tailored for structured data.
**Do This:**
* Use logging libraries that support structured logging (e.g., "taoensso.timbre" with its "with-context" and metadata capabilities).
* Log data as key-value pairs rather than just raw text strings.
* Use consistent keys and value types for structured log data.
**Don't Do This:**
* Rely solely on string concatenation to create log messages, as it makes parsing and analysis difficult.
Why: Structured logging makes it easier to analyze logs programmatically, filter based on specific data points, and derive insights.
**Example:**
"""clojure
(ns my-app.payment-processor
(:require [taoensso.timbre :as log]))
(defn process-payment [user-id amount card-number]
(log/info "Processing payment" :user-id user-id :amount amount)
;; ... payment processing logic ...
(log/info "Payment processed successfully" :user-id user-id :amount amount :transaction-id "12345"))
"""
## 6. Build Automation and Deployment
### 6.1. Build Tools
**Standard:** Use "clojure.tools.build", Clojure's official build tool.
**Do This:**
* Define a "build.clj" file in your project root.
* Define tasks for compiling, testing, and packaging your application.
* Automate builds using command-line tools or CI/CD pipelines.
**Don't Do This:**
* Use "Leiningen" or other build tools for new projects.
* Manually build and deploy applications without automation.
**Why:** Build automation ensures reproducible builds, simplifies deployment, and promotes consistency across environments.
**Example "build.clj":**
"""clojure
(ns build
(:require [clojure.tools.build.api :as b]))
(def lib 'my-app/core)
(def version (format "1.0.%s" (b/git-count-revs nil)))
(def class-dir "target/classes")
(def basis (b/create-basis {:project "deps.edn"}))
(def jar-file (format "target/%s-%s.jar" (name lib) version))
(defn clean [_]
(b/delete {:path "target"}))
(defn compile [_]
(b/compile-clj {:basis basis
:src-dirs ["src"]
:class-dir class-dir}))
(defn jar [_]
(b/create-jar {:basis basis
:class-dir class-dir
:jar-file jar-file}))
(defn deploy [_]
;; Implement deployment logic here
(println "Deploying" jar-file))
"""
### 6.2. Containerization
**Standard:** Use Docker for containerizing Clojure applications.
**Do This:**
* Create a "Dockerfile" that defines the application environment.
* Use a minimal base image (e.g., Alpine Linux with OpenJDK).
* Copy the compiled JAR file into the container.
* Define the entry point for running the application.
* Use multi-stage builds to reduce the size of the Docker image.
**Don't Do This:**
* Build Docker images manually without a "Dockerfile."
* Include unnecessary files or dependencies in the Docker image.
* Expose sensitive information in the Docker image.
**Why:** Containerization provides a consistent and isolated environment for running Clojure applications, simplifying deployment and ensuring portability.
**Example "Dockerfile":**
"""dockerfile
FROM eclipse-temurin:17-jre-alpine as builder
WORKDIR /app
COPY deps.edn .
COPY src ./src
RUN clojure -T:build compile
RUN clojure -T:build jar
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/my-app-1.0.0.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
"""
## 7. Documentation
### 7.1. Code Documentation
**Standard:** Document Clojure code thoroughly using docstrings.
**Do This:**
* Add docstrings to all functions, namespaces, and macros.
* Describe the purpose, arguments, and return value of each function.
* Use clear and concise language.
* Include examples of how to use the function.
**Don't Do This:**
* Omit docstrings or write incomplete documentation.
* Write overly verbose or confusing docstrings.
**Why:** Documentation is essential for code maintainability, readability, and collaboration. Docstrings provide a quick reference for developers using your code.
**Example:**
"""clojure
(ns my-app.core
"This namespace contains the core functions of the application.")
(defn add
"Adds two numbers together.
Args:
x: The first number.
y: The second number.
Returns:
The sum of x and y.
Example:
(add 1 2) => 3"
[x y]
(+ x y))
"""
### 7.2. API Documentation
**Standard:** Generate API documentation using tools like "codox".
**Do This:**
* Configure "codox" to generate documentation from docstrings.
* Publish API documentation online for easy access.
* Tailor "codox" configurations (via comments) to suit the specific formatting of your API
* Include detailed explanations of API endpoints and data structures.
**Don't Do This:**
* Rely solely on code comments without generating formal documentation.
* Leave APIs uncommented.
**Why:** API documentation provides developers with a comprehensive guide to using your application's APIs, reducing the need for manual code inspection.
**Example configuration comment for "codox"**
"""clojure
(ns my-library.functions
"A collection of useful functions.
;; :codox/metadata
{:doc/format :markdown}
")
(defn my-function
"Does something useful.
;; :codox/visibility :private
;; :codox/tags [:experimental]
"
[x]
x)
"""
## 8. Security Best Practices
### 8.1 Dependency Vulnerability Scanning
**Standard:** Implement automated dependency vulnerability scanning as part of your build process.
**Do This:**
* Evaluate and integrate a vulnerability scanning tool (e.g., OWASP Dependency-Check, Snyk, or similar) into your CI/CD pipeline, either via CLI or a CI/CD platform plugin.
* Configure the tool to scan your "deps.edn" file for known vulnerabilities in third-party dependencies.
* Regularly update the vulnerability database used by the scanner.
* Configure the tool to fail the build if vulnerabilities are found with a severity level above a predefined threshold.
**Don't Do This:**
* Neglect to scan dependencies for vulnerabilities, as this can introduce significant security risks.
* Ignore vulnerability reports or postpone addressing them, as this increases the potential for exploitation.
* Rely solely on manual dependency updates without automated vulnerability scanning.
**Why:** Automated dependency vulnerability scanning helps identify and mitigate security risks arising from vulnerable third-party libraries, reducing the attack surface of your application.
### 8.2 Avoid direct use of "eval"
**Standard:** Avoid using "eval" or similar functions that execute arbitrary code from strings.
**Do This:**
* Find alternatives to "eval" such as data-driven architectures and function dispatch.
* If "eval" is unavoidable, sanitize the input string to prevent code injection attacks.
* Restrict the scope of "eval" to a minimum.
**Don't Do This:**
* Use "eval" directly on user-supplied strings.
* Use "eval" without understanding its security implications.
**Why:** Executing arbitrary code from strings can lead to severe security vulnerabilities, allowing attackers to inject malicious code into your application.
**Example:**
Instead of using eval:
"""clojure
(defn execute-command [command]
(eval (read-string command))) ; Avoid this!
"""
Use a data-driven approach:
"""clojure
(def commands
{"add" +
"subtract" -})
(defn execute-command [command & args]
(if-let [f (get commands command)]
(apply f args)
(throw (Exception. (str "Unknown command: " command)))))
"""
## 9. Performance Optimization Tools
### 9.1 Profiling tools
**Standard:** Employ profiling tools to identify performance bottlenecks in Clojure code.
**Do This:**
* Integrate a profiling library (e.g., "clj-async-profiler" or "criterium") into your development workflow.
* Use the profiler to measure the execution time of different code sections and identify performance bottlenecks.
* Analyze the profiling results to pinpoint areas where optimization is needed.
* Repeat profiling after applying optimizations to verify their effectiveness.
**Don't Do This:**
* Rely solely on intuition or guesswork to identify performance bottlenecks.
* Neglect to profile code before and after applying optimizations to measure their impact.
* Profile in a non-representative environment for the workload characteristics.
**Why:** Profiling provides valuable insights into the performance characteristics of Clojure code, enabling developers to identify and address bottlenecks effectively.
**Example Usage of "criterium"**
"""clojure
(ns my-app.performance-test
(:require [criterium.core :as crit]
[my-app.core :as core]))
(defn slow-function [n]
(reduce + (range n)))
(defn fast-function [n]
(apply + (range n)))
(comment
(crit/quick-bench (slow-function 10000))
(crit/quick-bench (fast-function 10000)))
"""
This concludes the Tooling and Ecosystem standards for Clojure. Adhering to these guidelines will improve the consistency, maintainability, and security of Clojure projects.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Code Style and Conventions Standards for Clojure This document outlines the coding style and conventions to be followed when developing Clojure code. Adhering to these standards will ensure code readability, maintainability, and consistency across projects. The guidelines presented here align with modern Clojure practices and aim to leverage the language's strengths for effective software development. ## 1. General Principles * **Consistency:** Maintain a consistent style throughout the codebase. Prefer using established idioms and patterns over inventing new ones. * **Why:** Consistent code is easier to understand and modify. It also reduces cognitive load for developers working on the project. * **Readability:** Write code that is easy to read and understand. Prioritize clarity over brevity. * **Why:** Code is read much more often than it is written. Readable code reduces debugging time and makes it easier for new developers to join the project. * **Simplicity:** Keep code as simple as possible. Avoid unnecessary complexity. * **Why:** Simple code is easier to reason about and less prone to errors. It also makes it easier to refactor and maintain the codebase. * **Idempotence:** Functions should always return the same result if given the same arguments. Prefer immutable data structures to reduce side effects. * **Why:** Idempotent data structures promote easier reasoning about code. * **Testability:** Design code that is easy to test. Use dependency injection and avoid tight coupling. * **Why:** Testable code is more reliable and easier to maintain. It also allows for more confident refactoring. ## 2. Formatting ### 2.1. Indentation * Use 2 spaces for indentation. Avoid tabs. * **Why:** Consistent indentation makes code easier to read. Two spaces are a good balance between readability and line length. * Align code vertically where appropriate. * **Why:** Vertical alignment highlights the structure of the code and makes it easier to scan. * Use indentation to reflect the structure of the code. """clojure (defn calculate-total [price quantity discount] (let [discounted-price (- price (* price discount)) total-price (* discounted-price quantity)] total-price)) """ ### 2.2. Line Length * Limit lines to 80 characters. Consider breaking lines at logical points. * **Why:** Shorter lines are easier to read on a variety of screen sizes. They also make it easier to compare code side-by-side. * When breaking lines, indent the continuation lines by 2 spaces. """clojure (defn long-function-name [argument1 argument2 argument3] (let [result (+ argument1 argument2 argument3)] result)) """ ### 2.3. Whitespace * Use whitespace to separate logical sections of code. * **Why:** Whitespace improves readability and makes it easier to scan the code. * Add a newline after each top-level form. * **Why:** Clear separation of forms improves understanding. """clojure (ns my-project.core) (defn hello [name] (println (str "Hello, " name))) (hello "World") """ * Use a space after commas in collections. """clojure (def my-vector [1, 2, 3, 4]) ; Do this (def my-vector [1,2,3,4]) ; Don't do this """ ### 2.4. File Structure * Place namespaces in directories that match the namespace name. For example, the namespace "my-project.core" should be in the file "src/my_project/core.clj". * **Why:** This makes it easy to find the source code for a given namespace. * Keep related functions and data structures in the same namespace. * **Why:** This improves code organization and makes it easier to understand the dependencies between different parts of the application. * Use "-main" function to start the application if needed. ## 3. Naming Conventions ### 3.1. Namespaces * Use lowercase letters and underscores for namespace names. * **Why:** This is the convention in the Clojure community. * Use a hierarchical structure for namespace names (e.g., "com.example.my-project.core"). * **Why:** This helps to organize code and avoid naming conflicts. * Follow reverse domain name notation for the root namespace (e.g., "com.example.my-project"). """clojure (ns com.example.my-project.core) """ ### 3.2. Functions * Use lowercase letters and hyphens for function names. * **Why:** This is the convention in the Clojure community. * Use descriptive names that clearly indicate the function's purpose. * **Why:** Clear names make it easier to understand the code. * Use verbs for function names. * **Why:** Verbs indicate that the function performs an action. * When naming functions with side effects, end the name with an exclamation mark ("!"). * **Why:** This alerts developers that the function has side effects. Avoid this in most cases, prefer immutable data structures. """clojure (defn calculate-sum [numbers] (reduce + numbers)) (defn print-message! [message] (println message)) ; use with caution, prefer immutable structures """ ### 3.3. Variables and Constants * Use lowercase letters and hyphens for variable names. * **Why:** This is the convention in the Clojure community. * Use descriptive names that clearly indicate the variable's purpose. * **Why:** Clear names make it easier to understand the code. * Use "def" for constants and capitalize the name with underscores. * **Why:** Constants should be easily identifiable. """clojure (def PI 3.14159) (def MAX_VALUE 100) """ ### 3.4. Keywords * Use keywords for map keys. * **Why:** Keywords are interned, which makes them more efficient than strings. They also prevent naming conflicts. """clojure (def person {:name "John" :age 30}) """ ## 4. Stylistic Consistency ### 4.1. Idiomatic Clojure * Use Clojure's built-in functions and data structures whenever possible. * **Why:** Clojure's built-in functions are highly optimized. They also follow established conventions. * Use "destructuring" to extract values from data structures. * **Why:** Destructuring makes code more concise and readable. """clojure (defn greet [{:keys [name age]}] (println (str "Hello, " name "! You are " age " years old."))) (greet {:name "John" :age 30}) """ * Use "->" and "->>" for threading operations. * **Why:** Threading macros make code more readable by chaining operations together. """clojure (defn process-data [data] (-> data (map inc) (filter even?) (reduce +))) """ * Use Records and Types sparingly, opting for maps. While Records can provide performance benefits, the added complexity may outweigh the gains in many scenarios. * **Why:** Maps are more flexible data. ### 4.2. Immutability * Use immutable data structures whenever possible. * **Why:** Immutable data structures prevent side effects and make code easier to reason about. * Use "transient" to optimize operations that require modifying a data structure. * **Why:** Transients allow for in-place modification of data structures while still maintaining the illusion of immutability. """clojure (defn update-vector [v] (persistent! (reduce conj! (transient v) (range 10)))) """ ### 4.3. Error Handling * Use "try"/"catch"/"finally" for handling exceptions. * **Why:** Exception handling prevents the application from crashing and allows for graceful recovery from errors. * Use "assert" for validating input. * **Why:** Assertions can help catch errors early in the development process. """clojure (defn divide [a b] (assert (not= b 0) "Cannot divide by zero") (/ a b)) (try (divide 10 0) (catch ArithmeticException e (println "Error: " (.getMessage e)))) """ ### 4.4 Comments * Write meaningful comments. Explain the "why" not the "what". * **Why:** Code tells you what is happening, comments should explain why it is happening. * Use docstrings to document functions and namespaces. * **Why:** Docstrings are the primary way to document Clojure code. """clojure (ns my-project.core "This namespace contains the core functions for my project.") (defn calculate-sum "Calculates the sum of a collection of numbers." [numbers] (reduce + numbers)) """ ### 4.5 Testing * Write unit tests for all functions. * **Why:** Unit tests ensure that the code works as expected and prevent regressions. * Use a testing framework such as "clojure.test" or "Midje". * **Why:** Testing frameworks provide a structured way to write and run tests. * Write integration tests to verify that different parts of the application work together correctly. * **Why:** Integration tests ensure that the application as a whole works as expected. ## 5. Specific Anti-Patterns and Mistakes to Avoid * **Over-commenting trivial code:** Avoid stating the obvious. Comments should explain *why* the code is written, not *what* it does (unless the what is obfuscated). * **Using mutable state excessively:** Embrace Clojure's immutability. Minimize the use of "atoms", "refs", and "agents". For instance, if a function can be written without using an "atom", it generally *should* be. * **Ignoring REPL-driven development:** Clojure excels with REPL-driven development. Use it! Don't just write code in files and then execute them. Interact with your code, test functions directly, and explore the data flow in real-time. * **Premature optimization:** Don't optimize code before it's necessary. Focus on writing clear and correct code first. Profile before attempting any speed increases. ## 6. Modern Approaches and Patterns * **Data-Oriented Programming (DOP):** DOP is a core concept in Clojure. Treat data as immutable and first-class. Functions transform data. * Example: Configuration Management """clojure (def default-config {:db-host "localhost" :db-port 5432 :cache-size 1000}) (defn merge-config [base-config overrides] (merge base-config overrides)) (def prod-config (merge-config default-config {:db-host "prod.example.com" :cache-size 2000})) """ * **Component-Based Architecture:** Using libraries like "component" or "integrant" to manage the lifecycle of application components. * **Example (using Integrant):** """clojure (ns my-app.system (:require [integrant.core :as ig])) (defmethod ig/init-key :db/connection [_ config] (println "Connecting to DB:" config) ;; Actual DB connection code here {:connection config}) (defmethod ig/halt-key :db/connection [_ {:keys [connection]}] (println "Disconnecting from DB:" connection)) (def config {:db/connection {:host "localhost" :port 5432}}) (comment (def system (ig/init config)) (ig/halt! system)) """ * **Asynchronous Programming:** Using "core.async" for managing asynchronous operations and concurrency. * Example: """clojure (ns my-app.async (:require [clojure.core.async :as async])) (defn process-item [item] (Thread/sleep 1000) ; Simulate processing time (str "Processed: " item)) (defn start-processor [input-chan output-chan] (async/go-loop [] (when-let [item (async/<! input-chan)] (let [result (process-item item)] (async/>! output-chan result) (recur))))) (comment (let [input-chan (async/chan) output-chan (async/chan) _ (start-processor input-chan output-chan)] (async/>!! input-chan "Item 1") (async/>!! input-chan "Item 2") (async/close! input-chan) (println (async/<!! output-chan)) ; Prints "Processed: Item 1" (println (async/<!! output-chan)))) ; Prints "Processed: Item 2" """ * **Using Spec for Data Validation and Generation:** "clojure.spec.alpha" allows you to define specifications for your data and use these specifications for validation, data generation, and documentation. """clojure (ns my-app.spec (:require [clojure.spec.alpha :as s])) (s/def ::name string?) (s/def ::age pos-int?) (s/def ::person (s/keys :req-un [::name ::age])) (def valid-person {:name "Alice" :age 30}) (def invalid-person {:name "Bob" :age -5}) (comment (s/valid? ::person valid-person) ; true (s/valid? ::person invalid-person) ; false (s/explain ::person invalid-person)) """ ## 7. Performance Optimization Techniques * **Use transients for mutable operations on persistent data structures:** Transients allow for in-place modification of persistent data structures, which can significantly improve performance. Convert back to a persistent data structure after the modifications are complete. """clojure (defn modify-vector [v] (let [t (transient v)] (dotimes [i (count v)] (aset! t i (* i 2))) ; Mutating transient vector (persistent! t))) ; Convert back to persistent vector """ * **Minimize reflection by using type hints:** Clojure uses reflection to determine the types of arguments at runtime. Type hints can help the compiler avoid reflection, which can improve performance. """clojure (defn add [^long x ^long y] ; type hints for x and y (+ x y)) """ * **Use the "loop" and "recur" construct for tail-recursive functions:** Tail-recursive functions can be optimized by the compiler to avoid stack overflow errors. """clojure (defn factorial [n] (loop [n n acc 1] (if (<= n 1) acc (recur (dec n) (* acc n))))) """ * **Leverage Java Interop Judiciously:** While Clojure promotes functional programming, leveraging well-optimized Java libraries (while maintaining Clojure style) can boost performance for specific tasks. ## 8. Security Best Practices * **Input Validation:** Always validate user input to prevent injection attacks (e.g., SQL injection, XSS). Use "clojure.spec.alpha" or other validation libraries to ensure that input data conforms to expected formats and values. * **Output Encoding:** Properly encode output data to prevent XSS attacks. Use appropriate encoding functions for different output contexts (e.g., HTML, JavaScript, SQL). * **Authentication and Authorization:** Implement robust authentication and authorization mechanisms to protect sensitive data and resources. Use established libraries and frameworks for authentication and authorization, such as "friend" or "buddy". * **Dependency Management:** Keep dependencies up to date to address known security vulnerabilities. Regularly audit dependencies and update them to the latest versions. * **Avoid Using "eval":** Using "eval" can introduce security vulnerabilities if the input is not carefully validated. Avoid using "eval" whenever possible. If you must use it, ensure that the input is from a trusted source and that it is properly validated. * **Secrets Management:** Store secrets (e.g., passwords, API keys) securely and avoid hardcoding them in the codebase. Use environment variables or dedicated secrets management tools to store and manage secrets. * **Secure Random Number Generation:** Use secure random number generators for generating cryptographic keys and other security-sensitive data. Use the "java.security.SecureRandom" class or a dedicated security library for generating random numbers. This coding style and conventions document provides a comprehensive set of guidelines for developing Clojure code. By following these standards, developers can ensure that their code is readable, maintainable, and consistent. This leads to improved collaboration, reduced development time, and higher-quality software. Remember that these are guidelines, and there will be situations where it makes sense to deviate from them. Always use your best judgment and consider the specific context of the code you are writing.
# Core Architecture Standards for Clojure This document outlines the core architectural standards for Clojure projects, encompassing architectural patterns, project structure, and organization principles. Adhering to these standards enhances maintainability, performance, and security. ## 1. Architectural Patterns Selecting the right architectural pattern forms the foundation of a robust and scalable Clojure application. ### 1.1. Functional Core, Imperative Shell **Description:** This pattern separates the pure functional core of the application from the imperative shell that interacts with the outside world (e.g., databases, network services). **Do This:** * Maximize the amount of code in the functional core to leverage Clojure's strengths in immutability, concurrency, and testability. * Isolate side-effecting operations in the imperative shell using protocols/multimethods for abstraction. **Don't Do This:** * Mix side-effecting code directly within the core business logic. * Create large, monolithic functions that perform both pure computations and I/O operations. **Why:** Improves testability, reduces complexity, and allows for easier reasoning about the application's behavior. **Example:** """clojure ;; Functional Core (pure functions) (ns my-app.core (:require [my-app.domain :as domain])) (defn calculate-discount "Calculates the discount based on customer type and purchase amount." [customer purchase-amount] (domain/apply-discount customer purchase-amount)) ;; Imperative Shell (side-effecting operations) (ns my-app.handler (:require [my-app.core :as core] [ring.util.response :as response])) (defn handle-purchase "Handles a purchase request. This is the 'shell'." [request] (let [customer (get-in request [:body :customer]) purchase-amount (get-in request [:body :purchase-amount]) discounted-amount (core/calculate-discount customer purchase-amount)] (response/response (str "Discounted amount: " discounted-amount)))) """ **Anti-Pattern:** Embedding database calls within the "calculate-discount" function. ### 1.2. Component-Based Architecture (using "integrant" or "component") **Description:** Decomposes the application into independent, reusable components that can be easily assembled and configured. Use libraries like "integrant" or "component". **Do This:** * Define components as records that implement lifecycle protocols (e.g., "init", "start", "stop"). * Use configuration maps to specify component dependencies and settings. * Favor composition over inheritance. **Don't Do This:** * Create tightly coupled components with circular dependencies. * Hardcode component configurations. **Why:** Enhances modularity, testability, and reusability. Facilitates easier deployments and system evolution. **Example (using "integrant"):** """clojure (ns my-app.components (:require [integrant.core :as ig] [clojure.java.jdbc :as jdbc])) ;; Database Component (defmethod ig/init-key :db/h2 [_ config] (println "Initializing database...") (jdbc/get-connection config)) (defmethod ig/halt-key :db/h2 [_ db] (println "Closing database connection...") (.close db)) ;; HTTP Server Component (example) (defmethod ig/init-key :http/server [_ config] (println "Starting HTTP server...") ;; code to start the server {:server config}) ;; dummy server (defmethod ig/halt-key :http/server [_ {:keys [server]}] (println "Stopping HTTP server...") ;; code to stop the server nil) ;; dummy server ;; System Configuration (def config {:db/h2 {:dbtype "h2" :dbname "my-app"} :http/server {:port 3000}}) ;; Starting the system (def system (ig/init config)) ;; Stopping the system (ig/halt! system) """ **Anti-Pattern:** Creating a global database connection outside the component lifecycle. ### 1.3 Data-Oriented Architecture **Description:** Architecting the application around data transformations, where data flows through a series of functions. Emphasize the use of immutable data structures. **Do This:** * Model data using persistent data structures (maps, vectors, sets). * Use functions to transform data from one representation to another. * Leverage "clojure.spec.alpha" for data validation and generation. **Don't Do This:** * Rely on mutable state to track application data. * Mix data transformation logic with side-effecting operations. **Why:** Improves predictability, simplifies concurrency, and facilitates data provenance. **Example:** """clojure (ns my-app.data (:require [clojure.spec.alpha :as s])) ;; Define data specifications (s/def :user/id uuid?) (s/def :user/name string?) (s/def :user/email string?) (s/def :user/profile (s/keys :req [:user/id :user/name :user/email])) ;; Data transformation function (defn enrich-user-data "Enriches user data with additional calculated fields." [user] (assoc user :user/creation-date (java.util.Date.))) ;; Example usage (def raw-user {:user/id (java.util.UUID/randomUUID) :user/name "Alice" :user/email "alice@example.com"}) (def validated-user (s/conform :user/profile raw-user)) (def enriched-user (enrich-user-data validated-user)) (println enriched-user) """ **Anti-Pattern:** Modifying the "raw-user" map directly instead of creating a new, enriched map. ## 2. Project Structure and Organization A well-defined project structure is essential for maintainability and collaboration. ### 2.1. Standard Directory Layout **Do This:** * Follow the Leiningen/Boot standard directory layout: * "src/": Source code. * "test/": Test code. * "resources/": Static assets and configuration files. * Organize namespaces according to the project's domain. Use a hierarchical structure matching the directory structure reflecting the conceptual domain. **Don't Do This:** * Store source code in arbitrary locations. * Mix source code with test code. **Why:** Provides a consistent and predictable structure for all Clojure projects. **Example:** """ my-app/ ├── project.clj ├── src/ │ ├── my_app/ │ │ ├── core.clj │ │ ├── database.clj ; DB related operations are in a named namespace │ │ ├── api/ ; subfolder for API definitions │ │ │ └── routes.clj │ │ └── utils.clj ; Generic utility functions ├── test/ │ ├── my_app/ │ │ └── core_test.clj └── resources/ └── config.edn """ ### 2.2. Namespaces and Modules **Do This:** * Use meaningful namespace names that reflect the module's purpose ("com.example.my-app.db"). * Keep modules small and focused, each responsible for a single aspect of the application. * Use "require" and "use" judiciously to manage dependencies between modules. * Qualify function names when using "require" to improve readability ("(db/get-user user-id)"). * Expose only the necessary functions from a module using "defn-" for private functions. **Don't Do This:** * Create large, monolithic namespaces with hundreds of functions. * Use "use" excessively, as it can lead to namespace pollution. * Create circular dependencies between modules. **Why:** Promotes modularity, reduces cognitive load, and prevents naming conflicts. **Example:** """clojure ;; src/my_app/db.clj (ns my-app.db (:require [clojure.java.jdbc :as jdbc])) (def db-spec {:dbtype "postgresql" :dbname "my-database" :host "localhost" :user "db-user" :password "secret"}) (defn get-user "Retrieves a user from the database by ID." [user-id] (jdbc/query db-spec ["SELECT * FROM users WHERE id = ?" user-id])) (defn- internal-helper-function ;; private function "Only available within the my-app.db namespace" [x] (* x 2)) """ ### 2.3. Configuration Management **Do This:** * Store configuration settings in external files (e.g., EDN, YAML) or environment variables. * Use a library like "aero" or "environ" to load and manage configuration. * Avoid hardcoding configuration values in the source code. **Don't Do This:** * Store sensitive information (e.g., passwords, API keys) in plain text configuration files. Favor using environment variables or secure storage. * Load configuration settings directly into functions. **Why:** Simplifies deployment, allows for environment-specific configurations, and protects sensitive data. **Example (using "aero"):** """clojure ;; project.clj (add aero dependency) ;; (defproject my-app "0.1.0-SNAPSHOT" ;; ... ;; :dependencies [[org.clojure/clojure "1.11.1"] ;; [integrant "0.8.0"] ;; [aero "1.1.6"]]) ;; resources/config.edn {:db {:dbtype "postgresql" :dbname "my-database" :host "localhost"}} ;; src/my_app/config.clj (ns my-app.config (:require [aero.core :as aero])) (def config (aero/read-config "config.edn")) (println config) ;; Access configuration values (e.g., (:db config)) """ **Anti-Pattern:** Hardcoding database credentials directly in the "get-user" function. ## 3. Design Patterns Applying established design patterns can improve code structure and reusability. ### 3.1. Protocol-Oriented Programming **Description:** Defines abstract interfaces (protocols) and provides concrete implementations using records or types. **Do This:** * Define protocols for common operations or behaviors. * Implement protocols for different data types or components. * Use protocols to decouple components and promote code reuse. **Don't Do This:** * Create overly complex protocols with too many functions. * Implement protocols for trivial operations. **Why:** Enables polymorphism and extensibility, allowing you to add new functionality without modifying existing code. **Example:** Note the use of clojure.spec.alpha to validate that implementations of the protocol meet the expected contract. This is good practice to ensure protocol implementations are correct. """clojure (ns my-app.protocols (:require [clojure.spec.alpha :as s])) ;; Define a protocol (defprotocol Loggable (log [this message] "Logs a message.")) ;; Define a specification for the log function (s/fdef log :args (s/cat :this any? :message string?) :ret any?) ; Adjust 'any?' to a more specific return type if applicable ;; Implement the protocol for a record (defrecord User [id name email] Loggable (log [this message] (println (str "User " id ": " message)))) ;; Example usage (def user (->User 123 "Alice" "alice@example.com")) (log user "User logged in.") """ **Anti-Pattern:** Using concrete types directly instead of relying on protocols. ### 3.2. State Management with Atoms and Refs **Description:** Clojure provides built-in mechanisms for managing mutable state in a concurrent environment. Leverage "atom" for coordinated atomic updates, and "ref" for coordinated, transactional updates. Use "agent" sparingly, as it offers less control over timing. **Do This:** * Use "atom" for simple, independent state updates. * Use "ref" for complex, coordinated updates that require transactions. * Use "swap!" to update atoms atomically. * Use "dosync" to perform transactional updates on refs. * Consider using libraries like "mount" or "component" to manage application state. **Don't Do This:** * Use mutable global variables without proper synchronization. * Perform long-running or blocking operations within transactions. **Why:** Ensures data consistency and prevents race conditions in concurrent applications. **Example (using "atom"):** """clojure (def counter (atom 0)) (defn increment-counter "Increments the counter atomically." [] (swap! counter inc)) (increment-counter) (println @counter) """ **Example (using "ref"):** """clojure (def account1 (ref 100)) (def account2 (ref 50)) (defn transfer "Transfers money from one account to another." [amount from to] (dosync (alter from - amount) (alter to + amount))) (transfer 20 account1 account2) (println @account1) (println @account2) """ **Anti-Pattern:** Directly modifying the value of an atom without using "swap!". ### 3.3. Asynchronous Programming (using "core.async") **Description:** Clojure provides "core.async" for managing asynchronous operations using channels and go blocks. **Do This:** * Use "core.async" to handle I/O-bound operations without blocking the main thread. * Use channels to communicate between different parts of the application asynchronously. * Use "go" blocks to perform computations concurrently. * Utilize "alts!!" or "alts!" for non-blocking channel operations **Don't Do This:** * Perform CPU-intensive operations within "go" blocks. Use "pmap" or "future" for these. * Block the main thread with synchronous I/O operations. **Why:** Improves responsiveness and scalability by allowing the application to handle multiple requests concurrently. **Example:** """clojure (ns my-app.async (:require [clojure.core.async :as async :refer [chan go >! <! alts!]])) (defn process-data "Simulates a long-running process." [data] (Thread/sleep 1000) (str "Processed: " data)) (defn start-worker "Starts a worker that processes data from a channel." [input-chan output-chan] (go (loop [] (let [data (<! input-chan)] (when data (let [result (process-data data)] (>! output-chan result) (recur))))))) (defn example-async [] (let [input-chan (chan) output-chan (chan) _ (start-worker input-chan output-chan)] ; start worker in background (go (>! input-chan "Data 1") (>! input-chan "Data 2") (async/close! input-chan)) (go (loop [] (let [[val port] (alts! [output-chan (async/timeout 5000)])] (cond (= port output-chan) (do (println (str "Received: " val)) (recur)) :else (println "Timeout or channel closed"))))))) """ **Anti-Pattern:** Performing synchronous HTTP requests directly within a Ring handler. ## 4. Clojure Ecosystem and Tooling Leveraging the Clojure ecosystem's tools and libraries can significantly improve productivity and code quality. ### 4.1. Leiningen/Boot **Do This:** * Use Leiningen or Boot as the build tool for managing dependencies, running tests, and creating deployments. * Define dependencies explicitly in the "project.clj" or "build.boot" file. * Use Leiningen plugins to automate common tasks (e.g., linting, formatting). **Don't Do This:** * Manually manage dependencies by downloading JAR files. * Use outdated versions of Leiningen or Boot. **Why:** Provides a standardized and automated build process. ### 4.2. REPL-Driven Development **Do This:** * Use the REPL (Read-Eval-Print Loop) extensively for interactive development and experimentation. * Load code into the REPL using "load-file" or "require". * Use the REPL to test functions and explore data structures. * Use tools like "cider" or "refactor-nrepl" to enhance the REPL experience. **Don't Do This:** * Rely solely on unit tests without exploring the code in the REPL. * Restart the REPL frequently, instead leverage tools to reload your code. **Why:** Enables rapid prototyping, debugging, and iterative development. ### 4.3. Testing **Do This:** * Write comprehensive unit tests using "clojure.test". * Use mocking libraries like "with-redefs" or "mockfn" to isolate units of code. * Write integration tests to verify the interaction between different components. * Use generative testing libraries like "clojure.test.check" to automatically generate test cases. * Use "lein test" or "boot test" to run the test suite. * Run tests frequently during development. **Don't Do This:** * Skip writing tests altogether. * Write tests that are too brittle or tightly coupled to the implementation. **Why:** Ensures code correctness, prevents regressions, and facilitates refactoring. **Example:** """clojure (ns my-app.core-test (:require [clojure.test :refer :all] [my-app.core :as core])) (deftest calculate-discount-test (testing "Calculates discount for regular customer" (is (= 90 (core/calculate-discount :regular 100)))) (testing "Calculates discount for premium customer" (is (= 80 (core/calculate-discount :premium 100))))) """ ### 4.4. Linting and Formatting **Do This:** * Use a linter like Eastwood or Kibit to identify potential code issues. * Use a formatter like cljfmt to automatically format the code according to a consistent style. * Configure the IDE to automatically run the linter and formatter on save. * Configure your editor to automatically remove trailing whitespace on save. **Don't Do This:** * Ignore linting warnings or formatting issues. * Relying on developers to manually cleanup formatting when tools can automate this. **Why:** Improves code quality, consistency, and readability. ## 5. Conclusion Adhering to these core architecture standards provides a solid foundation for building robust, maintainable, and scalable Clojure applications. Consistent application of these guidelines leads to higher quality code, reduced technical debt, and increased developer productivity. This document should be reviewed and updated periodically to reflect the evolving best practices in the Clojure community.
# Component Design Standards for Clojure This document outlines the component design standards for Clojure, focusing on creating reusable, maintainable, and performant components. It is intended to guide developers and inform AI coding assistants to produce idiomatic and high-quality Clojure code. It is specifically tailored to component design patterns in Clojure, and should be viewed as a single rule in a larger set of standards. ## 1. General Principles ### 1.1. Definition of a Component In Clojure, a component is a self-contained unit of functionality that encapsulates data and behavior. Components should be designed to be composable, independent, and easily testable. They should have clear, well-defined interfaces and responsibilities. ### 1.2. Key Principles of Component Design * **Single Responsibility Principle (SRP):** Each component should have one, and only one, reason to change. * **Open/Closed Principle (OCP):** Components should be open for extension but closed for modification. * **Liskov Substitution Principle (LSP):** Subtypes must be substitutable for their base types without altering the correctness of the program. While less directly applicable in Clojure with its focus on data and protocols, this principle translates to ensuring that functions accepting one type of data can also reliably handle variations or extensions of that data. * **Interface Segregation Principle (ISP):** Clients should not be forced to depend on methods they do not use. In Clojure, this suggests favoring small, focused protocols over large, monolithic ones. * **Dependency Inversion Principle (DIP):** High-level modules should not depend on low-level modules. Both should depend on abstractions. ### 1.3. Why These Principles Matter These principles are crucial for: * **Maintainability:** Easier to understand, modify, and debug components. * **Reusability:** Components can be used in different parts of the application or in different applications altogether. * **Testability:** Independent components are easier to test in isolation. * **Scalability:** Well-designed components facilitate easier scaling and distribution of the application. ## 2. Component Composition Approaches ### 2.1. Function Composition Clojure's functional nature makes function composition a natural way to build components. **Do This:** * Use "comp" to combine functions into a new function. * Leverage transducers for efficient data transformations. **Don't Do This:** * Create overly complex, deeply nested function compositions that are hard to read. * Depend excessively on mutable state within composed functions. **Example:** """clojure (defn add-one [x] (+ x 1)) (defn square [x] (* x x)) ; Compose functions to square a number and then add one (def square-and-add-one (comp add-one square)) (println (square-and-add-one 5)) ; Output: 26 ; Using transducers for efficient composition (def increment-and-square (comp (map inc) (map #(* % %)))) (println (into [] increment-and-square [1 2 3])) ; Output: [4 9 16] """ **Explanation:** Function composition allows combining simple functions into more complex operations. Transducers enhance this by providing a way to compose transformations on collections without creating intermediate collections. ### 2.2. Data-Driven Composition Components can be designed to operate on data structures. **Do This:** * Use data structures (maps, vectors, sets) as the primary means of communication between components. * Define functions that operate on these data structures to perform specific tasks. * Use schema libraries like "clojure.spec.alpha" or "malli" to define the structure and validity of your data. **Don't Do This:** * Pass large, mutable stateful objects between components. * Rely on side effects to communicate between components. **Example:** """clojure (require '[clojure.spec.alpha :as s]) ; Define a spec for a user (s/def ::user (s/keys :req-un [::id ::name ::email])) (s/def ::id uuid?) (s/def ::name string?) (s/def ::email string?) ; A function to validate a user (defn validate-user [user] (s/valid? ::user user)) ; A function to format a user's name (defn format-user-name [user] (str "User: " (:name user))) (def user-data {:id (random-uuid) :name "Alice" :email "alice@example.com"}) (println (validate-user user-data)) ; Output: true (println (format-user-name user-data)) ; Output: User: Alice """ **Explanation:** This approach emphasizes data structures as the central point of interaction. Using schemas further solidifies the interfaces between components, making them more robust and easier to reason about. ### 2.3. Protocol-Based Composition Protocols define interfaces that components can implement. This allows for polymorphism and extensibility. **Do This:** * Define protocols that represent the capabilities of a component. * Implement these protocols for different data types or data structures. * Use protocols to abstract away implementation details. **Don't Do This:** * Create overly large protocols with many methods. * Violate the Liskov Substitution Principle by implementing protocols inconsistently. **Example:** """clojure (defprotocol Printable (to-string [this] "Converts the object to a string representation.")) (extend-protocol Printable java.lang.String (to-string [this] this) ; Strings are already printable clojure.lang.PersistentVector (to-string [this] (str "[" (clojure.string/join ", " (map to-string this)) "]")) java.lang.Integer (to-string [this] (str this))) (println (to-string "Hello")) ; Output: Hello (println (to-string [1 2 "World"])) ; Output: [1, 2, World] """ **Explanation:** Protocols enable polymorphism. Different data types can implement the same protocol, providing a consistent interface. "extend-protocol" provides a flexible way to add protocol implementations to existing types. ### 2.4. Component Libraries and Frameworks Several libraries and frameworks facilitate component-based development in Clojure. * **Component:** A library for managing component lifecycles (start, stop). * **Integrant:** A configuration-based system for building applications from components controlled by a data structure. * **Mount:** A simpler alternative to Component for managing application state. * **System:** A newer library aiming to combine the best aspects of Component and Integrant. **Do This:** * Choose a component library appropriate for your project's complexity. * Use the library's lifecycle management features to manage component dependencies and state. * Employ configuration-based systems like Integrant to externalize component configuration. **Don't Do This:** * Manually manage component lifecycles without a library. * Hardcode component dependencies within the component itself. **Example (using Integrant):** """clojure (ns my-app (:require [integrant.core :as ig])) ; Define a database component (defmethod ig/init-key ::db [_ {:keys [url]}] (println "Connecting to DB at" url) {:connection url}) ; In a real app, establish an actual DB connection (defmethod ig/halt-key ::db [_ db] (println "Disconnecting from DB at" (:connection db))) ; Close the connection ; Define an HTTP server component (defmethod ig/init-key ::server [_ {:keys [port db]}] (println "Starting server on port" port "using DB" db) {:port port :db db}) ; In a real app, start an HTTP server (defmethod ig/halt-key ::server [_ server] (println "Stopping server on port" (:port server))) ; Stop the server ; Define a configuration (def config {::db {:url "jdbc://localhost:5432/mydb"} ::server {:port 8080 :db (ig/ref ::db)}}) ; Start the system (def system (ig/init config)) ; Stop the system when done (ig/halt! system) """ **Explanation:** Integrant allows defining components (like "::db" and "::server") and their dependencies in a configuration map. "ig/init" starts the system, resolving dependencies using references (e.g., "(ig/ref ::db)"). "ig/halt!" shuts down the system in the reverse order. The "ig/init-key" and "ig/halt-key" multimethods define the initialization and termination logic for each component. This removes boilerplate from your component implementation and centralizes configuration. ## 3. Component Communication ### 3.1. Asynchronous Messaging For decoupled components, consider asynchronous messaging using libraries like "core.async" or message queues (e.g., RabbitMQ, Kafka). **Do This:** * Use channels ("core.async") or message queues for non-blocking communication. * Define clear message formats using data structures and schemas. **Don't Do This:** * Overuse asynchronous messaging when synchronous calls are sufficient. * Create complex message routing logic within components. **Example (using core.async):** """clojure (require '[clojure.core.async :as async]) ; Create a channel (def message-channel (async/chan)) ; Component 1 (producer) (defn send-message [message] (async/>!! message-channel message)) ; Blocking send ; Component 2 (consumer) (defn receive-message [] (async/<!! message-channel)) ; Blocking receive ; Example Usage (future (send-message "Hello from Component 1")) (println (receive-message)) ; Output: Hello from Component 1 """ **Explanation:** "core.async" provides channels for asynchronous communication. The "send-message" function sends a message to the channel, and "receive-message" retrieves it. The "future" macro runs the sender in a separate thread. Non-blocking versions of these operations are available (">!", "<!"). ### 3.2. Publish/Subscribe (Pub/Sub) For components that need to react to events, consider using a publish/subscribe pattern. Libraries like "clojure.tools.namespace.repl" use this pattern internally. **Do This:** * Use a dedicated pub/sub library (e.g., implementing a simple one with atoms and callbacks). * Define clear event types and data structures. **Don't Do This:** * Create tightly coupled event listeners. * Overuse global event buses. **Example (Simple Pub/Sub implementation):** """clojure (def event-bus (atom {})) (defn subscribe [event-type callback] (swap! event-bus update event-type (fn [callbacks] (conj (or callbacks []) callback)))) (defn publish [event-type event-data] (doseq [callback (get @event-bus event-type)] (callback event-data))) ; Example Usage (subscribe :user-created (fn [user] (println "User created:" user))) (subscribe :user-created (fn [user] (println "Sending welcome email to:" (:email user)))) (publish :user-created {:name "Bob" :email "bob@example.com"}) """ **Explanation:** This demonstrates a basic pub/sub system managed with an atom. "subscribe" registers a callback for a specific event type. "publish" triggers all callbacks associated with that event type, passing event data to each. ### 3.3. Services and APIs Expose component functionality as services or APIs using libraries like Ring/Compojure or shadow-cljs for frontend components. **Do This:** * Define clear API contracts using schemas or Swagger/OpenAPI. * Implement proper authentication and authorization. **Don't Do This:** * Expose internal component implementation details in the API. * Neglect security considerations. ## 4. Testing ### 4.1. Unit Testing Test individual components in isolation. **Do This:** * Use "clojure.test" or a testing framework like Midje. * Mock dependencies when necessary. * Write clear and concise test cases. **Don't Do This:** * Write tests that are tightly coupled to implementation details. * Neglect edge cases and error handling. **Example:** """clojure (ns my-app.core-test (:require [clojure.test :refer :all] [my-app.core :refer :all])) (deftest test-add-one (testing "Should add one to a number" (is (= (add-one 5) 6)) (is (= (add-one -1) 0)))) """ ### 4.2. Integration Testing Test how components interact with each other. **Do This:** * Test the entire system or a significant portion. * Use realistic test data. * Verify that components communicate correctly. **Don't Do This:** * Skip integration testing. * Assume that components work together correctly without testing. ### 4.3. Component Lifecycle Testing If using a component lifecycle library like "component" or "integrant", verify the start and stop behavior of components. **Do This:** * Write tests that start and stop components. * Verify that resources are acquired and released correctly. * Ensure that dependencies are started in the correct order. ## 5. Error Handling ### 5.1. Exceptions Use exceptions to signal exceptional conditions. **Do This:** * Throw exceptions when errors occur. * Catch exceptions at appropriate boundaries. * Provide informative error messages. **Don't Do This:** * Ignore exceptions. * Use exceptions for normal control flow. ### 5.2. Error Values Return error values (e.g., "nil", "false", or a tagged union) to indicate errors. **Do This:** * Use error values when exceptions are not appropriate. * Check for error values and handle them appropriately. **Don't Do This:** * Ignore error values. * Assume that all operations succeed. ### 5.3. Logging Log errors and other important events. **Do This:** * Use a logging library like "clojure.tools.logging". * Log at appropriate levels (e.g., "error", "warn", "info", "debug"). * Include relevant context in log messages. **Don't Do This:** * Log too much information. * Log sensitive information. ## 6. Modern Clojure Features ### 6.1. clojure.spec.alpha and Malli Use "clojure.spec.alpha" or "malli" extensively for data validation, generation, and documentation. **Do This:** * Define specs/schemas for all data structures used in your application. * Use "s/valid?" or "malli.core/validate" to validate data. * Use "s/gen" or "malli.core/generate" for property-based testing. **Don't Do This:** * Neglect data validation. * Assume that all data is valid. ### 6.2. Datafy/Nav Consider using "datafy" and "nav" (Clojure 1.10+) to provide a consistent way to inspect and navigate data structures. This can improve debugging and introspection. **Do This:** * Implement "datafy" and "nav" for custom data types. * Use the "datafy" function in debugging tools. ## 7. Anti-Patterns ### 7.1. God Object Avoid creating large, monolithic components that do too much. ### 7.2. Tight Coupling Minimize dependencies between components. ### 7.3. Premature Optimization Don't optimize components before they are needed. ### 7.4. Reinventing the Wheel Use existing libraries and frameworks when possible. ## 8. Performance Optimization ### 8.1. Immutability Leverage Clojure's immutable data structures for performance and concurrency benefits. ### 8.2. Laziness Use lazy sequences efficiently to avoid unnecessary computation. ### 8.3. Concurrency Use Clojure's concurrency features (e.g., atoms, refs, agents, core.async) to improve performance. Be extremely cautious and deliberate using these as they are easy to misuse. ### 8.4. Profiling Use profiling tools to identify performance bottlenecks. ## 9. Security Best Practices ### 9.1. Input Validation Validate all user input to prevent injection attacks. ### 9.2. Authentication and Authorization Implement proper authentication and authorization to protect sensitive data. ### 9.3. Dependency Management Keep dependencies up to date to patch security vulnerabilities. By adhering to these component design standards, Clojure developers can create applications that are maintainable, reusable, testable, and performant. This document provides a framework for building high-quality Clojure code.
# State Management Standards for Clojure This document outlines the standards and best practices for managing state in Clojure applications. It's intended to guide developers in creating maintainable, performant, and robust systems. These guidelines reflect current Clojure best practices and are applicable to modern Clojure development. ## 1. Principles of State Management in Clojure Clojure, as a functional programming language, emphasizes immutability. However, real-world applications require managing state. The key is to manage state in a controlled and predictable manner. * **Immutability as the Default:** Favor immutable data structures whenever possible. This simplifies reasoning about code, avoids side effects, and enables concurrency. * **Explicit State Management:** Make state transitions explicit and avoid implicit modification. This improves auditability and reduces the risk of unintended consequences. * **Controlled Concurrency:** Clojure provides powerful concurrency primitives (atoms, refs, agents, vars). Choose the appropriate primitive based on the specific concurrency requirements. * **Reactivity and Dataflow:** Consider using libraries like "integrant", Component, "re-frame", "Fulcro", or plain clojure.spec/watch for managing application lifecycle and data flow, especially in larger applications with complex dependencies and reactivity requirements. ## 2. Vars: Global Mutable State (Use with Caution!) Vars are the simplest mechanism for managing state in Clojure, providing thread-local mutable storage. However, overuse can lead to global state issues. ### 2.1. Standards for Using Vars * **Do This:** Use vars for configuration values that rarely change or for thread-local storage. * **Don't Do This:** Use vars for application state that is frequently modified or shared between threads unless thread isolation is explicitly managed. ### 2.2. Why This Matters Global mutable state (vars) can make code harder to reason about, especially in concurrent environments. Uncontrolled mutation can lead to race conditions and unpredictable behavior. ### 2.3. Code Examples """clojure (ns my-app.config (:defonce +config+ (atom {}))) ; Use defonce to initialize only once (defn set-config! [key value] (swap! +config+ assoc key value)) (defn get-config [key] (@+config+ key)) ;; Example Usage: (set-config! :database-url "jdbc:...") (println (get-config :database-url)) """ ### 2.4 Anti-patterns * **Over-reliance on Vars:** Using vars for core application logic instead of local bindings and immutable data flow. * **Uncontrolled Mutation:** Modifying vars without consideration for concurrency or data consistency. ## 3. Atoms: Mutable References Atoms provide a mechanism for managing mutable state with atomic updates. ### 3.1. Standards for Using Atoms * **Do This:** Use atoms for managing shared mutable state that requires atomic updates. Use "swap!" and "compare-and-set!" for modifying the atom's value safely. * **Don't Do This:** Perform long-running or potentially blocking operations within "swap!" as this can degrade performance. ### 3.2. Why This Matters Atoms ensure that updates to the state are atomic, preventing race conditions and data corruption in concurrent environments. ### 3.3. Code Examples """clojure (def counter (atom 0)) (defn increment! [] (swap! counter inc)) (defn get-count [] @counter) ;; Example Usage: (future (dotimes [_ 1000] (increment!))) (future (dotimes [_ 1000] (increment!))) (Thread/sleep 100) ; give futures some time to complete (println (get-count)) ; should be close to 2000 """ ### 3.4. Anti-patterns * **Direct Dereferencing without Atomic Updates:** Directly dereferencing the atom ("@atom-name") and then modifying and resetting it, leading to possible race conditions. Always use "swap!" or "compare-and-set!". * **Using Atoms for Immutable Data:** Using atoms to hold immutable data structures needlessly. * **Over-contention:** When multiple threads are constantly trying to update the same atom, it can lead to contention and performance issues. ## 4. Refs and Transactions Refs provide transactional state management, ensuring that a series of operations are performed atomically. ### 4.1. Standards for Using Refs * **Do This:** Use refs for managing shared state where consistency across multiple, related values is crucial. Update refs within "dosync" blocks to ensure transactional semantics. * **Don't Do This:** Perform I/O operations or other side effects within "dosync" blocks as these can violate the atomicity and isolation guarantees. ### 4.2. Why This Matters Refs guarantee ACID (Atomicity, Consistency, Isolation, Durability) properties for state updates, crucial for maintaining data integrity in complex systems. ### 4.3. Code Examples """clojure (def account1 (ref 100)) (def account2 (ref 0)) (defn transfer! [amount] (dosync (if (>= @account1 amount) (do (alter account1 #(- % amount)) (alter account2 #(+ % amount))) (throw (Exception. "Insufficient funds"))))) ;; Example Usage: (try (transfer! 50) (println "Transfer successful") (catch Exception e (println "Transfer failed:" (.getMessage e)))) (println "Account 1:" @account1) ; 50 (println "Account 2:" @account2) ; 50 """ ### 4.4. Anti-patterns * **Performing I/O in Transactions:** Performing potentially failing I/O operations inside "dosync" blocks. * **Long-running Transactions:** Holding transactions open for extended periods, leading to contention and reduced concurrency. ## 5. Agents: Asynchronous State Updates Agents provide a mechanism for managing asynchronous state updates. ### 5.1. Standards for Using Agents * **Do This:** Use agents for performing asynchronous, potentially long-running operations that update state. * **Don't Do This:** Rely on agents for immediate, synchronous updates. Agents are designed for asynchronous processing. ### 5.2. Why This Matters Agents allow decoupling state updates from the main thread, improving responsiveness and preventing blocking. ### 5.3. Code Examples """clojure (def logger (agent [])) (defn log-message [message] (send logger conj message)) ;; Use send to ensure proper agent queueing (defn get-logs [] @logger) ;;Example Usage: (log-message "Starting process...") (log-message "Processing data...") (log-message "Process completed.") (await-for 100 logger) ; Wait for agent to process all messages (println (get-logs)) """ ### 5.4. Anti-patterns * **Synchronous Agent Use:** Using "send-off" when "send" is required, resulting in unintended asynchronous dispatch, or vice versa. * **Ignoring Agent Errors:** Failing to handle exceptions thrown during agent actions; agents halt on exceptions by default. Use "set-error-handler!" on each agent, even if the handler just logs the error. ## 6. Component and Integrant: Managing Application Lifecycle and State Dependencies Component and Integrant are libraries that provide a structured approach to managing application lifecycle and dependencies. ### 6.1. Standards for Using Component/Integrant * **Do This:** Use Component or Integrant for applications with more than a few basic services/dependencies and especially when application shutdown is an important consideration. Define components as records implementing "Lifecycle" protocol (for Component) or use the component keys and init/halt functions as the core of Integrant. * **Don't Do This:** Build complex and difficult-to-test systems manually managing startup/shutdown state. ### 6.2. Why This Matters These libraries offer a declarative way to define application components, their dependencies, and their lifecycle (start/stop). This promotes modularity, testability, and maintainability. Integrant works with plain data and functions making it a great fit for Clojure. ### 6.3. Code Examples (Integrant) """clojure (ns my-app.system (:require [integrant.core :as ig])) (defmethod ig/init-key :db/database [_ config] (println "Initializing database with config:" config) {:connection (atom {})}) ; Replace with actual DB initialization (defmethod ig/halt-key :db/database [_ db] (println "Closing database connection") ;; Close DB connection here ) (defmethod ig/init-key :web/server [_ {:keys [db port]}] (println "Starting web server on port" port) {:server (atom {:db db :port port})}) ; Simulate a web server (defmethod ig/halt-key :web/server [_ server] (println "Stopping web server") ;; Stop web server here ) (def system-config {:db/database {:url "jdbc:..."} :web/server {:db (ig/ref :db/database) :port 8080}}) ;; Example usage: (def system (ig/init system-config)) ;; ... application runs ... (ig/halt! system) """ ### 6.4. Anti-patterns * **Ignoring Component Lifecycle:** Failing to properly start and stop components, leading to resource leaks or unexpected behavior. * **Tight Coupling:** Creating tight dependencies between components, reducing modularity and testability. ## 7. Managing Reactivity with "re-frame" and Fulcro For UI-intensive applications, libraries like "re-frame" and Fulcro provide a structured way to manage application state and reactivity. ### 7.1. Standards for Using "re-frame" and Fulcro * **Do This:** Adopt "re-frame"'s event-driven architecture or Fulcro's data-driven (normalized) approach for managing application state in complex UIs. Use subscriptions for deriving UI state from the central data store. * **Don't Do This:** Mutate application state directly within UI components, bypassing the "re-frame" event handling or Fulcro mutations. ### 7.2. Why This Matters These libraries provide a predictable and efficient mechanism for updating the UI in response to application state changes. ### 7.3. Code Example ("re-frame") """clojure (ns my-app.re-frame (:require [re-frame.core :as rf])) ;;Define an event handler (rf/reg-event-fx :increment (fn [{:keys [db]} [_]] {:db (update db :counter inc)})) ;; Define a subscription (rf/reg-sub :counter (fn [db [_]] (:counter db))) ;; Usage in a component (Om Next example) (defn my-component [] (let [counter (rf/subscribe [:counter])] (fn [] (dom/div (dom/h1 (str "Counter: " @counter)) (dom/button {:on-click #(rf/dispatch [:increment])} "Increment"))))) """ ### 7.4. Anti-patterns * **Direct State Mutation:** Modifying the "re-frame" or Fulcro application database directly instead of using events or mutations. * **Complex Subscriptions:** Creating overly complex subscriptions that perform heavy computations, impacting UI performance. ## 8. Metadata and Watches Clojure provides a mechanism for attaching metadata to data structures and setting up watches to observe changes to vars, atoms, refs, and agents. ### 8.1. Standards for metadata and watches * **Do This:** Use metadata to attach auxiliary information to data structures without modifying their core functionality. Use watches to observe changes in state variables during development and debugging or react to state changes in production. * **Don't Do This:** Overuse watches in production as they can negatively affect performance. ### 8.2. Why This Matters Metadata allows associating additional information with data without altering its structure or semantics. Watches provides notifications when the value of a stateful value changes. ### 8.3. Code Examples """clojure ;; Using metadata (def my-vector (with-meta [1 2 3] {:description "A simple vector"})) (println (meta my-vector)) ;; Prints: {:description "A simple vector"} ;; Using watches (def my-atom (atom 0)) (add-watch my-atom :my-watch (fn [key atom old-state new-state] (println "Atom" key "changed from" old-state "to" new-state))) (swap! my-atom inc) ;; Output: Atom :my-watch changed from 0 to 1 (remove-watch my-atom :my-watch) """ ### 8.4. Anti-patterns * **Over-reliance on Watches for Core Logic:** Do not implement business logic inside of watch functions as they are for debugging. * **Ignoring Watch Performance:** Using watches in performance-critical sections of code without considering their impact. * **Abusing Metadata:** Attaching critical application data or logic to metadata, making it less discoverable and harder to maintain. ## 9. General Recommendations * **Choose the Right Tool:** Select the appropriate state management mechanism based on the specific requirements of your application. * **Keep State Local:** Minimize the scope of mutable state to reduce complexity and improve testability. * **Embrace Immutability:** Favor immutable data structures whenever possible to simplify reasoning about code and prevent side effects. * **Test Thoroughly:** Write comprehensive tests to ensure that state transitions are correct and that concurrent access is handled safely. * **Monitor Performance:** Use performance monitoring tools to identify and address any bottlenecks related to state management. * **Avoid Legacy Code Patterns:** Clojure evolves, avoid techniques that are out of date or have known drawbacks. * **Always document the reason behind state selection**: Explain briefly why a specific approach for managing state has been selected in a particular context. By following these standards, you can create Clojure applications that are maintainable, performant, and robust. Remember that this document should be adapted according to particular use cases and team conventions.
# Performance Optimization Standards for Clojure This document outlines performance optimization standards for Clojure development. These standards aim to improve application speed, responsiveness, and resource utilization. They are tailored to Clojure's unique characteristics and leverage modern approaches. ## 1. Architectural Considerations ### 1.1 Data Structures and Algorithms **Standard:** Choose the right data structure and algorithm for the specific task. Consider time and space complexity trade-offs. **Why:** Selecting an appropriate data structure and algorithm can significantly impact performance. For example, using a "set" for membership testing is much faster than iterating through a "list". **Do This:** * Understand the performance characteristics of Clojure's data structures: "list", "vector", "map", "set". * Analyze algorithmic complexity (Big O notation) when choosing algorithms. * Profile code to identify performance bottlenecks. **Don't Do This:** * Use "list" for random access. Prefer "vector". * Use linear search on sorted data. Use a binary search implementation or leverage sorted sets/maps. **Example:** """clojure ;; Efficient membership testing using a set (def my-set (set (range 10000))) (time (contains? my-set 5000)) ; Evaluates very quickly ;; Inefficient membership testing using a list (def my-list (list (range 10000))) (time (some #(= 5000 %) my-list)) ; Considerably slower """ ### 1.2 Concurrency and Parallelism **Standard:** Utilize Clojure's concurrency features (atoms, refs, agents, and "pmap") appropriately to leverage multi-core processors. **Why:** Concurrency can dramatically improve performance for CPU-bound tasks. However, incorrect concurrency can introduce race conditions and deadlocks, harming performance. **Do This:** * Use "pmap" for embarrassingly parallel operations on collections (map-like operations where each element can be processed independently). * Use atoms for simple state management that doesn't require coordination between multiple operations. * Use refs with "dosync" for coordinated state changes that require transactions. * Use agents for asynchronous operations that don't require immediate results. * Carefully manage contention and locking. **Don't Do This:** * Overuse concurrency. Adding unnecessary threads can increase overhead. * Use "future" without understanding its limitations (e.g., eager evaluation). * Ignore exceptions thrown in concurrent computations. **Example:** """clojure ;; Parallel processing of a collection using pmap (defn square [x] (* x x)) (time (pmap square (range 1 10000))) ;; Atomic counter increment (def counter (atom 0)) (defn increment [] (swap! counter inc)) """ ### 1.3 Lazy Evaluation **Standard:** Leverage lazy evaluation for efficiency, but be aware of potential pitfalls. **Why:** Lazy evaluation can defer computation until needed, saving processing time and memory. However, it can also lead to unexpected performance issues if not managed correctly (e.g., holding onto the head of a sequence). **Do This:** * Use lazy sequences ("map", "filter", "range") for large or infinite data sets. * Realize only the necessary portion of a lazy sequence. * Use "doall" or "dorun" to force evaluation when side effects are required. **Don't Do This:** * Hold onto the head of a lazy sequence for extended periods, preventing garbage collection. * Chain together too many lazy operations without realizing intermediate results. This can create excessive stack usage. **Example:** """clojure ;; Lazy sequence example (def numbers (range 1000000)) (def even-numbers (filter even? numbers)) ;; Only realize the first 10 even numbers (take 10 even-numbers) ;; Force evaluation to prevent head retention: (dorun (map prn (take 10 even-numbers))) """ ### 1.4 Reducing Object Creation **Standard:** Minimize unnecessary object creation to reduce garbage collection overhead. **Why:** Frequent object creation and garbage collection can be a major performance bottleneck, especially in long-running applications. **Do This:** * Use transients for mutable operations on data structures within a local scope. * Prefer primitive operations ("unchecked-*") for numerical calculations when possible. * Reuse existing objects when applicable. **Don't Do This:** * Create temporary objects unnecessarily. * Rely solely on immutable data structures without considering the overhead of creating new copies. **Example:** """clojure ;; Using transients for efficient vector modification (defn modify-vector [v] (persistent! (reduce (fn [acc i] (assoc! acc i (* i 2))) (transient v) (range (count v))))) (def my-vector (vec (range 1000))) (time (modify-vector my-vector)) ;; Using unchecked math (defn sum-range [n] (loop [i 0 acc 0] (if (>= i n) acc (recur (inc i) (unchecked-add acc i))))) (time (sum-range 1000000));Evaluates much fasters than normal + function because of primtive math. """ ## 2. Coding Practices ### 2.1 Function Performance **Standard:** Write efficient functions that minimize unnecessary computations. **Why:** The performance of individual functions directly impacts the overall application performance. **Do This:** * Use memoization for pure functions with expensive calculations. * Employ tail-recursion for efficient looping. * Use function arities to improve dispatch performance. * Avoid unnecessary type checking. **Don't Do This:** * Perform redundant calculations within a function. * Create functions that are too large or complex. Break them down into smaller, more manageable units. **Example:** """clojure ;; Memoization example (def fibonacci (memoize (fn [n] (if (<= n 1) n (+ (fibonacci (- n 1)) (fibonacci (- n 2))))))) (time (fibonacci 30)) ; First call takes longer (time (fibonacci 30)) ; Subsequent calls are much faster ;; Tail-recursive function for summing a sequence (defn sum-seq [s] (loop [s s acc 0] (if (empty? s) acc (recur (rest s) (+ acc (first s)))))) """ ### 2.2 I/O Optimization **Standard:** Optimize input/output operations to minimize latency. **Why:** I/O operations are often the slowest part of an application. **Do This:** * Use buffered I/O for reading and writing files. * Batch multiple requests in database and network communications. * Use appropriate data serialization formats (e.g., Protocol Buffers, edn instead of JSON for Clojure-to-Clojure communication). Minimize the size of serialized data. * Use "slurp" and "spit" judiciously for small files, but use streaming approaches for large files. **Don't Do This:** * Perform frequent small I/O operations. * Read entire files into memory if only a portion is needed. * Block the UI thread with I/O operations. **Example:** """clojure ;; Buffered file reading (with-open [r (clojure.java.io/reader "large-file.txt" :encoding "UTF-8" :buffer-size 8192)] (doseq [line (line-seq r)] (println line))) ;; Asynchronous I/O using futures (defn process-data [data] (future (println "Processing data:" data))) (process-data "Some large data set") ;;Using streams (from clojure.java.io) (with-open [input-stream (clojure.java.io/input-stream "large-file.bin")] (let [buffer (byte-array 4096)] (loop [bytes-read (.read input-stream buffer)] (when (pos? bytes-read) ;; Process the buffer (byte-array) here (println "Read " bytes-read " bytes") (recur (.read input-stream buffer)))))); Avoids loading everything in memory """ ### 2.3 String Manipulation **Standard:** Use efficient string manipulation techniques. **Why:** String operations can be performance-intensive, especially when dealing with large strings or frequent concatenations. **Do This:** * Use "StringBuilder" for efficient string concatenation. * Use regular expressions carefully, being mindful of backtracking and compilation overhead. Consider caching compiled patterns using "re-pattern". * Prefer "subs" over creating new strings when extracting substrings. **Don't Do This:** * Repeatedly concatenate strings using "str" in a loop. * Use complex regular expressions unnecessarily. * Convert to and from strings excessively. **Example:** """clojure ;; Efficient string concatenation using StringBuilder (defn build-string [n] (let [sb (StringBuilder.)] (dotimes [i n] (.append sb i)) (.toString sb))) (time (build-string 10000)) ;; Cached regular expression pattern (def pattern (re-pattern "abc")) (re-find pattern "abcdefg") """ ## 3. Tooling and Debugging ### 3.1 Profiling **Standard:** Use profiling tools to identify performance bottlenecks. **Why:** Profiling helps pinpoint the areas of code that consume the most time and resources. **Do This:** * Use the "criterium" library for microbenchmarking. * Use profiling tools like VisualVM or YourKit for JVM-level profiling. * Use Clojure's built-in "time" macro for simple timing. **Don't Do This:** * Guess at performance bottlenecks. * Optimize prematurely without profiling. **Example:** """clojure ;; Using criterium for benchmarking (require '[criterium.core :as c]) (c/bench (reduce + (range 10000))) (c/quick-bench (reduce + (range 10000))) ;; Timing macro example (time (reduce + (range 10000))) """ ### 3.2 Memory Management **Standard:** Monitor memory usage to prevent memory leaks and excessive garbage collection. **Why:** Memory leaks and excessive garbage collection can lead to performance degradation and application instability. **Do This:** * Use memory profiling tools to identify memory leaks. * Be mindful of object lifetimes and release resources when no longer needed. * Avoid creating large, temporary data structures. **Don't Do This:** * Ignore memory usage patterns. * Assume that garbage collection will solve all memory-related problems. ### 3.3 Logging and Monitoring **Standard:** Implement adequate logging and monitoring to track application performance in production. **Why:** Logging and monitoring provide valuable insights into application behavior, enabling proactive identification and resolution of performance issues. **Do This:** * Use a logging library (e.g., "tools.logging") to record performance metrics. * Monitor key performance indicators (KPIs) such as response time, throughput, and CPU utilization. * Set up alerts for performance degradation. **Don't Do This:** * Log excessively, which can impact performance. * Fail to monitor application performance in production. ## 4. Clojure Specific Optimizations ### 4.1 AOT Compilation **Standard**: Use Ahead-of-Time (AOT) compilation for faster startup times. **Why:** AOT compilation compiles Clojure code to Java bytecode at compile time, reducing the amount of work needed at runtime, especially on application startup. **Do This**: * Enable AOT compilation in your "project.clj" file. * AOT compile namespaces that contain entry points or performance-critical functions. **Don't Do This**: * AOT compile everything blindly. Focus on critical namespaces. * Forget to recompile after changes. **Example**: In "project.clj": """clojure (defproject my-project "0.1.0-SNAPSHOT" :dependencies [[org.clojure/clojure "1.11.1"]] :main my-project.core :aot :all ; or a list of namespaces :uberjar-name "my-project.jar") """ ### 4.2 Type Hints **Standard**: Utilize type hints to improve performance of numerical operations and avoid reflection. **Why**: Clojure is dynamically typed, which can sometimes lead to reflection and decreased performance. Type hints provide the compiler with more information, allowing it to generate more efficient bytecode. **Do This**: * Add type hints to function arguments and local bindings where type information is known. * Use primitive type hints (e.g., "^long", "^double") for numerical operations. * Carefully consider the trade-off between verbosity and performance gains. **Don't Do This**: * Add type hints indiscriminately, which can clutter your code and provide minimal benefit. * Use incorrect type hints, which can lead to runtime errors. **Example**: """clojure (defn add-longs [^long x ^long y] (+ x y)) ; Without the hints, the Clojure compiler must use reflection to determine the types of x and y (defn vector-access [^clojure.lang.PersistentVector v ^long index] (.nth v index)) """ ### 4.3 Avoiding Reflection **Standard**: Minimize or eliminate reflection in performance-critical code. **Why**: Reflection is a runtime mechanism for inspecting and manipulating classes and objects. It's powerful but slow compared to direct method calls. **Do This**: * Use "*warn-on-reflection*" to identify reflection sites in your code during development. * Use type hints to guide the compiler. * Consider using protocols or interfaces to avoid reflection when dealing with Java classes. **Don't Do This**: * Ignore reflection warnings. * Rely on reflection for core logic. **Example**: """clojure (set! *warn-on-reflection* true) (defn print-length [^String s] (.length s)) ; Reflection warning will show unless type hinting is used. """ ### 4.4 Protocols and Multimethods **Standard:** Employ protocols and multimethods judiciously for extensibility and polymorphism while considering performance implications. **Why:** Protocols offer a performant way of defining interfaces, enabling polymorphism with compile-time dispatch. Multimethods provide flexible runtime dispatch based on arbitrary criteria, but can be slower than protocols. **Do This:** * Use protocols when you need high-performance polymorphism with known types. * Use multimethods for more complex dispatch logic where performance is less critical. * Cache multimethod dispatch functions when applicable. **Don't Do This:** * Overuse multimethods when protocols would suffice. * Ignore the performance impact of complex multimethod dispatch logic. **Example:** """clojure ;; Protocol example (defprotocol Shape (area [this])) (defrecord Circle [radius] Shape (area [this] (* Math/PI radius radius))) (defrecord Square [side] Shape (area [this] (* side side))) (defn print-area [x] (println (area x)));Very fast. ;; Multimethod example (defmulti describe (fn [x] (type x))) (defmethod describe java.lang.String [s] (str "This is a string: " s)) (defmethod describe :default [x] (str "This is something else: " x)) (println (describe "Hello")) (println (describe 123)); Slower than Protocols. """ ### 4.5 Libraries and Frameworks **Standard**: Carefully select and utilize Clojure libraries and frameworks, considering their performance characteristics. **Why**: The choice of libraries can dramatically impact performance. Some libraries are more optimized than others. **Do This**: * Benchmark libraries before using them in performance-critical code. * Choose libraries that are designed for performance and efficiency. * Understand the internal workings of the libraries you use. **Don't Do This**: * Use libraries blindly without considering their performance impact. * Rely on outdated or unmaintained libraries. **Example**: * "core.async" can be used for asynchronous operations, but benchmark it compared to simpler futures if performance is critical. * For JSON processing, consider "data.json" (part of clojure.data) or "cheshire" which are generally faster than "clojure.data.json" for certain workloads. * When working with numerical data, explore libraries like "tech.v3.datatype" for efficient array operations and interoperability with other JVM libraries. By adhering to these coding standards, developers can create Clojure applications that are both performant and maintainable. This document provides a solid foundation for building high-quality Clojure code that meets the demands of modern applications. Remember to continuously monitor and profile your application in production to identify and address any performance bottlenecks that may arise.