Claude Code Plugins

Community-maintained marketplace

Feedback

convert-clojure-haskell

@aRustyDev/ai
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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

  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:

(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 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:

(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 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