Understanding 'Side Causes' and Global Constants in Functional Programming and Clojure
What Are "Side Causes" in Functional Programming?
Most developers learning functional programming (FP) quickly encounter the term side effects—a function modifying state outside of its local scope (e.g., updating a database, mutating a global variable, or printing to the console). However, an equally critical but less frequently named concept is side causes (often called hidden inputs or implicit dependencies).
A side cause occurs when a function relies on state or values from outside its local scope or argument list. When a function depends on external data that can change or vary depending on context, it loses referential transparency.
1. Do Global Constants Break Pure Functions?
In mathematical terms, a function is pure if it satisfies two conditions:
- Deterministic: Given the same arguments, it always returns the exact same result.
- No Side Effects: It does not alter external program state.
Under this formal definition, referencing an immutable global constant does not violate purity. For example, in Clojure:
(def pi 3.14159265359)
(defn circle-area [radius]
(* pi radius radius))Because pi is immutable, calling (circle-area 5) will yield the exact same answer every single time, without mutating any state. Mathematically, circle-area remains pure.
However, when tutorials or style guides suggest that pure functions should not interact with anything outside their boundaries, they are emphasizing decoupling, composability, and testability.
2. The Difference Between Universal Truths and Configuration Data
To decide whether to refactor a global reference into a function parameter, it is helpful to categorize what the global variable represents:
A. Universal Constants (Keep Global)
Mathematical constants, fixed domain rules, or standard library functions (e.g., Math/PI, seconds-in-minute, standard protocols) do not need to be passed as arguments. Passing + or Math/PI into every function creates unnecessary boilerplate without adding real flexibility.
B. Configuration and Environment State (Pass as Arguments)
Values that could change between environments, user sessions, or test scenarios (e.g., database URLs, tax rates, base URIs) should generally not be hardcoded top-level globals inside functions. Doing so binds your function to a single execution context and complicates testing.
3. Should You Refactor to Explicit Arguments?
Consider the example:
;; Approach A: Implicit Dependency
(def a 1)
(defn f [] (+ a 1))
(f)
;; Approach B: Explicit Parameter
(def a 1)
(defn f [x] (+ x 1))
(f a)In this specific case, Approach B is vastly superior, not solely for mathematical purity, but for code reusability and clarity:
- Increased Generality: In Approach B,
fbecomes a reusable increment function. It can incrementa,b, or any number passed to it. - Testability: You can unit test
fwith arbitrary inputs without redefining global vars or mocking namespaces. - Clarity: The function signature
[x]explicitly advertises what data it needs to perform its job.
Best Practices in Clojure and ClojureScript
When structuring Clojure projects, apply the following guidelines:
- Use Function Arguments for Dynamic Data: Core business logic should operate on explicit arguments passed down through the call stack.
- Leverage Dynamic Binding Sparingly: If you must pass contextual data deep down a call stack without cluttering argument lists, consider
bindingand dynamic vars (^:dynamic *context*), though explicit arguments or map-passing is preferred. - Pass Configuration Maps: Instead of having functions look up global configuration, pass a system/configuration map (using libraries like Integrant, Component, or Mount).
- Don't Over-parameterize Invariants: If a constant never changes and represents an inherent invariant of your application logic, keeping it top-level via
defis idiomatic and clean.
Conclusion
While referencing an immutable global variable does not make a function technically impure in the mathematical sense, relying heavily on outside references introduces tight coupling. Refactoring functions to accept explicit parameters makes your code easier to reason about, test, and reuse.