"Making Clojure more readable for data science, business logic, and mathematical expressions."
Infix is a Clojure library that adds optional infix notation for mathematical expressions, comparisons, boolean logic, and data processing pipelines — while compiling to standard Clojure code.
a * b + c / d instead of (+ (* a b) (/ c d))-> and ->> as infix operators for readable transformationsx => x * 2 syntax for anonymous functionsinfix-defn for functions with infix bodiesreturn statementfn(args) syntax within expressionsobj -> .method() syntaxTransform readable code like this:
(infix-defn calculate-discount [subtotal tier quantity]
(let [rate (cond (tier = :premium) 0.15
(quantity >= 10) 0.10
:else 0.05)]
(min (subtotal * rate) 100)))
Into efficient Clojure that runs at full speed.
Add as a git dependency in deps.edn:
;; deps.edn
io.github.flexiana/infix {:git/url "https://github.com/Flexiana/infix"
:git/tag "v1.0.0"
:git/sha "SHORT_SHA"}
Or clone and use locally:
git clone https://github.com/Flexiana/infix.git
cd infix
lein install ;; installs to local Maven repo
Then add to your project:
;; deps.edn
com.flexiana/infix {:local/root "/path/to/infix"}
;; or project.clj after lein install
[com.flexiana/infix "1.0.0"]
Use (infix …) to write math, comparisons, and logic in natural order.
(infix a * b + c / d)
;; => (+ (* a b) (/ c d))
(infix x < y and y <= z)
;; => (and (< x y) (<= y z))
(infix not done or a = b)
;; => (or (not done) (= a b))
Supported operators:
| Category | Operators | Precedence |
|---|---|---|
| Threading | -> ->> some-> some->> | Lowest (0.05) |
| Boolean | and or not | Low (0.1-0.8) |
| Comparison | = not= < <= > >= | Medium (0.5) |
| Arithmetic | + - * / | High (1-2) |
Grouping with normal parentheses works:
(infix (a * (b + c)) / d)
The infix library treats Clojure's threading macros (->, ->>, some->, some->>) as infix operators with the lowest precedence, enabling natural data transformation pipelines.
Threading operators have lowest precedence (0.05), meaning they bind last:
;; This expression:
(infix data -> :key + 5 ->> (map inc))
;; Is parsed as:
(infix (data -> :key + 5) ->> (map inc))
;; Not as:
(infix data -> (:key + 5) ->> (map inc))
This design allows mathematical expressions to be computed before threading:
;; Calculate first, then thread
(infix [1 2 3] ->> count * 2 + 1)
;; => (+ (* (count [1 2 3]) 2) 1) => 7
;; Comparison result gets threaded
(infix 5 > 3 -> (if true "yes" "no"))
;; => (if (> 5 3) "yes" "no") => "yes"
->) vs Thread-Last (->>)Thread-First (->): Insert result as first argument
(infix {:a 1 :b 2} -> (assoc :c 3) -> (get :c))
;; => (-> {:a 1 :b 2} (assoc :c 3) (get :c))
;; => (get (assoc {:a 1 :b 2} :c 3) :c) => 3
;; Perfect for data access chains
(infix user -> :profile -> :address -> :city)
;; => (-> user :profile :address :city)
Thread-Last (->>): Insert result as last argument
(infix [1 2 3 4 5] ->> (filter even?) ->> (map #(* % 2)))
;; => (->> [1 2 3 4 5] (filter even?) (map #(* % 2)))
;; => (map #(* % 2) (filter even? [1 2 3 4 5])) => (4 8)
;; Perfect for collection transformations
(infix data ->> (map transform) ->> (filter valid?) ->> (take 10))
some->, some->>)Handle nil values gracefully in pipelines:
;; Returns nil if any step returns nil
(infix {:user {:name "john"}} some-> :user some-> :name some-> .toUpperCase)
;; => "JOHN"
(infix {:user nil} some-> :user some-> :name some-> .toUpperCase)
;; => nil (stops at :user step)
;; Useful for deep data access
(infix request some-> :params some-> :user-id some-> parse-int)
Combine -> and ->> in the same expression:
;; Access data first, then transform collection
(infix {:data [1 2 3 4 5]} -> :data ->> (filter odd?) ->> (reduce +))
;; => (->> (-> {:data [1 2 3 4 5]} :data) (filter odd?) (reduce +)) => 9
;; Build data structure, then process
(infix (range 10) ->> (map #(* % %)) -> vec -> (nth 3))
;; => (nth (vec (map #(* % %) (range 10))) 3) => 9
Threading works seamlessly with arithmetic, comparisons, and boolean logic:
;; Threading with arithmetic (threading happens first due to precedence)
(infix ([1 2 3] ->> count) + 5)
;; => (+ (count [1 2 3]) 5) => 8
;; Threading with comparisons
(infix ([1 2 3 4 5] ->> count) > 3)
;; => (> (count [1 2 3 4 5]) 3) => true
;; Threading with boolean logic
(infix ([1 2] ->> empty?) or ([3 4] ->> empty?))
;; => (or (empty? [1 2]) (empty? [3 4])) => false
;; Complex business logic with threading
(infix {:transactions txns}
-> :transactions
->> (filter #((:type %) = :debit))
->> (map :amount)
->> (reduce +)
-> (> 1000))
;; Check if total debits exceed $1000
;; ETL pipeline with multiple threading operators
(infix raw-data
-> :payload ; Extract payload
->> (map parse-record) ; Transform each record
->> (filter valid?) ; Filter valid records
-> (group-by :category) ; Group by category
-> vals ; Get category groups
->> (map count) ; Count each group
->> (reduce max)) ; Find largest group
;; Error-safe data processing
(infix user-input
some-> .trim() ; Safely trim if not nil
some-> parse-json ; Parse JSON if string exists
some-> :data ; Extract data if parse succeeded
some->> (map process) ; Process items if data is collection
some->> (take 10)) ; Take first 10 if processing succeeded
Key Insight: Threading operators in infix notation make data transformation pipelines read left-to-right like natural language, while maintaining all the power of Clojure's threading macros.
Understanding precedence ensures expressions work as expected:
Precedence Order (High to Low):
* /) - Highest precedence (2)+ -) - High precedence (1)< <= > >= = not=) - Medium precedence (0.5)and) - Low precedence (0.2)or) - Lower precedence (0.1)-> ->> some-> some->>) - Lowest precedence (0.05)Precedence Examples:
;; Arithmetic first, then comparison, then boolean
(infix a + b * c > d and e < f)
;; => (and (> (+ a (* b c)) d) (< e f))
;; Threading happens last
(infix [1 2 3] ->> count + 5 > 10)
;; => (> (+ (count [1 2 3]) 5) 10)
;; Use parentheses to override precedence
(infix (a + b) * (c + d))
;; => (* (+ a b) (+ c d))
Left-to-Right Evaluation: All operators are left-associative, so they evaluate left-to-right:
(infix a - b - c) ; => (- (- a b) c)
(infix a -> f -> g) ; => (-> (-> a f) g)
(infix a and b and c) ; => (and (and a b) c)
Use => syntax for clean, readable anonymous functions:
(map (infix x => x * x) [1 2 3])
;; => [1 4 9]
(filter (infix x => x > 5) [1 5 10 15])
;; => (10 15)
(reduce (infix (acc x) => acc + x) 0 [1 2 3 4])
;; => 10
((infix (x y) => (x + y) * 2) 3 4)
;; => 14
(map (infix x => x * x + 2 * x + 1) [1 2 3])
;; => [4 9 16] ; (x+1)²
;; Mixed with threading
(map (infix x => x -> str -> clojure.string/upper-case) [1 2 3])
;; => ["1" "2" "3"]
Arrow lambdas (=>) are completely separate from threading (->):
;; Arrow lambda
(map (infix x => x * 2) [1 2 3]) ; => [2 4 6]
;; Threading
(infix data -> :key ->> (map inc))
infix-defnDefine functions with infix expressions in their bodies:
(infix-defn square [x] x * x)
(square 4) ; => 16
(infix-defn add-multiply [x y z] x + y * z)
(add-multiply 1 2 5) ; => 11 (1 + (2 * 5))
(infix-defn in-range? [x min-val max-val]
x >= min-val and x <= max-val)
(in-range? 5 1 10) ; => true
(infix-defn greater-than-ten? [x] x > 10)
(greater-than-ten? 15) ; => true
(infix-defn process-users [users]
users
->> (filter :active?)
->> (map :name)
->> (take 10)
->> sort)
(infix-defn calculate-discount [order]
(:total order)
-> (* 0.1) ; 10% discount
-> (max 5) ; minimum $5 discount
-> (min 100)) ; maximum $100 discount
(infix-defn circle-area
"Calculate the area of a circle given radius"
[radius]
3.14159 * radius * radius)
(infix-defn quadratic [a b c x]
a * x * x + b * x + c)
(quadratic 1 2 3 1) ; => 6 (1*1*1 + 2*1 + 3)
returnUse the return statement for early exits from functions:
(infix-defn safe-divide [x y]
(when (= y 0) (return nil))
(/ x y))
(safe-divide 10 0) ; => nil
(safe-divide 10 2) ; => 5
;; Guard clauses pattern
(infix-defn validate-age [age]
(when (age < 0) (return "Invalid: negative age"))
(when (age > 150) (return "Invalid: too old"))
(when (age < 18) (return "Minor"))
"Adult")
;; Multiple return points
(infix-defn categorize-number [n]
(when (n < 0) (return "negative"))
(when (n = 0) (return "zero"))
(when (n <= 10) (return "small positive"))
"large positive")
Note: Early returns work by throwing and catching special exceptions internally, providing clean non-local exit semantics without affecting normal exception handling.
Use familiar fn(args) syntax within infix expressions:
(infix max(3, 5)) ; => 5
(infix min(1, 2)) ; => 1
(infix count("hello")) ; => 5
(infix Math/sqrt(9)) ; => 3.0
(infix max(3, 5) + min(1, 2)) ; => 6 (5 + 1)
(infix Math/sqrt(9) * 2) ; => 6.0
(infix count("hello") > 3) ; => true
(infix .toUpperCase("hello")) ; => "HELLO"
(infix .length("hello")) ; => 5
(infix .substring("hello", 0, 2)) ; => "he"
(infix "hello" -> .toUpperCase() -> .length()) ; => 5
(infix [1 2 3] ->> (map(#(* % 2))) ->> vec()) ; => [2 4 6]
(infix-defn distance [x1 y1 x2 y2]
(infix Math/sqrt(Math/pow(x2 - x1, 2) + Math/pow(y2 - y1, 2))))
(infix-defn string-processor [s]
(infix .toUpperCase(.trim(s))))
Note: Function call syntax transforms fn(args) to standard Clojure (fn args) during parsing, maintaining full compatibility while providing familiar syntax.
It is required not to use any whitespace between the function name and the opening parenthesis. For example, Math/sqrt(9) is valid, but Math/sqrt (9) is not.
Seamless Java interop with familiar object-oriented syntax:
;; Java-style method chaining using -> operator
(infix "hello" -> .toUpperCase() -> (.substring 0 3)) ; => "HEL"
(infix obj -> (.setName "John") -> (.setSurname "Newman") -> .save())
;; => (-> obj (.setName "John") (.setSurname "Newman") (.save))
;; StringBuilder example
(infix (StringBuilder. "Hello")
-> (.append " ")
-> (.append "World")
-> .toString()) ; => "Hello World"
(infix-defn clean-text [text]
(text -> .trim() -> .toLowerCase()))
(infix-defn build-greeting [name]
(StringBuilder. "Hello, ")
-> (.append name)
-> (.append "!")
-> .toString()))
(infix-defn string-length-plus [s n]
((.length s) + n))
;; Method results in mathematical expressions
(infix (.length "hello") + 5) ; => 10
(infix (.getAge user) > 18) ; => true/false
(infix (.isValid obj) and (.isActive obj)) ; => boolean result
Note: The infix library supports Java interop through Clojure's standard method call syntax (.method) combined with threading operators for elegant method chaining.
;; Simple arithmetic with proper precedence
(infix a * b + c / d)
;; => (+ (* a b) (/ c d))
;; Complex mathematical formulas
(infix (x + y) * (z - w))
;; => (* (+ x y) (- z w))
;; Function calls in math expressions
(infix Math/sqrt(a * a + b * b) + c)
;; => (+ (Math/sqrt (+ (* a a) (* b b))) c)
;; Thread-last for collection transformations
(infix users
->> (filter #((:age %) > 18))
->> (map :email)
->> (take 10)
->> set)
;; => (set (take 10 (map :email (filter #(> (:age %) 18) users))))
;; Thread-first for data access and manipulation
(infix {:data [1 2 3 4 5]}
-> :data
->> (filter odd?)
->> (reduce +))
;; => (reduce + (filter odd? (:data {:data [1 2 3 4 5]})))
Combining all features in realistic business functions:
;; Complete order processing with all infix features
(infix-defn process-order [order customer-tier]
;; Early validation with guard clauses
(when (not (:items order)) (return {:error "No items"}))
(when ((:total order) <= 0) (return {:error "Invalid total"}))
;; Calculate discount using comparisons and math
(let [base-discount (cond (customer-tier = :premium) 0.15
((:total order) >= 100) 0.10
:else 0.05)
;; Function calls with arithmetic
discount-amount (min((:total order) * base-discount, 50))
;; String processing with method chaining
customer-name ((:customer-name order)
-> .trim()
-> .toLowerCase())
;; Complex pipeline with threading
processed-items ((:items order)
->> (filter #((:price %) > 0))
->> (map #(assoc % :discounted-price
((:price %) * (1 - base-discount))))
->> (sort-by :discounted-price)
->> reverse)]
;; Return comprehensive result
{:customer customer-name
:discount-rate base-discount
:discount-amount discount-amount
:final-total ((:total order) - discount-amount)
:items processed-items
:valid true}))
;; Usage example
(let [order {:total 120
:customer-name " JOHN DOE "
:items [{:name "Widget A" :price 50}
{:name "Widget B" :price 70}]}
result (process-order order :premium)]
(:final-total result)) ; => 102.0 (120 - 18 discount)
;; Statistical calculations with arrow lambdas
(infix-defn analyze-sales-data [sales]
(let [;; Filter and transform with threading
valid-sales (sales
->> (filter #((:amount %) > 0))
->> (filter #((:date %) != nil)))
;; Mathematical aggregations with function calls
total-revenue (valid-sales
->> (map :amount)
->> (reduce +))
average-sale (total-revenue / count(valid-sales))
;; Complex filtering with lambda functions
high-value-sales (valid-sales
->> (filter (infix sale => (:amount sale) > average-sale))
->> count)
;; String manipulation and formatting
summary-text (StringBuilder("Sales Analysis: ")
-> (.append count(valid-sales))
-> (.append " sales, $")
-> (.append total-revenue)
-> (.append " revenue")
-> .toString())]
{:total-sales count(valid-sales)
:total-revenue total-revenue
:average-sale average-sale
:high-value-count high-value-sales
:high-value-percentage (high-value-sales * 100.0 / count(valid-sales))
:summary summary-text}))
;; Risk assessment combining everything
(infix-defn assess-transaction-risk [transaction user]
;; Guard clauses with early returns
(when (not transaction) (return {:risk :high :reason "Missing transaction"}))
(when (not user) (return {:risk :high :reason "Missing user"}))
(let [;; Mathematical risk scoring
amount-risk (cond ((:amount transaction) > 10000) 0.8
((:amount transaction) > 1000) 0.4
:else 0.1)
;; Time-based calculations with method chaining
hours-since ((:timestamp transaction)
-> .getTime()
-> (- (System/currentTimeMillis))
-> (/ 3600000)) ; Convert to hours
time-risk (cond (hours-since < 1) 0.6 ; Very recent
(hours-since > 24) 0.3 ; Older transaction
:else 0.2)
;; User history analysis with pipelines
user-score ((:transaction-history user)
->> (filter #((:status %) = :completed))
->> (map :amount)
->> (filter (infix amt => amt > 0))
->> count
-> (max 1) ; Avoid division by zero
-> (min 100) ; Cap the score
-> (/ 100.0)) ; Normalize to 0-1
;; Geographic risk with string operations
location-risk (let [country ((:location user) -> .toLowerCase())]
(cond (country = "us") 0.1
(country = "ca") 0.1
:else 0.3))
;; Combined risk calculation
total-risk (amount-risk + time-risk + location-risk - user-score)
normalized-risk (max(0, min(1, total-risk)))
;; Risk categorization
risk-level (cond (normalized-risk >= 0.7) :high
(normalized-risk >= 0.4) :medium
:else :low)]
{:risk risk-level
:score normalized-risk
:factors {:amount amount-risk
:timing time-risk
:location location-risk
:user-history user-score}
:recommendation (cond (risk-level = :high) "Block transaction"
(risk-level = :medium) "Require additional verification"
:else "Approve transaction")}))
This example demonstrates:
amount-risk calculations with proper precedencecount(valid-sales), min(1, total-risk)-> for data access, ->> for collection processing.getTime() and .toLowerCase() on Java objects(infix amt => amt > 0) for filteringreturn statementscond statements with infix operatorsStringBuilder with method chaininginfix, infix-let, and infix-defn can nest freely.The infix library provides comprehensive infix notation support for Clojure!
✅ Core Features
+ - * /)= < > and or not)-> ->> some-> some->>x => expr syntax with perfect disambiguationinfix-defn with infix expressions in function bodiesreturn statementfn(args) notation within expressionsobj -> .method() syntax✅ Production Ready
🔮 Future Enhancements
in, not-in) for membership testinguser=> (require '[infix.core :refer :all])
user=> (macroexpand-1 '(infix 1 + 2 * 3))
(+ 1 (* 2 3))
user=> (infix 1 + 2 * 3)
7
user=> (infix [1 2 3 4 5] ->> (filter even?) ->> (map #(* % 2)) ->> vec)
[4 8]
user=> (infix {:user {:name "john"}} some-> :user some-> :name some-> .toUpperCase)
"JOHN"
user=> (infix 5 > 3 and 2 < 4)
true
user=> (map (infix x => x * x) [1 2 3 4])
(1 4 9 16)
user=> ((infix (x y) => x + y * 2) 3 4)
11
user=> (infix-defn square [x] x * x)
#'user/square
user=> (square 5)
25
user=> (infix-defn process-data [items] items ->> (filter :active?) ->> count)
#'user/process-data
user=> (process-data [{:active? true} {:active? false} {:active? true}])
2
user=> (infix-defn safe-divide [x y] (when (= y 0) (return nil)) (/ x y))
#'user/safe-divide
user=> (safe-divide 10 0)
nil
user=> (safe-divide 10 2)
5
user=> (infix max(3, 5) + min(1, 2))
6
user=> (infix count("hello") > 3)
true
user=> (infix (.toUpperCase "hello"))
"HELLO"
user=> (infix "hello" -> .toUpperCase() -> (.substring 0 3))
"HEL"
user=> (infix-defn greet-user [name]
(name -> .toUpperCase() -> (.concat "!")))
#'user/greet-user
user=> (greet-user "alice")
"ALICE!"
user=> (infix (StringBuilder. "hello") -> (.append " world") -> .toString())
"hello world"
user=> (infix (.length "hello") + 5)
10
user=> (infix-defn string-processor [text]
(text -> .trim() -> .toLowerCase()))
#'user/string-processor
user=> (string-processor " HELLO WORLD ")
"hello world"
user=> ;; Complex example combining all features
user=> (infix-defn analyze-numbers [numbers]
(when (numbers ->> empty?) (return {:error "No data"}))
(let [avg (numbers ->> (reduce +) -> (/ count(numbers)))
above-avg (numbers ->> (filter (infix n => n > avg)) ->> count)]
{:average avg
:above-average-count above-avg
:percentage (above-avg * 100.0 / count(numbers))}))
#'user/analyze-numbers
user=> (analyze-numbers [1 2 3 4 5])
{:average 3.0, :above-average-count 2, :percentage 40.0}
user=> ;; Threading with arithmetic and comparisons
user=> (infix [10 20 30] ->> count * 2 + 1 > 5)
true
Clojure is beautiful, but for math-heavy or dataflow-heavy code, prefix notation hides intent.
Infix is not a new language — it’s a lens. It lets engineers, analysts, or business rule authors read logic naturally while keeping full Clojure semantics under the hood.
You can use it for:
Apache License 2.0
This project commits Lisp heresy.
Use it to make your Clojure more readable — or to horrify your local REPL priest.
Can you improve this documentation? These fine people already did:
CEO System User & Jiri KneslEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |