Ruby.CodeCompared.To/Clojure

An interactive executable cheatsheet for Rubyists learning Clojure

Ruby 4.0 Clojure 1.12
Output
Hello World
puts "Hello, World!"
(println "Hello, World!")
println writes a string to stdout followed by a newline, just like Ruby's puts. The outer parentheses are a function call β€” println is the function, "Hello, World!" is the argument. In Clojure, all calls use this prefix form.
print vs println
print "no newline" puts "" # newline puts 42 # auto-converts
(print "no newline") (println "") ; newline (println 42) ; auto-converts
println appends a newline; print does not β€” the same distinction as Ruby's puts vs print. Both accept any value and convert it to a string. prn is like println but prints in a machine-readable form (strings include quotes).
Comments
# Single-line comment total = 42 # inline puts total
; Single-line comment ;; Top-level comment (conventional) (def total 42) ; inline (println total)
Clojure uses semicolons for comments, not #. By convention, ;; opens a standalone comment on its own line, while a single ; follows code on the same line. Clojure also has a #! shebang comment and a #_ reader macro that comments out the next form entirely.
Variables and Bindings
Global definitions
GREETING = "Hello" person = "World" puts "#{GREETING}, #{person}!"
(def greeting "Hello") (def person "World") (println (str greeting ", " person "!"))
def creates a var in the current namespace. By convention, Clojure uses kebab-case for all names β€” no SCREAMING_SNAKE_CASE for constants. str concatenates any number of values as strings, replacing Ruby's #{} interpolation.
Local bindings
radius = 5 area = Math::PI * radius ** 2 puts "Area: #{area.round(2)}"
(let [radius 5 area (* 3.14159 radius radius)] (println (str "Area: " area)))
let creates immutable local bindings scoped to its body. The binding vector is evaluated in order, so later bindings can reference earlier ones β€” area can reference radius. There is no standalone assignment statement in Clojure; let replaces Ruby's local variable assignment.
Immutable bindings
numbers = [1, 2, 3] numbers << 4 # mutates in place puts numbers.inspect # [1, 2, 3, 4]
(let [numbers [1 2 3] more (conj numbers 4)] (println numbers) ; [1 2 3] β€” unchanged (println more)) ; [1 2 3 4]
let bindings cannot be reassigned β€” they are immutable. conj does not modify numbers; it returns a new vector. The original binding is untouched. This is the fundamental difference from Ruby: Clojure never mutates data in place. All "updates" produce new values.
Everything is an expression
result = if true "yes" else "no" end puts result
(let [result (if true "yes" "no")] (println result))
In Clojure, every form returns a value β€” if, let, do, cond, and defn bodies all return their last expression. Ruby shares this property. Unlike many C-family languages, there is no distinction between expressions and statements.
Data Types
Numbers
puts 10 / 3 # 3 (integer division) puts 10.0 / 3 # 3.333... puts 10 % 3 # 1 puts 2 ** 10 # 1024
(println (quot 10 3)) ; 3 β€” integer division (println (/ 10.0 3)) ; 3.333... (println (mod 10 3)) ; 1 (println (Math/pow 2 10)) ; 1024.0
Use quot for integer division (Ruby's / on integers) and mod for remainder. JVM Clojure also has exact Ratio types β€” (/ 1 3) produces 1/3 as a ratio, not a float β€” but this feature is not available in the browser. Math/pow calls the JavaScript Math.pow function.
Keywords
status = :active puts status # :active puts status.class # Symbol puts status.to_s # "active"
(def status :active) (println status) ; :active (println (keyword? status)) ; true (println (name status)) ; "active"
Clojure keywords (:active) are similar to Ruby symbols β€” self-evaluating, interned, and very fast to compare. They are the idiomatic key type for maps. Keywords are also functions: (:name person) looks up :name in the map person, which is more readable than (get person :name).
Booleans and nil
puts true.class # TrueClass puts nil.class # NilClass puts nil.nil? # true
(println (true? true)) ; true (println (nil? nil)) ; true (println (boolean nil)) ; false (println (some? "hi")) ; true β€” (some? x) is (not (nil? x))
Clojure's nil is equivalent to Ruby's nil. true? and false? test for the exact boolean values. boolean coerces any value to true or false β€” only nil and false produce false. some? returns true for anything that is not nil.
Truthiness
puts "nil is falsy" unless nil puts "false is falsy" unless false puts "0 is truthy" if 0 puts "empty str truthy" if ""
(println (if nil "nil is truthy" "nil is falsy")) (println (if false "false is truthy" "false is falsy")) (println (if 0 "0 is truthy" "0 is falsy")) (println (if "" "str is truthy" "str is falsy"))
Clojure and Ruby agree: only nil and false are falsy β€” everything else, including 0, "", and empty collections, is truthy. This differs from JavaScript (where 0 and "" are falsy) and Python (where empty collections are falsy).
Strings
Concatenation and interpolation
first = "Hello" last = "World" puts first + ", " + last + "!" puts "#{first}, #{last}!"
(def first-name "Hello") (def last-name "World") (println (str first-name ", " last-name "!"))
str concatenates any number of arguments, converting non-strings automatically. Clojure has no string interpolation β€” str is the idiomatic replacement. For formatted output, use format (e.g., (format "Hello, %s!" name)), which works the same as Ruby's sprintf.
String operations
text = "Hello, World!" puts text.length # 13 puts text.upcase # HELLO, WORLD! puts text[0, 5] # Hello puts text.include?("World") # true
(def text "Hello, World!") (println (count text)) (println (clojure.string/upper-case text)) (println (subs text 0 5)) (println (clojure.string/includes? text "World"))
count returns the length of a string or any collection. subs extracts a substring by start and end index. The clojure.string namespace contains case conversion, trimming, searching, and splitting functions β€” equivalent to Ruby's built-in String methods.
Split and join
words = "one two three".split puts words.inspect # ["one", "two", "three"] puts words.join(", ") # one, two, three
(def words (clojure.string/split "one two three" #"\s+")) (println words) (println (clojure.string/join ", " words))
clojure.string/split takes a regex pattern (the #"" literal). clojure.string/join takes a separator and a collection. The #"\s+" regex splits on any whitespace, matching Ruby's default split behavior.
Trim and case
puts " hello ".strip # "hello" puts "hello world".capitalize # "Hello world" puts "Hello".downcase # "hello"
(println (clojure.string/trim " hello ")) (println (clojure.string/capitalize "hello world")) (println (clojure.string/lower-case "Hello"))
The clojure.string namespace provides trim, triml, trimr, capitalize, upper-case, and lower-case. These are pure functions that return new strings β€” like Ruby's non-bang string methods (strip, not strip!).
Collections
Vectors
numbers = [1, 2, 3, 4, 5] puts numbers.first # 1 puts numbers.last # 5 puts numbers.length # 5 puts numbers[2] # 3
(def numbers [1 2 3 4 5]) (println (first numbers)) ; 1 (println (last numbers)) ; 5 (println (count numbers)) ; 5 (println (get numbers 2)) ; 3 (println (numbers 2)) ; 3 β€” vectors are functions of their index
Clojure vectors ([...]) are the closest equivalent to Ruby arrays β€” indexed, ordered, and persistent. They are immutable by default. Vectors are also functions: (numbers 2) looks up index 2, so vectors can be used directly as data accessors.
Lists vs vectors
# Ruby has no separate list type list = [1, 2, 3] puts list.first # 1 puts list.drop(1).inspect # [2, 3]
(def my-list '(1 2 3)) ; quoted list (println (first my-list)) ; 1 (println (rest my-list)) ; (2 3) (println (cons 0 my-list)) ; (0 1 2 3) ; Prefer vectors [1 2 3] for most use cases
Clojure lists ('(...)) are linked lists β€” fast at prepending with cons, slow at indexed access. Vectors are fast at indexed access and appending. The quote prevents the list from being evaluated as a function call. In practice, prefer vectors for data and use lists mainly for representing code.
Maps
person = {name: "Alice", age: 30} puts person[:name] # Alice puts person[:age] # 30
(def person {:name "Alice" :age 30}) (println (:name person)) ; Alice β€” keyword as function (println (get person :age)) ; 30 (println (person :name)) ; Alice β€” maps are functions too
Clojure maps use keywords as keys by convention. Keywords and maps are both callable as functions: (:name person) looks up :name in the map, and (person :name) does the same. The keyword-as-function form is more idiomatic and reads naturally: "get the name of this person".
Sets
require "set" primes = Set.new([2, 3, 5, 7, 11]) puts primes.include?(5) # true puts primes.include?(4) # false
(def primes #{2 3 5 7 11}) (println (contains? primes 5)) ; true (println (contains? primes 4)) ; false (println (primes 5)) ; 5 β€” sets are functions (println (primes 4)) ; nil
Clojure sets (#{...}) are unordered collections of unique values. Like maps, sets are functions of their elements: (primes 5) returns the element if present or nil if absent. Ruby requires require "set"; sets are built into Clojure's core.
Updating collections
person = {name: "Alice", age: 30} person[:age] = 31 person[:email] = "a@b.com" puts person.inspect
(def person {:name "Alice" :age 30}) (def updated (assoc person :age 31 :email "a@b.com")) (println person) ; {:name "Alice", :age 30} β€” unchanged! (println updated) ; {:name "Alice", :age 31, :email "a@b.com"}
assoc returns a new map with the given keys set. The original map is untouched β€” this is the core of Clojure's immutability. dissoc removes keys. The underlying data structure uses structural sharing, so creating a "modified" copy is efficient β€” it shares most of its memory with the original.
Nested data
data = {user: {name: "Alice", scores: [95, 87, 92]}} puts data.dig(:user, :name) # Alice puts data.dig(:user, :scores, 1) # 87
(def data {:user {:name "Alice" :scores [95 87 92]}}) (println (get-in data [:user :name])) (println (get-in data [:user :scores 1])) (def updated (assoc-in data [:user :scores 2] 99)) (println (get-in updated [:user :scores])) (println (get-in data [:user :scores]))
get-in navigates nested collections using a path vector β€” like Ruby's dig. assoc-in returns a new structure with one nested value changed, leaving the original intact. update-in applies a function to a nested value: (update-in data [:user :age] inc).
Functions
Defining functions
def greet(name) "Hello, #{name}!" end puts greet("Alice")
(defn greet [name] (str "Hello, " name "!")) (println (greet "Alice"))
defn defines a named function. The argument vector uses square brackets. The last expression in the body is the return value β€” there is no return keyword. defn is a macro that expands to (def greet (fn [name] ...)).
Anonymous functions
double = ->(x) { x * 2 } triple = ->(x) { x * 3 } puts double.call(5) # 10 puts triple.(5) # 15
(def double-it (fn [x] (* x 2))) (def triple #(* % 3)) ; shorthand (println (double-it 5)) ; 10 (println (triple 5)) ; 15 (println (#(+ %1 %2) 3 4)) ; 7 β€” called immediately
fn creates an anonymous function. The #(...) shorthand is convenient for simple expressions β€” % is the first argument, %2 the second, and so on. Like Ruby's ->() lambdas, these are first-class values that can be stored, passed, and returned.
Multi-arity functions
def greet(name, greeting = "Hello") "#{greeting}, #{name}!" end puts greet("Alice") puts greet("Bob", "Hi")
(defn greet ([name] (greet name "Hello")) ([name greeting] (str greeting ", " name "!"))) (println (greet "Alice")) (println (greet "Bob" "Hi"))
Clojure functions can have multiple arities β€” a separate implementation for each argument count. Each arity is a parenthesized group within defn. By convention, lower-arity versions call the higher-arity version with defaults. This is more explicit than Ruby's default arguments, but equally expressive.
Variadic functions
def total(*numbers) numbers.sum end puts total(1, 2, 3, 4, 5) # 15
(defn total [& numbers] (reduce + numbers)) (println (total 1 2 3 4 5)) ; 15 (println (apply total [1 2 3])) ; apply unpacks a collection
& in the argument vector collects remaining args into a sequence, like Ruby's splat *. apply unpacks a collection as arguments β€” the Clojure equivalent of Ruby's method(*array) splat call. Any function can be applied to a collection with apply.
Partial application
add = ->(a, b) { a + b } add5 = add.curry.(5) puts add5.(3) # 8 puts add5.(10) # 15
(defn add [a b] (+ a b)) (def add5 (partial add 5)) (println (add5 3)) ; 8 (println (add5 10)) ; 15
partial returns a new function with the first arguments pre-filled. It is the direct equivalent of Ruby's curry. (partial add 5) produces a function that adds 5 to any argument. partial is a core language function in Clojure; Ruby requires calling .curry on a lambda.
Function composition
double = ->(x) { x * 2 } add1 = ->(x) { x + 1 } transform = add1 >> double # add1 first, then double puts transform.(3) # add1(3)=4, double(4)=8
(def double-it #(* % 2)) (def add1 #(+ % 1)) (def transform (comp double-it add1)) ; right-to-left (println (transform 3)) ; add1(3)=4, double-it(4)=8
comp composes functions right-to-left: (comp f g) applies g first, then f. Ruby's >> applies left-to-right, so add1 >> double-it in Ruby equals (comp double-it add1) in Clojure β€” both apply add1 first. Think of it as reading the composition in the order the data flows through.
Sequence Operations
map
numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |n| n * 2 } puts doubled.inspect # [2, 4, 6, 8, 10]
(def numbers [1 2 3 4 5]) (println (mapv #(* % 2) numbers)) ; [2 4 6 8 10] (println (map str numbers)) ; ("1" "2" "3" "4" "5")
mapv returns a vector (like Ruby's map). Plain map returns a lazy sequence. The function comes before the collection β€” the opposite of Ruby's method chaining. map can also zip multiple collections: (map + [1 2] [3 4]) gives (4 6).
filter and remove
numbers = [1, 2, 3, 4, 5, 6] evens = numbers.select { |n| n.even? } odds = numbers.reject { |n| n.even? } puts evens.inspect # [2, 4, 6] puts odds.inspect # [1, 3, 5]
(def numbers [1 2 3 4 5 6]) (println (filterv even? numbers)) ; [2 4 6] (println (filterv odd? numbers)) ; [1 3 5] (println (remove even? numbers)) ; (1 3 5)
filter keeps elements where the predicate returns truthy β€” like Ruby's select. remove is the complement β€” like Ruby's reject. filterv returns a vector; plain filter returns a lazy sequence. Clojure includes built-in predicates like even?, odd?, nil?, string?, and number?.
reduce
numbers = [1, 2, 3, 4, 5] sum = numbers.reduce(0, :+) product = numbers.reduce(1, :*) puts sum # 15 puts product # 120
(def numbers [1 2 3 4 5]) (println (reduce + 0 numbers)) ; 15 (println (reduce * 1 numbers)) ; 120 (println (reduce + numbers)) ; 15 β€” initial value optional (println (reduce str numbers)) ; "12345"
reduce takes the function first, then an optional initial value, then the collection. Clojure's core arithmetic functions (+, *, str) work as reducing functions directly β€” no need for the :+ symbol Ruby requires. Without an initial value, the first element is used.
into β€” building collections
puts ([1, 3] + [2, 4]).inspect # [1, 3, 2, 4] puts [1, 2, 2, 3].uniq.inspect # [1, 2, 3] puts [[:a, 1], [:b, 2]].to_h.inspect # {:a=>1, :b=>2}
(println (into [1 3] [2 4])) (println (into #{} [1 2 2 3])) (println (into {} [[:a 1] [:b 2]]))
into pours one collection into another, adapting to the target type. Pouring into a vector appends; into a set deduplicates; into a map takes key-value pairs. into with a transducer is also the most efficient way to chain filter and map in a single pass without intermediate collections.
sort and sort-by
puts [3, 1, 4, 1, 5].sort.inspect people = [{name: "Bob"}, {name: "Alice"}] puts people.sort_by { |p| p[:name] }.inspect
(println (sort [3 1 4 1 5])) (def people [{:name "Bob"} {:name "Alice"}]) (println (sort-by :name people))
sort returns a sorted sequence. sort-by takes a key function β€” keywords work here because they are functions of maps. (sort-by :name people) is equivalent to Ruby's people.sort_by { |p| p[:name] }. For reverse order, use (sort-by :name #(compare %2 %1) people).
Lazy sequences
squares = (1..).lazy.map { |n| n * n } puts squares.first(5).inspect # [1, 4, 9, 16, 25] puts (1..Float::INFINITY).lazy.select(&:odd?).first(3).inspect
(println (take 5 (map #(* % %) (range)))) (println (take 3 (filter odd? (range)))) (def naturals (iterate inc 1)) (println (take 5 naturals))
Clojure sequences are lazy by default β€” they compute values on demand. range with no arguments produces an infinite sequence. iterate generates an infinite sequence by repeatedly applying a function to a seed value. Laziness enables working with infinite or very large data sets without loading everything into memory.
Control Flow
if and cond
temp = 72 if temp > 80 puts "hot" elsif temp > 60 puts "comfortable" else puts "cold" end
(def temp 72) (println (cond (> temp 80) "hot" (> temp 60) "comfortable" :else "cold"))
Clojure's if accepts exactly three forms: condition, then, else. For multiple branches, use cond, which evaluates conditions top-to-bottom and returns the value of the first truthy branch. The :else keyword at the end serves as the default β€” any truthy value works there, but :else is the convention.
when and when-not
logged_in = true if logged_in puts "Welcome back!" puts "Loading profile..." end
(def logged-in true) (when logged-in (println "Welcome back!") (println "Loading profile..."))
when is if without an else branch, and it accepts multiple body expressions (an implicit do). Use when when you only care about the truthy case. when-not is the inverse. when returns nil when its condition is falsy, which is often useful in threading macros.
case
day = "Monday" type = case day when "Saturday", "Sunday" then "weekend" when "Monday", "Friday" then "bookend" else "midweek" end puts type
(def day "Monday") (println (case day ("Saturday" "Sunday") "weekend" ("Monday" "Friday") "bookend" "midweek"))
case dispatches on a constant value using fast equality. Multiple matches for one result are listed in a parenthesized group. The final bare expression is the default (no condition needed). Unlike cond, case does not evaluate test expressions β€” they must be compile-time constants.
and / or (short-circuit)
puts (true && false) # false puts (false || "hi") # "hi" β€” returns value puts (nil || "default") # "default"
(println (and true false)) ; false (println (or false "hi")) ; "hi" β€” returns value! (println (or nil "default")) ; "default" (println (and 1 2 3)) ; 3 β€” last truthy value
Like Ruby, Clojure's and and or short-circuit and return the deciding value rather than just true or false. (or nil "default") is the idiomatic nil-coalescing pattern, equivalent to Ruby's nil || "default". and returns the last value if all are truthy, or the first falsy value.
do β€” sequencing
# Ruby has no standalone do β€” blocks handle sequencing result = begin x = 10 y = 20 x + y end puts result
(def result (do (def x 10) (def y 20) (+ x y))) (println result)
do evaluates multiple expressions in order and returns the last one β€” like Ruby's begin/end block. Most forms that accept a body (when, let, defn) include an implicit do, so explicit do is mainly needed inside if's then/else branches when multiple side effects are required.
Destructuring
Sequence destructuring
first, *rest = [1, 2, 3, 4, 5] a, b, c = [10, 20, 30] puts "first=#{first} rest=#{rest.inspect}" puts "a=#{a} b=#{b} c=#{c}"
(let [[head & tail] [1 2 3 4 5] [a b c] [10 20 30]] (println "head=" head "tail=" (vec tail)) (println "a=" a "b=" b "c=" c))
Destructuring in let mirrors the collection syntax. [head & tail] is Clojure's equivalent of Ruby's first, *rest. Unused trailing elements can be discarded with _ by convention: [a b _]. Destructuring works in let, fn, defn, and for.
Map destructuring
person = {name: "Alice", age: 30, city: "Portland"} name, age = person.values_at(:name, :age) puts "#{name}, #{age}"
(def person {:name "Alice" :age 30 :city "Portland"}) (let [{:keys [name age city]} person] (println name age city))
{:keys [name age]} destructures a map and binds each key's value to a local variable of the same name. This is extremely common in idiomatic Clojure, especially in function arguments. For different local names, use {local-name :map-key}: (let [{person-name :name} p] ...).
Destructuring in function args
def describe(name:, age:, city: "Unknown") "#{name}, #{age}, from #{city}" end puts describe(name: "Alice", age: 30)
(defn describe [{:keys [name age city] :or {city "Unknown"}}] (str name ", " age ", from " city)) (println (describe {:name "Alice" :age 30})) (println (describe {:name "Bob" :age 25 :city "NYC"}))
{:keys [...] :or {...}} in a function argument both destructures the map and provides default values. This is Clojure's idiomatic equivalent of Ruby's keyword arguments. The entire map is always passed as a single argument β€” there are no separate keyword parameters at the call site.
Threading Macros
Thread-first (->)
result = " hello, world " .strip .split(", ") .first .upcase puts result # HELLO
(println (-> " hello, world " clojure.string/trim (clojure.string/split #", ") first clojure.string/upper-case))
-> threads a value through a series of functions, inserting it as the first argument of each step. It mirrors Ruby's method chaining (.method) and makes left-to-right data flow readable in a language that normally reads inside-out. Wrapping a step in parens adds extra arguments: (split #", ") β†’ (split value #", ").
Thread-last (->>)
result = (1..10) .select(&:odd?) .map { |n| n ** 2 } .sum puts result # 165
(println (->> (range 1 11) (filter odd?) (map #(* % %)) (reduce +)))
->> threads the value as the last argument, matching the convention for sequence-processing functions like map, filter, and reduce. Use -> for object-like operations (string methods, map lookups), and ->> for sequence pipelines. Together they replace nearly all deeply-nested function calls.
some-> (nil-safe threading)
# Ruby uses &. (safe navigation operator) user = nil puts user&.dig(:profile, :name) # nil, no error
(def user nil) (println (some-> user :profile :name)) (def real-user {:profile {:name "Alice"}}) (println (some-> real-user :profile :name))
some-> is a nil-safe version of ->: if any step returns nil, the entire chain short-circuits and returns nil. This is the Clojure equivalent of Ruby's &. safe navigation operator. some->> does the same for thread-last pipelines.
State
Atoms
counter = 0 counter += 1 counter += 1 puts counter # 2
(def counter (atom 0)) (swap! counter inc) (swap! counter inc) (println @counter) ; 2 β€” @ dereferences the atom
atom is Clojure's primary tool for managing mutable state. Unlike Ruby's direct variable mutation, an atom holds a value that can only be changed with swap! or reset!. @atom (or (deref atom)) reads the current value. Atom updates are atomic and thread-safe β€” no mutex needed.
swap! and reset!
scores = [85, 92, 78] scores << 95 scores.map! { |s| s + 5 } puts scores.inspect
(def scores (atom [85 92 78])) (swap! scores conj 95) (swap! scores #(mapv (partial + 5) %)) (println @scores)
swap! applies a function to the current atom value and stores the result. reset! replaces the value unconditionally. (swap! scores conj 95) calls (conj @scores 95) and stores the result. swap! may retry if the atom is updated concurrently β€” the function must be pure and free of side effects.
Accumulating state
results = [] [1, 2, 3].each { |n| results << n * n } puts results.inspect # [1, 4, 9]
; Idiomatic: use mapv instead of mutating (println (mapv #(* % %) [1 2 3])) ; With an atom when you truly need accumulation: (def results (atom [])) (doseq [n [1 2 3]] (swap! results conj (* n n))) (println @results)
In Clojure, prefer pure transformation functions (mapv, reduce, into) over accumulation with atoms. Atoms are for state that genuinely changes over time β€” a counter, a cache β€” not for building up results. doseq is a side-effectful loop (like Ruby's each), and its return value is nil.
Error Handling
try / catch / finally
begin raise "something went wrong" rescue => e puts "Caught: #{e.message}" ensure puts "always runs" end
(try (throw (ex-info "something went wrong" {})) (catch :default error (println "Caught:" (ex-message error))) (finally (println "always runs")))
try / catch / finally map directly to Ruby's begin / rescue / ensure. In ClojureScript (the browser runtime used here), (catch :default e ...) catches any thrown value. In JVM Clojure, use (catch Exception e ...) or a specific exception class.
Structured errors with ex-info
begin raise ArgumentError, "user not found" rescue ArgumentError => e puts e.message end
(try (throw (ex-info "user not found" {:code 404 :id 99})) (catch :default error (println (ex-message error)) (println (ex-data error))))
ex-info creates a structured error with a message string and an arbitrary data map. ex-message retrieves the message; ex-data retrieves the map. This is idiomatic Clojure error handling β€” no need to define custom exception classes. The data map carries typed, queryable context, far richer than a string message alone.
Preconditions
def divide(a, b) raise ArgumentError, "cannot divide by zero" if b.zero? a / b.to_f end puts divide(10, 3).round(4)
(defn divide [a b] {:pre [(not (zero? b))]} (/ a (float b))) (println (divide 10 3)) (try (divide 10 0) (catch js/Error e (println "Error:" (ex-message e))))
{:pre [...]} is Clojure's built-in precondition system β€” each expression in the vector must be truthy or an AssertionError is thrown. Preconditions are a concise replacement for defensive guard clauses. {:post [...]} does the same for return values. Both are disabled if *assert* is bound to false.
Gotchas
Prefix notation (no operator precedence)
# Is this 8 or 14? puts 2 + 3 * 4 # 14 β€” must know precedence rules puts (2 + 3) * 4 # 20
; Nesting IS the grouping β€” no precedence rules (println (+ 2 (* 3 4))) ; 14 (println (* (+ 2 3) 4)) ; 20 (println (+ 1 2 3 4 5)) ; 15 β€” + takes any number of args
Clojure has no infix operators β€” all operations are prefix function calls. The nesting structure is unambiguous: there are no operator precedence rules to memorize. The trade-off is a lot of parentheses. The upside is that + and * take any number of arguments: (+ 1 2 3 4) is valid.
Equality operators
puts 1 == 1.0 # true (value equality) puts "a" == "a" # true puts :foo == :foo # true puts 1.equal?(1) # true (same object) puts "a".equal?("a") # false (different objects)
(println (= 1 1.0)) ; true β€” value equality (println (= "a" "a")) ; true (println (= :foo :foo)) ; true (println (== 1 1.0)) ; true β€” numeric equality (println (identical? "a" "a")) ; false β€” reference equality
= is value equality for all types β€” like Ruby's ==. identical? is reference equality β€” like Ruby's equal?. == is numeric equality across types, so (== 1 1.0) is true even though 1 and 1.0 are different types. Keywords and small integers are always identical to themselves.
Lazy sequences vs vectors
# Ruby map always returns an Array puts [1, 2, 3].map { |n| n * 2 }.class # Array
(def as-lazy (map inc [1 2 3])) (def as-vec (mapv inc [1 2 3])) (println (vector? as-lazy)) ; false β€” lazy sequence (println (vector? as-vec)) ; true (println as-lazy) ; (2 3 4) β€” parens, not brackets (println as-vec) ; [2 3 4]
map, filter, and most sequence functions return lazy sequences, not vectors. This is a common surprise for Ruby developers. Use mapv and filterv to get vectors, or wrap in (vec ...). Lazy sequences print with parentheses; vectors print with brackets. Both support the same sequence operations.
nil punning
list = nil puts list.to_a.inspect # [] puts list.to_s # "" puts list&.first # nil
(println (seq nil)) ; nil β€” empty seq collapses to nil (println (count nil)) ; 0 (println (first nil)) ; nil β€” safe to call on nil (println (map inc nil)) ; () β€” mapping over nil is empty (println (str nil)) ; ""
In Clojure, nil and empty collections are interchangeable in many contexts β€” this is called nil-punning. Most sequence functions accept nil safely. seq returns nil for empty collections, enabling the idiom (when (seq coll) ...) to check for non-empty. This is more idiomatic than (not (empty? coll)).
Parentheses always mean "call"
# Ruby: (1 + 2) groups; method() calls puts (1 + 2) * 3 # 9 puts [1,2,3].length # 3
; Clojure: every ( starts a function call ; (+ 1 2) calls + with args 1 and 2 (println (* (+ 1 2) 3)) ; 9 (println (count [1 2 3])) ; 3 ; [1 2 3] is data, '(1 2 3) is a quoted list
In Clojure, every opening parenthesis starts a function (or macro) call β€” the first element is always the operator. Parentheses never just group expressions. Square brackets ([...]) are data vectors. Curly braces ({...}) are maps. This regularity is Lisp's core insight: code and data use the same notation, enabling macros to manipulate code as data.