| name | convert-clojure-haskell |
| description | Convert Clojure code to idiomatic Haskell. Use when migrating Clojure projects to Haskell, translating Clojure patterns to idiomatic Haskell, or refactoring Clojure codebases. Extends meta-convert-dev with Clojure-to-Haskell specific patterns. |
Convert Clojure to Haskell
Convert Clojure code to idiomatic Haskell. This skill extends meta-convert-dev with Clojure-to-Haskell specific type mappings, idiom translations, and tooling for migrating functional JVM code to native compiled Haskell.
This Skill Extends
meta-convert-dev- Foundational conversion patterns (APTV workflow, testing strategies)
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: Clojure types → Haskell types
- Idiom translations: Clojure patterns → idiomatic Haskell
- Error handling: Clojure exceptions → Haskell Maybe/Either
- Concurrency patterns: Clojure atoms/refs/agents → Haskell STM/async
- REPL workflow: REPL-driven development → GHCi interactive development
- Macro translation: Clojure macros → Haskell Template Haskell or type-level patterns
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Clojure language fundamentals - see
lang-clojure-dev - Haskell language fundamentals - see
lang-haskell-dev - Reverse conversion (Haskell → Clojure) - see
convert-haskell-clojure - ClojureScript → PureScript/Elm - see dedicated frontend conversion skills
Quick Reference
| Clojure | Haskell | Notes |
|---|---|---|
String |
String or Text |
Use Text for production |
Long |
Int or Integer |
Integer for unbounded |
Double |
Double |
Direct mapping |
Boolean |
Bool |
Direct mapping |
nil |
Nothing |
Part of Maybe type |
vector |
[] or Vector |
List or Data.Vector |
map |
Map |
Use Data.Map |
set |
Set |
Use Data.Set |
keyword |
Custom type or String |
No direct equivalent |
atom |
IORef or TVar |
Mutable reference |
ref |
TVar |
Software transactional memory |
agent |
Async |
Asynchronous computation |
(defn f [x] ...) |
f x = ... |
Function definition |
(fn [x] ...) |
\x -> ... |
Anonymous function |
try/catch |
Either or ExceptT |
Error handling |
When Converting Code
- Analyze source thoroughly - Understand Clojure's dynamic nature before writing static Haskell
- Map types first - Clojure's dynamic types need explicit Haskell types
- Preserve semantics over syntax similarity
- Embrace static typing - Use Haskell's type system to prevent runtime errors
- Handle nil properly - All potential nils become Maybe/Either
- Test equivalence - Same inputs → same outputs for pure logic
Type System Mapping
Primitive Types
| Clojure | Haskell | Notes |
|---|---|---|
String |
String |
List of Char (inefficient) |
String |
Text |
Preferred for production (from Data.Text) |
Long |
Int |
Bounded integer (architecture-dependent) |
Long |
Integer |
Unbounded (arbitrary precision) |
Double |
Float |
Single precision |
Double |
Double |
Preferred double precision |
Boolean |
Bool |
Direct mapping |
nil |
Nothing |
Use Maybe type |
Keyword |
Text or custom ADT |
Keywords are symbols; consider tagged types |
Symbol |
String |
Rarely needed; use at compile time |
Collection Types
| Clojure | Haskell | Notes |
|---|---|---|
(list ...) |
[a] |
Linked list |
[...] (vector) |
[a] or Vector a |
Use Data.Vector for indexed access |
{:key val} |
Map Text a |
Use Data.Map from containers |
#{...} |
Set a |
Use Data.Set from containers |
(seq ...) |
[a] |
Lazy sequences → lazy lists |
| Transient collections | Vector or mutable structures |
Use ST monad or Data.Vector |
Composite Types
| Clojure | Haskell | Notes |
|---|---|---|
(defrecord User [name age]) |
data User = User { name :: Text, age :: Int } |
Record syntax |
(deftype ...) |
data or newtype |
For performance or type safety |
| Maps as records | Record types or Map |
Prefer explicit records |
| Multi-arity functions | Multiple function definitions or tuples | Pattern matching on arity |
| Protocols | Type classes | Polymorphic behavior |
| Multimethods | Type classes or pattern matching | Dynamic dispatch → static dispatch |
Nil and Optional Values
Clojure:
(defn find-user [id users]
(first (filter #(= (:id %) id) users))) ; Returns nil if not found
(get {:name "Alice"} :age) ; Returns nil
Haskell:
import Data.Maybe (listToMaybe)
import qualified Data.Map as Map
findUser :: Int -> [User] -> Maybe User
findUser userId = listToMaybe . filter (\u -> userId == userId u)
-- Map lookup returns Maybe
Map.lookup "age" (Map.fromList [("name", "Alice")]) -- Nothing
Why this translation:
- Clojure's nil is pervasive; Haskell makes optionality explicit with Maybe
- All nil checks in Clojure become pattern matches on Nothing/Just
- Type safety prevents null pointer exceptions at compile time
Idiom Translation
Pattern 1: Threading Macros to Function Composition
Clojure:
;; Thread-first (->)
(-> data
(parse-json)
(get :users)
(filter active?)
(map :name)
(sort))
;; Thread-last (->>)
(->> (range 100)
(map inc)
(filter even?)
(reduce +))
Haskell:
import Data.Function ((&))
import qualified Data.List as List
-- Using function composition (right to left)
processData :: Value -> [Text]
processData = List.sort . map name . filter active . getUsers . parseJSON
-- Or using & operator (left to right, like ->)
processData' :: Value -> [Text]
processData' data =
data
& parseJSON
& getUsers
& filter active
& map name
& List.sort
-- Thread-last style
sumEvenInc :: Int
sumEvenInc = sum . filter even . map (+1) $ [0..99]
-- Or with $
sumEvenInc' = sum $ filter even $ map (+1) [0..99]
Why this translation:
- Clojure's
->maps to Haskell's&operator (from Data.Function) - Function composition (
.) is more idiomatic in Haskell but reads right-to-left - Use
$for right-associative application - Pattern:
(->> x f g h)→h . g . f $ x
Pattern 2: Destructuring
Clojure:
;; Sequential destructuring
(let [[a b & rest] [1 2 3 4 5]]
(+ a b))
;; Map destructuring
(defn greet [{:keys [name age] :or {age 0}}]
(str "Hello " name ", age " age))
(greet {:name "Alice" :age 30})
Haskell:
-- Pattern matching on lists
exampleList :: [Int] -> Int
exampleList (a:b:rest) = a + b
exampleList _ = 0
-- Record pattern matching
data Person = Person { name :: Text, age :: Int }
greet :: Person -> Text
greet Person{name, age} = "Hello " <> name <> ", age " <> show age
-- With default values (Maybe pattern)
greetMaybe :: Maybe Int -> Person -> Text
greetMaybe maybeAge Person{name} =
let actualAge = fromMaybe 0 maybeAge
in "Hello " <> name <> ", age " <> show actualAge
Why this translation:
- Clojure's destructuring is runtime; Haskell's pattern matching is compile-time
- Haskell enforces exhaustive pattern matching
- Record syntax provides named field access
- Default values use Maybe or function parameters
Pattern 3: Sequence Operations
Clojure:
;; Map, filter, reduce
(->> users
(filter :active)
(map :email)
(filter valid-email?)
(reduce conj #{}))
;; List comprehension with for
(for [x (range 10)
y (range 10)
:when (= (+ x y) 10)]
[x y])
Haskell:
import Data.Set (Set)
import qualified Data.Set as Set
-- Map, filter, fold
processUsers :: [User] -> Set Email
processUsers =
Set.fromList .
filter validEmail .
map email .
filter active
-- List comprehension
pairs :: [(Int, Int)]
pairs = [(x, y) | x <- [0..9], y <- [0..9], x + y == 10]
-- Or with do-notation (list monad)
pairs' :: [(Int, Int)]
pairs' = do
x <- [0..9]
y <- [0..9]
guard (x + y == 10)
return (x, y)
Why this translation:
- Both languages support functional pipelines
- Haskell's list comprehensions are more powerful (guards, multiple generators)
- Set.fromList is idiomatic for building sets from lists
- Do-notation provides monadic abstraction over list comprehension
Pattern 4: Lazy Sequences
Clojure:
;; Infinite sequences
(def naturals (iterate inc 0))
(take 5 naturals) ; (0 1 2 3 4)
;; Lazy evaluation
(def evens (filter even? naturals))
(take 3 evens) ; (0 2 4)
;; Custom lazy sequence
(defn fibonacci []
(map first (iterate (fn [[a b]] [b (+ a b)]) [0 1])))
Haskell:
-- Infinite lists (lazy by default)
naturals :: [Integer]
naturals = iterate (+1) 0
take 5 naturals -- [0,1,2,3,4]
-- Lazy filtering
evens :: [Integer]
evens = filter even naturals
take 3 evens -- [0,2,4]
-- Custom lazy sequence
fibonacci :: [Integer]
fibonacci = 0 : 1 : zipWith (+) fibonacci (tail fibonacci)
-- Or more explicit
fibonacci' :: [Integer]
fibonacci' = map fst $ iterate (\(a, b) -> (b, a + b)) (0, 1)
Why this translation:
- Both languages are lazy by default for sequences
- Haskell's laziness is pervasive; Clojure's is opt-in for sequences
- Infinite data structures work the same way
- Haskell's
zipWithprovides elegant recursive definitions
Paradigm Translation
Mental Model: Dynamic → Static Typing
| Clojure Approach | Haskell Approach | Key Insight |
|---|---|---|
| Runtime type checks | Compile-time type checking | Types guarantee correctness |
| Maps as flexible data | Records with defined fields | Explicit structure |
| Protocols for polymorphism | Type classes for polymorphism | Principled abstraction |
| nil anywhere | Maybe/Either for optionality | Explicit error handling |
| Exception throwing | Pure error values (Either) | Errors are values |
Concurrency Mental Model
| Clojure Model | Haskell Model | Conceptual Translation |
|---|---|---|
| Atoms (atomic updates) | IORef or TVar | Mutable reference |
| Refs (coordinated) | STM (Software Transactional Memory) | Coordinated updates |
| Agents (async) | Async library | Background computation |
| core.async channels | Concurrency library channels | CSP-style communication |
| Future/promise | Async or Future | Deferred computation |
Error Handling
Exceptions → Maybe/Either
Clojure:
(defn parse-age [s]
(try
(let [age (Integer/parseInt s)]
(if (pos? age)
age
(throw (ex-info "Age must be positive" {:age age}))))
(catch NumberFormatException e
(throw (ex-info "Invalid number" {:input s})))))
(defn validate-user [age-str email-str]
(try
{:age (parse-age age-str)
:email (validate-email email-str)}
(catch Exception e
nil)))
Haskell:
import Text.Read (readMaybe)
import Data.Text (Text)
data ValidationError
= InvalidNumber Text
| NegativeAge Int
| InvalidEmail Text
deriving (Show, Eq)
parseAge :: Text -> Either ValidationError Int
parseAge s =
case readMaybe (unpack s) of
Nothing -> Left (InvalidNumber s)
Just age ->
if age > 0
then Right age
else Left (NegativeAge age)
validateUser :: Text -> Text -> Either ValidationError User
validateUser ageStr emailStr = do
age <- parseAge ageStr
email <- validateEmail emailStr
return $ User age email
-- Or with Applicative for independent validations
validateUser' :: Text -> Text -> Either ValidationError User
validateUser' ageStr emailStr =
User <$> parseAge ageStr <*> validateEmail emailStr
Why this translation:
- Clojure exceptions are runtime; Haskell Either is type-checked
- Either forces handling of error cases at compile time
- Do-notation provides clean error chaining (like try/catch flow)
- Applicative style validates independently and collects errors
Exception Handling Patterns
| Clojure Pattern | Haskell Pattern | Notes |
|---|---|---|
try/catch |
Either a b or ExceptT |
Pure error handling |
throw |
Left err or throwError |
Return error value |
ex-info with data |
Custom ADT error types | Structured errors |
finally |
bracket or finally |
Resource cleanup |
nil for missing |
Maybe a |
Optional values |
Concurrency Patterns
Atoms → IORef/TVar
Clojure:
(def counter (atom 0))
;; Atomic update
(swap! counter inc)
(swap! counter + 10)
;; Read value
@counter
;; Reset value
(reset! counter 0)
;; Conditional update
(compare-and-set! counter 0 100)
Haskell:
import Data.IORef
import Control.Concurrent.STM
-- Using IORef (not transactional)
example :: IO ()
example = do
counter <- newIORef 0
-- Atomic update
modifyIORef' counter (+1)
modifyIORef' counter (+10)
-- Read value
value <- readIORef counter
-- Write value
writeIORef counter 0
-- Using TVar (transactional)
exampleSTM :: IO ()
exampleSTM = do
counter <- newTVarIO 0
atomically $ do
modifyTVar' counter (+1)
modifyTVar' counter (+10)
-- Read
value <- readTVarIO counter
-- Write
atomically $ writeTVar counter 0
Why this translation:
- Clojure atoms provide atomic updates; Haskell IORef or TVar similar
- For simple cases, IORef sufficient; for composition, use STM
- STM provides composable transactions like Clojure refs
- Both guarantee atomic updates without locks
Refs → Software Transactional Memory (STM)
Clojure:
(def account-a (ref 100))
(def account-b (ref 200))
;; Coordinated transaction
(dosync
(alter account-a - 50)
(alter account-b + 50))
;; Read consistent snapshot
(dosync
[@account-a @account-b])
Haskell:
import Control.Concurrent.STM
transfer :: TVar Int -> TVar Int -> Int -> IO ()
transfer fromAccount toAccount amount = atomically $ do
fromBalance <- readTVar fromAccount
toBalance <- readTVar toAccount
writeTVar fromAccount (fromBalance - amount)
writeTVar toAccount (toBalance + amount)
-- Read consistent snapshot
readAccounts :: TVar Int -> TVar Int -> IO (Int, Int)
readAccounts account1 account2 = atomically $ do
bal1 <- readTVar account1
bal2 <- readTVar account2
return (bal1, bal2)
-- Usage
main :: IO ()
main = do
accountA <- newTVarIO 100
accountB <- newTVarIO 200
transfer accountA accountB 50
(a, b) <- readAccounts accountA accountB
print (a, b) -- (50, 250)
Why this translation:
- Both use Software Transactional Memory for coordinated updates
- Clojure's dosync = Haskell's atomically
- alter/commute = modifyTVar/writeTVar
- Both provide automatic retry on conflicts
- Both guarantee ACID properties
Agents → Async
Clojure:
(def logger (agent []))
;; Send async update
(send logger conj "Entry 1")
(send logger conj "Entry 2")
;; Wait for completion
(await logger)
;; For blocking operations
(send-off logger
(fn [logs]
(Thread/sleep 1000)
(conj logs "Delayed")))
Haskell:
import Control.Concurrent.Async
import Control.Concurrent (threadDelay)
-- Using async library
exampleAsync :: IO ()
exampleAsync = do
-- Launch async computations
a1 <- async $ return (1 :: Int)
a2 <- async $ return (2 :: Int)
-- Wait for results
result1 <- wait a1
result2 <- wait a2
print (result1 + result2)
-- For sequential async operations (like agents)
processLogs :: [String] -> IO ()
processLogs initialLogs = do
ref <- newIORef initialLogs
-- Spawn background worker
async $ do
threadDelay 1000000 -- 1 second
modifyIORef' ref (++ ["Delayed entry"])
-- Continue main work...
return ()
Why this translation:
- Clojure agents are for async sequential updates
- Haskell async provides concurrent execution
- For sequential updates, combine async with IORef
- Both allow non-blocking computation
core.async → Channels
Clojure:
(require '[clojure.core.async :as async])
(let [ch (async/chan 10)]
;; Producer
(async/go
(async/>! ch "Hello")
(async/>! ch "World")
(async/close! ch))
;; Consumer
(async/go-loop []
(when-let [msg (async/<! ch)]
(println msg)
(recur))))
Haskell:
import Control.Concurrent
import Control.Concurrent.Chan
exampleChannels :: IO ()
exampleChannels = do
ch <- newChan
-- Producer
forkIO $ do
writeChan ch "Hello"
writeChan ch "World"
-- Note: Chan doesn't have explicit close
-- Consumer
forkIO $ forever $ do
msg <- readChan ch
putStrLn msg
threadDelay 1000000 -- Wait for processing
-- Or with STM channels (bounded)
import Control.Concurrent.STM.TBQueue
exampleBounded :: IO ()
exampleBounded = do
queue <- newTBQueueIO 10
forkIO $ do
atomically $ writeTBQueue queue "Hello"
atomically $ writeTBQueue queue "World"
forkIO $ forever $ do
msg <- atomically $ readTBQueue queue
putStrLn msg
Why this translation:
- Both provide CSP-style channels for communication
- Clojure's core.async go blocks = Haskell's forkIO
- STM channels provide bounded queues like core.async
- Both enable producer/consumer patterns
Memory & Ownership
Immutability by Default
Both Clojure and Haskell embrace immutability, but with different enforcement:
| Aspect | Clojure | Haskell |
|---|---|---|
| Default | Immutable persistent structures | Pure values (immutable) |
| Mutable escape hatch | Atoms, refs, agents | IO monad, ST monad |
| Enforcement | Convention (runtime) | Type system (compile-time) |
| Structure sharing | Yes (persistent data structures) | Yes (via laziness and GC) |
Clojure:
;; All updates return new values
(def v1 [1 2 3])
(def v2 (conj v1 4)) ; v1 unchanged
;; v1 => [1 2 3]
;; v2 => [1 2 3 4]
;; Structural sharing
(def big-map (into {} (map vector (range 10000) (range 10000))))
(def updated (assoc big-map 5000 "changed")) ; O(log n), shares most nodes
Haskell:
-- All values are immutable by default
v1 = [1, 2, 3]
v2 = v1 ++ [4] -- v1 unchanged
-- v1 = [1,2,3]
-- v2 = [1,2,3,4]
-- Structural sharing via laziness
import qualified Data.Map as Map
bigMap = Map.fromList [(i, i) | i <- [0..9999]]
updated = Map.insert 5000 "changed" bigMap -- O(log n), shares structure
Why this translation:
- Both languages default to immutability
- Haskell enforces purity via types; Clojure via convention
- Performance characteristics similar due to structural sharing
- Mutable state explicit in both (atoms/refs vs IORef/TVar)
Macro Translation
Clojure Macros → Haskell Alternatives
Clojure macros operate at the syntactic level; Haskell provides multiple alternatives:
| Clojure Macro Use Case | Haskell Alternative | Notes |
|---|---|---|
| Code generation | Template Haskell | Compile-time metaprogramming |
| DSL creation | Embedded DSL with operators | Type-safe DSLs |
| Conditional compilation | CPP or Cabal flags | Preprocessing |
| Control flow abstraction | Higher-order functions | Functions as first-class |
| Syntax transformation | Type classes + operators | Principled abstraction |
Clojure:
;; Custom control flow macro
(defmacro unless [condition & body]
`(if (not ~condition)
(do ~@body)))
(unless false
(println "This runs"))
;; DSL macro
(defmacro with-logging [expr]
`(let [start# (System/currentTimeMillis)
result# ~expr]
(println "Took" (- (System/currentTimeMillis) start#) "ms")
result#))
Haskell:
{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH
-- Template Haskell for code generation
-- (Advanced use case, often unnecessary)
-- More idiomatic: Higher-order functions
unless :: Bool -> IO () -> IO ()
unless condition action =
if not condition
then action
else return ()
-- Usage
unless False $ putStrLn "This runs"
-- Logging via function composition
import System.CPUTime
import Text.Printf
withLogging :: IO a -> IO a
withLogging action = do
start <- getCPUTime
result <- action
end <- getCPUTime
let diff = fromIntegral (end - start) / (10^12)
printf "Computation time: %0.3f sec\n" (diff :: Double)
return result
-- Usage
withLogging $ do
putStrLn "Working..."
return ()
Why this translation:
- Most Clojure macros can be replaced with Haskell functions
- Higher-order functions provide abstraction without compile-time magic
- Template Haskell available for true compile-time metaprogramming
- Type system + operators enable many DSLs without macros
Common Pitfalls
1. Dynamic Type Assumptions → Static Type Requirements
Problem: Clojure allows heterogeneous collections; Haskell requires homogeneous types.
;; Clojure: Mixed types OK
(def mixed [1 "two" :three 4.0])
-- Haskell: Need sum type for mixed
data Value
= IntVal Int
| StringVal String
| KeywordVal String
| DoubleVal Double
mixed :: [Value]
mixed = [IntVal 1, StringVal "two", KeywordVal "three", DoubleVal 4.0]
Fix: Use algebraic data types (ADTs) to represent variants.
2. Nil Propagation → Maybe Chaining
Problem: Clojure's nil freely propagates; Haskell requires explicit handling.
;; Clojure: nil just flows through
(-> data :user :email str/upper-case) ; NPE if any step is nil
-- Haskell: Must handle Maybe at each step
import qualified Data.Text as T
import Data.Maybe (fromMaybe)
processEmail :: Data -> Maybe T.Text
processEmail d = do
user <- getUser d
email <- getEmail user
return $ T.toUpper email
-- Or with combinators
processEmail' :: Data -> T.Text
processEmail' d =
fromMaybe "" $ fmap T.toUpper (getUser d >>= getEmail)
Fix: Use Maybe monad or applicative functors to chain computations.
3. Lazy Sequences vs Lazy Evaluation
Problem: Clojure sequences are explicitly lazy; Haskell is lazy everywhere.
;; Clojure: Force evaluation when needed
(let [xs (map expensive-fn (range 1000))]
(doall xs) ; Force evaluation
xs)
-- Haskell: Lazy by default, force with strictness annotations
import Control.DeepSeq
let xs = map expensiveFn [0..999]
in xs `deepseq` xs -- Force full evaluation
-- Or use strict data structures
import qualified Data.Vector as V
let xs = V.map expensiveFn (V.enumFromN 0 1000)
in xs -- Vector is strict
Fix: Understand laziness difference; use strict evaluation when needed.
4. Keyword Keys → Text or Custom Types
Problem: Clojure keywords don't have direct Haskell equivalent.
;; Clojure: Keywords as map keys
{:name "Alice" :age 30 :email "alice@example.com"}
-- Option 1: Use records (preferred)
data User = User
{ name :: Text
, age :: Int
, email :: Text
}
-- Option 2: Use Text keys in Map
import qualified Data.Map as Map
userMap :: Map Text String
userMap = Map.fromList
[ ("name", "Alice")
, ("age", "30")
, ("email", "alice@example.com")
]
-- Option 3: Custom keyword type
newtype Keyword = Keyword Text deriving (Eq, Ord, Show)
keywordMap :: Map Keyword String
keywordMap = Map.fromList
[ (Keyword "name", "Alice")
, (Keyword "age", "30")
]
Fix: Use records for structured data; Map for truly dynamic cases.
5. REPL Workflow Differences
Problem: Clojure's REPL allows redefining anything; GHCi more restricted.
Clojure:
;; Can reload everything at runtime
(require 'myapp.core :reload)
(in-ns 'myapp.core)
Haskell (GHCi):
-- Type changes require restart
:reload -- Reload current modules
:type expr -- Check types
:info Name -- Get information
-- For rapid development, use ghcid
-- ghcid watches files and reloads automatically
Fix: Use ghcid for auto-reload; accept that type changes need restart.
Tooling
Development Tools
| Tool | Purpose | Clojure Equivalent |
|---|---|---|
| GHC | Haskell compiler | Clojure compiler (JVM) |
| GHCi | Interactive REPL | Clojure REPL |
| Cabal | Build tool & package manager | Leiningen |
| Stack | Alternative build tool | Leiningen + profiles |
| Hoogle | Type-based search | clojure.repl/apropos |
| HLint | Linter | Eastwood |
| ghcid | Auto-reload dev tool | REPL-driven dev |
| Haddock | Documentation generator | Codox |
Build Configuration Mapping
Clojure (project.clj):
(defproject myapp "0.1.0"
:dependencies [[org.clojure/clojure "1.11.1"]
[cheshire "5.12.0"]
[compojure "1.7.0"]]
:main myapp.core)
Haskell (package.yaml or .cabal):
# package.yaml (for stack/hpack)
name: myapp
version: 0.1.0
dependencies:
- base >= 4.7 && < 5
- aeson # JSON (like cheshire)
- text # Text handling
- warp # Web server (like ring)
executables:
myapp:
main: Main.hs
source-dirs: src
Library Equivalents
| Purpose | Clojure | Haskell |
|---|---|---|
| JSON | cheshire | aeson |
| HTTP client | clj-http | http-client, req |
| Web framework | Ring/Compojure | Warp/Servant |
| Database | clojure.java.jdbc | persistent, postgresql-simple |
| Testing | clojure.test | HUnit, QuickCheck, Hspec |
| Async | core.async | async, stm |
| CLI parsing | tools.cli | optparse-applicative |
| Logging | timbre | fast-logger, katip |
Examples
Example 1: Simple - Data Transformation
Before (Clojure):
(defn process-users [users]
(->> users
(filter :active)
(map :email)
(map str/lower-case)
(into #{})))
;; Usage
(process-users
[{:name "Alice" :email "ALICE@EXAMPLE.COM" :active true}
{:name "Bob" :email "BOB@EXAMPLE.COM" :active false}
{:name "Carol" :email "CAROL@EXAMPLE.COM" :active true}])
;; => #{"alice@example.com" "carol@example.com"}
After (Haskell):
import qualified Data.Text as T
import qualified Data.Set as Set
data User = User
{ name :: T.Text
, email :: T.Text
, active :: Bool
} deriving (Show, Eq)
processUsers :: [User] -> Set.Set T.Text
processUsers =
Set.fromList .
map (T.toLower . email) .
filter active
-- Usage
let users =
[ User "Alice" "ALICE@EXAMPLE.COM" True
, User "Bob" "BOB@EXAMPLE.COM" False
, User "Carol" "CAROL@EXAMPLE.COM" True
]
in processUsers users
-- fromList ["alice@example.com","carol@example.com"]
Example 2: Medium - Error Handling
Before (Clojure):
(defn parse-user [data]
(try
(let [age (Integer/parseInt (:age data))]
(when (neg? age)
(throw (ex-info "Negative age" {:age age})))
{:name (:name data)
:age age
:email (:email data)})
(catch NumberFormatException e
(throw (ex-info "Invalid age format" {:input (:age data)})))
(catch Exception e
nil)))
(defn process-user-data [raw-data]
(try
(let [user (parse-user raw-data)]
(when (some nil? (vals user))
(throw (ex-info "Missing fields" {:user user})))
user)
(catch Exception e
{:error (.getMessage e)})))
After (Haskell):
import qualified Data.Text as T
import Text.Read (readMaybe)
data User = User
{ userName :: T.Text
, userAge :: Int
, userEmail :: T.Text
} deriving (Show, Eq)
data ParseError
= InvalidAgeFormat T.Text
| NegativeAge Int
| MissingField T.Text
deriving (Show, Eq)
parseUser :: Map T.Text T.Text -> Either ParseError User
parseUser dataMap = do
name <- maybe (Left $ MissingField "name") Right $ Map.lookup "name" dataMap
ageStr <- maybe (Left $ MissingField "age") Right $ Map.lookup "age" dataMap
email <- maybe (Left $ MissingField "email") Right $ Map.lookup "email" dataMap
age <- case readMaybe (T.unpack ageStr) of
Nothing -> Left $ InvalidAgeFormat ageStr
Just a | a < 0 -> Left $ NegativeAge a
| otherwise -> Right a
return $ User name age email
-- Or with Applicative for cleaner code
import Control.Applicative ((<|>))
parseUser' :: Map T.Text T.Text -> Either ParseError User
parseUser' m =
User <$> getField "name" m
<*> (getField "age" m >>= parseAge)
<*> getField "email" m
where
getField k = maybe (Left $ MissingField k) Right $ Map.lookup k m
parseAge s = case readMaybe (T.unpack s) of
Nothing -> Left $ InvalidAgeFormat s
Just a | a < 0 -> Left $ NegativeAge a
| otherwise -> Right a
Example 3: Complex - Concurrent Processing
Before (Clojure):
(require '[clojure.core.async :as async])
(defn fetch-user [id]
(Thread/sleep 100) ; Simulate network call
{:id id :name (str "User-" id) :score (rand-int 100)})
(defn process-users [ids]
(let [ch (async/chan)
results (atom [])]
;; Spawn workers
(doseq [id ids]
(async/go
(let [user (fetch-user id)]
(async/>! ch user))))
;; Collect results
(async/go-loop [remaining (count ids)]
(when (pos? remaining)
(let [user (async/<! ch)]
(swap! results conj user)
(recur (dec remaining)))))
;; Wait and return
(Thread/sleep 500)
(->> @results
(sort-by :score)
(reverse)
(take 5))))
;; Usage
(process-users (range 20))
After (Haskell):
import Control.Concurrent.Async
import Control.Concurrent (threadDelay)
import Data.List (sortBy)
import Data.Ord (Down(..), comparing)
data User = User
{ userId :: Int
, userName :: String
, userScore :: Int
} deriving (Show, Eq)
fetchUser :: Int -> IO User
fetchUser uid = do
threadDelay 100000 -- 0.1 seconds (microseconds)
score <- randomRIO (0, 99)
return $ User uid ("User-" ++ show uid) score
processUsers :: [Int] -> IO [User]
processUsers ids = do
-- Spawn async tasks
asyncUsers <- mapM (async . fetchUser) ids
-- Wait for all results
users <- mapM wait asyncUsers
-- Sort by score (descending) and take top 5
let topUsers = take 5 $ sortBy (comparing (Down . userScore)) users
return topUsers
-- Usage
main :: IO ()
main = do
topUsers <- processUsers [0..19]
mapM_ print topUsers
-- Alternative with parallel processing
import Control.Parallel.Strategies
processUsersParallel :: [Int] -> IO [User]
processUsersParallel ids = do
users <- mapM fetchUser ids
let sorted = take 5 $ sortBy (comparing (Down . userScore)) users
return sorted
Why this translation:
- Clojure's core.async go blocks → Haskell's async tasks
- Both provide concurrent execution
- Haskell's async library handles errors automatically
- Sorting and taking top N is identical pattern
- Type safety prevents many concurrency bugs at compile time
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language examplesconvert-elm-haskell- Similar functional → Haskell conversionlang-clojure-dev- Clojure development patternslang-haskell-dev- Haskell development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- Async, channels, threads across languagespatterns-serialization-dev- JSON, validation across languagespatterns-metaprogramming-dev- Macros, Template Haskell across languages