Output & Basics
Hello, World
main :: IO ()
main = putStrLn "Hello, World!" (println "Hello, World!") Haskell requires a named
main :: IO () entry point that the runtime calls once. Clojure has no entry-point requirement at all — top-level forms simply evaluate in the order they are read, closer to a REPL session than a compiled program with a fixed starting point.Sequencing multiple outputs
main :: IO ()
main = do
putStrLn "First"
putStrLn "Second"
putStrLn "Third" (println "First")
(println "Second")
(println "Third") Haskell needs
do-notation to sequence IO actions, since it desugars to chained monadic binds. Clojure is strict, so top-level forms simply evaluate one after another in the order written — no monad or block syntax required for plain sequencing.Printing a value for inspection
main :: IO ()
main = print (Just [1, 2, 3]) (prn [1 2 3]) Haskell's
print uses the Show typeclass, deriving a textual representation for (almost) any value. Clojure's prn prints any value in a re-readable, machine-parseable form — every built-in data structure already knows how to print itself, needing no derivation step at all, since Clojure has no typeclass system for this to opt into in the first place.Values & Bindings
def and let are both immutable by default
main :: IO ()
main =
let radius = 5.0
area = pi * radius * radius
in print area (let [radius 5.0
area (* Math/PI radius radius)]
(println area)) Haskell's
let ... in and Clojure's let both introduce local, immutable bindings scoped to the following expression — genuinely close in spirit and even in visual shape, once the parentheses are accounted for.Immutability is a shared core value, not just a default
main :: IO ()
main =
let numbers = [1, 2, 3] :: [Int]
moreNumbers = 0 : numbers
in do
print numbers
print moreNumbers -- unchanged — nothing was mutated (def numbers [1 2 3])
(def more-numbers (cons 0 numbers))
(println numbers)
(println more-numbers) ; numbers is unchanged — nothing was mutated This is a genuine, deep philosophical alignment rarely found elsewhere on this site: both languages treat immutability as a CORE VALUE, not merely a syntactic default that gets opted out of constantly. Clojure, despite being dynamically typed, is culturally as committed to "prefer never mutating" as Haskell is structurally —
consing a new element onto numbers leaves the original completely untouched in both languages.Static Types vs. Fully Dynamic
No static type system at all
add :: Int -> Int -> Int
add x y = x + y
main :: IO ()
main = print (add 2 3) (defn add [x y]
(+ x y))
(println (add 2 3)) Like Scheme, Clojure has no static type system at all:
add has no declared or inferred type anywhere, and (add "two" 3) would run right up until the moment + itself rejects the string argument at runtime — there is no separate compile-time check that could catch this earlier, the way GHC would reject a mistyped call to Haskell's add before the program ever runs.clojure.spec: opt-in runtime validation, not compile-time checking
-- Haskell's type checking is unconditional and happens once, before
-- the program ever runs — there is no "opting in" to it per function:
describe :: Int -> String
describe n = "an Int: " ++ show n
main :: IO ()
main = putStrLn (describe 42) (defn describe [n]
(if (number? n)
(str "a number: " n)
"something else"))
(println (describe 42)) Clojure's
clojure.spec library can describe and validate a function's expected shapes, but it is opt-in tooling layered on top of the language, checked (if at all) at runtime via explicit instrumentation calls — a fundamentally different guarantee from Haskell's type checker, which runs unconditionally, for every function, before the program is ever executed at all.Strings & Keywords
String concatenation
main :: IO ()
main = putStrLn ("Hello, " ++ "World!") (println (str "Hello, " "World!")) Haskell's
++ is the general list-concatenation operator, since a String is really [Char]. Clojure's str concatenates any number of arguments, converting each to its string representation first — closer to a universal "stringify and join" function than a dedicated concatenation operator.Keywords: a genuinely new data type for Haskell
-- Haskell has nothing structurally equivalent to a Clojure keyword —
-- the closest analogue is a nullary data constructor, which must be
-- declared up front in a "data" statement before it can be used:
data Status = Ok | Failed deriving (Show, Eq)
main :: IO ()
main = print (Ok == Ok) (println (= :ok :ok)) A Clojure keyword like
:ok is a self-evaluating, interned atomic value that springs into existence at the point of use — no declaration required, comparable by identity, and idiomatically used as map keys and lightweight tags everywhere. Haskell has no equivalent data type; the closest comparison is a nullary constructor of a data type, which — unlike a Clojure keyword — must be declared upfront and belongs to one specific, closed type rather than a shared, open universe of interned atoms.Immutable Lists vs. Persistent Data
Persistent vectors, not just lists
main :: IO ()
main = print [1, 2, 3, 4, 5] (println [1 2 3 4 5]) Haskell has one primary sequential collection — the singly-linked list. Clojure offers a genuinely richer default:
[1 2 3 4 5] is a persistent VECTOR (efficient random access and append), a structurally different and more commonly reached-for collection than Clojure's own cons-cell list (built with (list 1 2 3)), which plays the role closer to Haskell's actual list representation.map and filter
main :: IO ()
main =
let doubled = map (* 2) [1, 2, 3, 4, 5]
evens = filter even doubled
in print evens (def doubled (map #(* % 2) [1 2 3 4 5]))
(def evens (filter even? doubled))
(println evens) Both languages call these operations
map and filter, with the same argument order (function first). Clojure's #(* % 2) reader-macro shorthand for a one-argument lambda is unique to Clojure — % refers to the single implicit argument, more compact than either language's full lambda syntax.Persistent maps
import qualified Data.Map as Map
main :: IO ()
main =
let ages = Map.fromList [("Ada", 36), ("Alan", 41)]
in print (Map.lookup "Ada" ages) (def ages {"Ada" 36, "Alan" 41})
(println (get ages "Ada")) Both languages provide a persistent, immutable map as a first-class value: Haskell's
Data.Map (usually imported qualified) and Clojure's literal { } map syntax, needing no import or constructor call at all. Both a missing-key Map.lookup and Clojure's get return an absence marker (Nothing versus nil) rather than throwing.Structural sharing makes "copying" cheap in both languages
main :: IO ()
main =
let original = [1, 2, 3] :: [Int]
extended = 0 : original
in do
print original
print extended (def original [1 2 3])
(def extended (conj original 0))
(println original)
(println extended) Both languages achieve efficient "copy-on-write" behavior the same way: through structural sharing, where a new value reuses most of the internal structure of the old one rather than copying it wholesale. This is why prepending to a Haskell list or
conj-ing onto a Clojure vector is cheap despite immutability — a shared implementation strategy, not a coincidence, since both languages need it to make immutability-by-default practical at all.Maybe vs. nil-Punning
Maybe becomes plain nil — no wrapper type at all
import qualified Data.Map as Map
main :: IO ()
main =
let ages = Map.fromList [("Ada", 36)]
in print (Map.lookup "Alan" ages) (def ages {"Ada" 36})
(println (get ages "Alan")) Haskell's
Maybe a is a real, compiler-checked wrapper type — a Just x is a genuinely different value from a bare x, and the compiler forces every caller to unwrap it explicitly. Clojure has nothing resembling Maybe at all: a missing lookup simply returns nil directly, the SAME nil used for every other kind of absence in the language, with no wrapper and no compiler enforcement that anyone ever checks for it."nil-punning": most core functions treat nil as harmless
-- Haskell has nothing resembling nil-punning — Nothing must always
-- be explicitly pattern-matched or handled via Maybe combinators
-- like fromMaybe before you can even attempt further operations:
main :: IO ()
main = print (length ([] :: [Int])) (println (count nil)) ; 0, not an error
(println (seq nil)) ; nil, not an error
(println (first nil)) ; nil, not an error
(println (map inc nil)) ; (), not an error Clojure's core sequence functions are deliberately designed to treat
nil as an empty collection rather than raise an error — a deliberate ergonomic choice called "nil-punning." Haskell has nothing resembling this: attempting to call a list function on Nothing is a straightforward type error, since Nothing and [] are not even the same type. Nil-punning makes Clojure code more forgiving of missing values, at the cost of Haskell-style compiler certainty about which values might actually be absent.Pattern Matching vs. Destructuring
Destructuring a vector
main :: IO ()
main =
let (first:rest) = [1, 2, 3]
in print (first, rest) (let [[first & rest] [1 2 3]]
(println first "and" rest)) Haskell's
(first:rest) pattern and Clojure's [first & rest] destructuring both pull the first element and the rest apart in one binding. Clojure's destructuring is a let-binding feature bolted onto an otherwise dynamically-typed language, not a full pattern-matching system tied to a closed set of constructors the way Haskell's is.No built-in shape-based case — use cond instead
describe :: Int -> String
describe n = case n of
0 -> "zero"
n | n > 0 -> "positive"
_ -> "negative"
main :: IO ()
main = putStrLn (describe (-5)) (defn describe [n]
(cond
(= n 0) "zero"
(> n 0) "positive"
:else "negative"))
(println (describe -5)) This is a real gap for a Haskeller. Core Clojure has no construct that matches on a value's SHAPE the way Haskell's
case destructures a constructor's payload — cond is a chain of independent boolean tests, closer to a sequence of if/else ifs. (The separate core.match library adds genuine pattern matching, but it is an external dependency, not core syntax, unlike Haskell, where matching is fundamental to the language itself.)Destructuring a map — a shape Haskell records handle differently
data Person = Person { name :: String, age :: Int } deriving (Show)
main :: IO ()
main =
let Person { name = personName, age = personAge } = Person { name = "Ada", age = 36 }
in putStrLn (personName ++ " is " ++ show personAge) (let [{:keys [name age]} {:name "Ada" :age 36}]
(println (str name " is " age))) Haskell's record pattern
Person { name = personName, age = personAge } and Clojure's {:keys [name age]} destructuring both pull specific fields out of a structured value by name in one binding — Clojure's form works on any ordinary map, with no type declaration required anywhere, where Haskell's form only works because Person was already declared as a record type.Higher-Order Functions
Anonymous functions
main :: IO ()
main = print (map (\x -> x * x) [1, 2, 3, 4]) (println (map (fn [x] (* x x)) [1 2 3 4])) Haskell spells a lambda
\x -> ... (the backslash evoking the Greek letter lambda). Clojure spells the same idea (fn [x] ...), or — as seen in earlier rows — the terser reader-macro shorthand #(* % %) when the parameter is used positionally and only once or twice.Folding a collection
main :: IO ()
main = print (foldl (+) 0 [1, 2, 3, 4, 5]) (println (reduce + 0 [1 2 3 4 5])) Haskell's
foldl and Clojure's reduce take the same pieces — a combining function, an initial accumulator, then the collection — in the same order, though Clojure's name follows the more common cross-language convention (reduce, matching Python, JavaScript, and Ruby) rather than Haskell's fold family naming.Currying: automatic in Haskell, manual in Clojure
add :: Int -> Int -> Int
add x y = x + y
main :: IO ()
main =
let addFive = add 5
in print (addFive 10) (defn add [x y] (+ x y))
(def add-five (partial add 5))
(println (add-five 10)) In Haskell,
add 5 alone is already valid — every function is secretly a chain of one-argument functions. Clojure has no automatic currying, but its partial function does the same job explicitly: (partial add 5) returns a new function with the first argument already supplied, functionally identical to Haskell's automatic partial application, just spelled out as an explicit call.Recursion & Explicit recur
Factorial
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
main :: IO ()
main = print (factorial 10) (defn factorial [n]
(if (= n 0)
1
(* n (factorial (dec n)))))
(println (factorial 10)) A base-case clause and a recursive clause — Haskell writes them as separate pattern-matched equations, Clojure writes the equivalent single branch inside one
if. Both are idiomatic for exactly this shape of recursion in their respective languages.recur: tail calls must be marked explicitly — a real difference
sumList :: [Int] -> Int -> Int
sumList [] accumulator = accumulator
sumList (x:xs) accumulator = sumList xs (x + accumulator)
main :: IO ()
main = print (sumList [1, 2, 3, 4, 5] 0) (defn sum-list [numbers accumulator]
(if (empty? numbers)
accumulator
(recur (rest numbers) (+ (first numbers) accumulator))))
(println (sum-list [1 2 3 4 5] 0)) GHC silently and reliably optimizes an accumulator-passing recursive call like
sumList's into a constant-stack-space loop — no special syntax needed. Clojure, running on the JVM (which has no native tail-call optimization), requires the recursive call to be spelled explicitly with recur in tail position — using an ordinary self-call instead of recur would eventually overflow the stack on a long enough list, a real, concrete difference from Haskell's automatic guarantee.A Rare Shared Trait: Laziness
Infinite sequences: a rare, genuine laziness match
main :: IO ()
main = print (take 5 [1..]) (println (take 5 (range))) This is a rare, genuine parallel: unlike every strict language this site compares Haskell to, Clojure's core sequence functions are LAZY BY DEFAULT.
(range) with no arguments produces a genuinely infinite lazy sequence, and take forces only as many elements as are actually needed — the same behavior, and nearly the same syntax, as Haskell's take 5 [1..].Chaining lazy operations avoids building intermediate collections
main :: IO ()
main = print (take 3 (filter even [0..])) (println (take 3 (filter even? (range)))) Chaining
filter, map, and take in Clojure evaluates elements one at a time without ever materializing an intermediate collection — exactly the same efficiency property Haskell's laziness gives a chain like filter even . take 3. Of every language this site compares Haskell to, Clojure is the only one where "just chain the lazy functions" is the default behavior rather than something requiring an explicit lazy type or deferred computation.Laziness is the default you sometimes opt OUT of, not into
main :: IO ()
main = print (sum [1..1000000]) (println (reduce + (range 1000001))) Both languages default to lazy sequence processing for exactly this reason — summing a million numbers never needs to hold the whole collection in memory at once in either language. Where Clojure differs from Haskell in practice is realized-vs-unrealized bookkeeping: Clojure programmers occasionally reach for
doall/dorun to force a lazy sequence eagerly (usually for side effects), a concern that simply does not arise in Haskell, where laziness is even more pervasive and forcing is almost always implicit.Composition vs. Threading Macros
Point-free composition versus the ->> threading macro
main :: IO ()
main =
let result = sum (filter even (map (* 2) [1, 2, 3, 4, 5]))
in print result (println
(->> [1 2 3 4 5]
(map #(* % 2))
(filter even?)
(reduce +))) Haskell composes function VALUES right-to-left with
., reading inside-out unless you introduce intermediate names. Clojure's ->> ("thread-last") macro threads a value through a sequence of calls top-to-bottom, inserting it as the LAST argument to each — the same transformation, but read in execution order, closer to a Unix pipe or F#'s |> than to Haskell's own idiom.-> versus ->>: first-argument vs. last-argument threading
-- Haskell has no equivalent distinction — "." always composes
-- functions the same way regardless of where an argument
-- conceptually "belongs" in each function's parameter list:
main :: IO ()
main =
let describe = show . (+ 1)
in putStrLn (describe 5) (println
(-> {:name "Ada" :age 36}
(assoc :active true)
(dissoc :age))) Clojure actually has TWO threading macros with no single Haskell parallel:
-> ("thread-first") inserts the threaded value as the FIRST argument to each call — natural for functions like assoc/dissoc that take the map first — while ->> ("thread-last") inserts it LAST, natural for sequence functions like map/filter that take the collection last. Haskell's single composition operator makes no such distinction, since it just composes functions as values regardless of argument position.Type Classes vs. Protocols
Protocols: closer to type classes than Scheme offers
class Describable a where
describe :: a -> String
data Dog = Dog
instance Describable Dog where
describe _ = "a dog"
main :: IO ()
main = putStrLn (describe Dog) (defprotocol Describable
(describe [this]))
(defrecord Dog []
Describable
(describe [this] "a dog"))
(println (describe (->Dog))) Clojure's
defprotocol/defrecord combination offers real, type-directed dispatch: calling describe on a value automatically finds the implementation registered for that value's concrete type, the same "the runtime picks the right implementation for the type" behavior Haskell type classes provide — a genuinely closer match than Scheme has anything for, since Scheme has no polymorphic dispatch mechanism at all. The difference: Clojure resolves this at RUNTIME based on the value's type, since there is no compile-time type checker to resolve it earlier the way Haskell's does.Multimethods: dispatch on anything, not just a type
-- Haskell's typeclass dispatch is always keyed on a type, and only
-- a type — there is no way to dispatch on an arbitrary property of
-- the VALUE itself the way Clojure's multimethods allow:
class Priced a where
price :: a -> Double
main :: IO ()
main = putStrLn "Haskell dispatch is type-based only" (defmulti price :category)
(defmethod price :electronics [item] (* (:base-price item) 1.2))
(defmethod price :food [item] (* (:base-price item) 1.05))
(println (price {:category :electronics :base-price 100}))
(println (price {:category :food :base-price 100})) Clojure's
defmulti/defmethod can dispatch on ANY function of the arguments — here, a :category keyword pulled out of an ordinary map, not the value's type at all. Haskell's type-class dispatch is always keyed on a type and only a type; there is no way to make two values of the identical type resolve to different instances based on their runtime CONTENTS the way a Clojure multimethod effortlessly can.Code as Data
Homoiconicity: code and data share one representation
-- Haskell's AST is an internal compiler detail, invisible to
-- ordinary code — there is no way to construct a fragment of a
-- Haskell program AS a Haskell value using the language's own
-- ordinary literal syntax:
main :: IO ()
main = putStrLn "no direct Haskell equivalent to quoted code as data" (def code-as-data '(+ 1 2))
(println code-as-data) ; (+ 1 2) — printed as ordinary data
(println (eval code-as-data)) ; 3 — evaluated as code A quoted Clojure form like
'(+ 1 2) is simultaneously a piece of Clojure syntax AND an ordinary Clojure list value — printable, inspectable, and passable around as data, then evaluated later with eval if desired. Haskell has no equivalent: its AST is an internal compiler detail, never a value ordinary code can construct or manipulate directly. This "code is data" property (homoiconicity) is the foundation Lisp macros are built on.Quoting suppresses evaluation; a genuinely new idea for Haskell
-- Haskell evaluates every expression exactly as written — there is
-- no built-in way to write down "this expression, unevaluated, as
-- a value" using ordinary syntax:
main :: IO ()
main = print (1 + 2) (println (+ 1 2)) ; 3 — evaluated normally
(println '(+ 1 2)) ; (+ 1 2) — the quote suppresses evaluation The single quote before a form tells Clojure "treat this as literal data, do not evaluate it" — the same three symbols
(+ 1 2) mean two entirely different things depending on whether a quote precedes them. Haskell has no comparable built-in mechanism for suppressing evaluation of an expression written in ordinary syntax; every Haskell expression is always evaluated (or, thanks to laziness, evaluated on demand) — never held back as inert data the way a quoted Clojure form is.IORef vs. Atoms
IORef becomes an atom
import Data.IORef
main :: IO ()
main = do
ref <- newIORef 0
modifyIORef ref (+1)
modifyIORef ref (+1)
value <- readIORef ref
print value (def counter (atom 0))
(swap! counter inc)
(swap! counter inc)
(println @counter) Haskell's
IORef and Clojure's atom both wrap a single mutable value behind a controlled interface: newIORef/(atom 0) to create it, modifyIORef/swap! to update it with a function, and readIORef/@ (deref) to read it. Clojure's swap! additionally guarantees atomicity via compare-and-swap retry under concurrent access, a safety property IORef alone does not provide — Haskell would reach for MVar or STM for that guarantee instead.Both languages require an explicit wrapper to mutate at all
main :: IO ()
main =
let count = 0
-- count = 1 below would be a NEW binding shadowing the old one,
-- not a mutation — Haskell has no assignment operator at all:
in print count (def count 0)
; (set! count 1) below would fail — a top-level "def" cannot be
; reassigned; only a var explicitly marked ":dynamic" (rare) or a
; mutable reference type like an atom supports genuine mutation:
(println count) Both languages require an explicit, separate mechanism to opt into mutation at all — a plain Haskell
let and a plain Clojure def are both immutable, and neither offers a bare reassignment operator for an ordinary binding. This is a real point of alignment: Clojure, despite being dynamically typed and often assumed to be "more permissive" than Haskell, is just as strict about requiring explicit opt-in machinery (an atom, ref, or agent) before a value can be changed in place.Gotchas for Haskell Developers
Prefix notation and parentheses: the biggest visual shock
main :: IO ()
main = print (1 + 2 * 3) (println (+ 1 (* 2 3))) Haskell uses ordinary infix notation with operator precedence, familiar from arithmetic. Every Clojure operation — arithmetic included — is a prefix call wrapped in parentheses, with no operator precedence to reason about at all:
(+ 1 (* 2 3)) must nest the multiplication explicitly, since there is no *-binds-tighter-than-+ rule the way ordinary math notation has. This is consistently the single biggest visual adjustment for a Haskeller new to any Lisp.Only nil and false are falsy — a real gotcha versus Haskell Bool
main :: IO ()
main =
if (0 :: Int) /= 0
then putStrLn "nonzero is truthy"
else putStrLn "this branch never runs in Haskell either way — 0 has no Bool meaning at all" (if 0
(println "0 is truthy in Clojure!")
(println "this branch never runs")) Haskell's
if requires a genuine Bool — 0 is not even the right TYPE to put there, so this kind of mistake is a compile error, not a surprise at runtime. Clojure has looser "truthiness": every value is truthy in a boolean context EXCEPT nil and false — meaning 0, empty strings, and empty collections are all truthy, unlike several other dynamically-typed languages (Python, JavaScript, Ruby all treat at least some of those as falsy), and unlike Haskell, where the question cannot even arise.