Skip to content
Back to skills

Convert Clojure Haskell

ASecurity

Bidirectional conversion between Clojure and Haskell. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Clojure↔Haskell specific patterns. 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.

  • 8 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 6, 2026
developmentgojavasqlnodetestingrefactoringapidatabasefrontendperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add aRustyDev/agents --skill convert-clojure-haskell --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Convert Clojure Haskell?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Convert Clojure Haskell
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/arustydev-convert-clojure-haskell-agents/badge)](https://www.skillsdirectory.com/skills/arustydev-convert-clojure-haskell-agents)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: convert-clojure-haskell
description: Bidirectional conversion between Clojure and Haskell. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Clojure↔Haskell specific patterns. 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.
---

# Clojure ↔ Haskell Conversion

Bidirectional conversion between Clojure and Haskell. This skill extends `meta-convert-dev` with Clojure↔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`
- 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

1. **Analyze source thoroughly** - Understand Clojure's dynamic nature before writing static Haskell
2. **Map types first** - Clojure's dynamic types need explicit Haskell types
3. **Preserve semantics** over syntax similarity
4. **Embrace static typing** - Use Haskell's type system to prevent runtime errors
5. **Handle nil properly** - All potential nils become Maybe/Either
6. **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:**
```clojure
(defn find-user [id users]
  (first (filter #(= (:id %) id) users)))  ; Returns nil if not found

(get {:name "Alice"} :age)  ; Returns nil
```

**Haskell:**
```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:**
```clojure
;; Thread-first (->)
(-> data
    (parse-json)
    (get :users)
    (filter active?)
    (map :name)
    (sort))

;; Thread-last (->>)
(->> (range 100)
     (map inc)
     (filter even?)
     (reduce +))
```

**Haskell:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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 `zipWith` provides 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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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:**
```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
;; Clojure: Mixed types OK
(def mixed [1 "two" :three 4.0])
```

```haskell
-- 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
;; Clojure: nil just flows through
(-> data :user :email str/upper-case)  ; NPE if any step is nil
```

```haskell
-- 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
;; Clojure: Force evaluation when needed
(let [xs (map expensive-fn (range 1000))]
  (doall xs)  ; Force evaluation
  xs)
```

```haskell
-- 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
;; Clojure: Keywords as map keys
{:name "Alice" :age 30 :email "alice@example.com"}
```

```haskell
-- 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:**
```clojure
;; Can reload everything at runtime
(require 'myapp.core :reload)
(in-ns 'myapp.core)
```

**Haskell (GHCi):**
```haskell
-- 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):**
```clojure
(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):**
```yaml
# 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):**
```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):**
```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):**
```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):**
```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):**
```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):**
```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 examples
- `convert-elm-haskell` - Similar functional → Haskell conversion
- `lang-clojure-dev` - Clojure development patterns
- `lang-haskell-dev` - Haskell development patterns

Cross-cutting pattern skills:
- `patterns-concurrency-dev` - Async, channels, threads across languages
- `patterns-serialization-dev` - JSON, validation across languages
- `patterns-metaprogramming-dev` - Macros, Template Haskell across languages

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…