Agents convert-clojure-haskell
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.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-clojure-haskell" ~/.claude/skills/arustydev-agents-convert-clojure-haskell && rm -rf "$T"
content/skills/convert-clojure-haskell/SKILL.mdClojure ↔ 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
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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 |
|---|---|---|
| or | Use Text for production |
| or | Integer for unbounded |
| | Direct mapping |
| | Direct mapping |
| | Part of Maybe type |
| or | List or Data.Vector |
| | Use Data.Map |
| | Use Data.Set |
| Custom type or | No direct equivalent |
| or | Mutable reference |
| | Software transactional memory |
| | Asynchronous computation |
| | Function definition |
| | Anonymous function |
| or | 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 |
|---|---|---|
| | List of Char (inefficient) |
| | Preferred for production (from ) |
| | Bounded integer (architecture-dependent) |
| | Unbounded (arbitrary precision) |
| | Single precision |
| | Preferred double precision |
| | Direct mapping |
| | Use Maybe type |
| or custom ADT | Keywords are symbols; consider tagged types |
| | Rarely needed; use at compile time |
Collection Types
| Clojure | Haskell | Notes |
|---|---|---|
| | Linked list |
(vector) | or | Use Data.Vector for indexed access |
| | Use Data.Map from |
| | Use Data.Set from |
| | Lazy sequences → lazy lists |
| Transient collections | or mutable structures | Use ST monad or Data.Vector |
Composite Types
| Clojure | Haskell | Notes |
|---|---|---|
| | Record syntax |
| or | For performance or type safety |
| Maps as records | Record types or | 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
provides elegant recursive definitionszipWith
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 |
|---|---|---|
| or | Pure error handling |
| or | Return error value |
with data | Custom ADT error types | Structured errors |
| or | Resource cleanup |
for missing | | 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:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Similar functional → Haskell conversionconvert-elm-haskell
- Clojure development patternslang-clojure-dev
- Haskell development patternslang-haskell-dev
Cross-cutting pattern skills:
- Async, channels, threads across languagespatterns-concurrency-dev
- JSON, validation across languagespatterns-serialization-dev
- Macros, Template Haskell across languagespatterns-metaprogramming-dev