| name | convert-haskell-clojure |
| description | Convert Haskell code to idiomatic Clojure. Use when migrating Haskell projects to Clojure, translating pure functional patterns to practical functional JVM programming, or refactoring Haskell codebases to leverage REPL-driven development and dynamic typing. Extends meta-convert-dev with Haskell-to-Clojure specific patterns. |
Convert Haskell to Clojure
Convert Haskell code to idiomatic Clojure. This skill extends meta-convert-dev with Haskell-to-Clojure specific type mappings, idiom translations, and transformation strategies for moving from pure lazy functional programming with static types to practical dynamic functional programming on the JVM.
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: Haskell static types (HM) → Clojure dynamic types
- Idiom translations: Type classes → protocols, monads → explicit threading, lazy → lazy seqs
- Error handling: Maybe/Either → nil/exceptions or tagged maps
- Concurrency patterns: STM/async → atoms/refs/agents, parallel → pmap/reducers
- Evaluation strategy: Lazy (default) → strict with explicit lazy seqs
- REPL workflow: GHCi → Clojure REPL-driven development
- Effects: IO monad → effects anywhere (with discipline)
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Haskell language fundamentals - see
lang-haskell-dev - Clojure language fundamentals - see
lang-clojure-dev - Reverse conversion (Clojure → Haskell) - see
convert-clojure-haskell - Advanced type-level programming - Template Haskell, GADTs, Type Families have no direct equivalent
Quick Reference
| Haskell | Clojure | Notes |
|---|---|---|
String or Text |
String |
Clojure strings are Java strings |
Int / Integer |
Long / BigInteger |
Clojure uses Long by default |
Double |
Double |
Java Double |
Bool |
Boolean |
true / false |
Nothing |
nil |
Represents absence |
Just x |
x |
Value present (check with some?) |
[a] |
[] or (list) |
Vector or list |
Map k v |
{} |
Hash map |
Set a |
#{} |
Hash set |
data T = ... |
(defrecord T ...) |
Named types |
newtype |
No equivalent | Use plain value or reify |
type |
(def T ...) |
Type alias |
f :: a -> b |
(defn f [a] ...) |
Function (no type signature) |
\x -> ... |
(fn [x] ...) or #(...) |
Lambda |
f . g |
(comp f g) |
Function composition (reversed order) |
fmap |
map |
Transform in context |
>>= |
mapcat or explicit chaining |
Monadic bind |
do notation |
let or threading macros |
Sequential operations |
Maybe a |
Value or nil |
Optional values |
Either e a |
{:ok val} / {:error e} |
Result type pattern |
IO a |
No equivalent | Effects anywhere |
| Type classes | Protocols | Polymorphism |
STM |
Refs with dosync |
Software transactional memory |
async |
future / pmap |
Async computation |
When Converting Code
- Remove type signatures - Clojure is dynamically typed
- Make laziness explicit - Haskell lazy by default, Clojure strict by default
- Replace monads with idioms -
Maybe→ nil checks,Either→ tagged maps,IO→ direct effects - Convert type classes to protocols - Or use multimethods for dynamic dispatch
- Embrace REPL workflow - Test incrementally, explore data interactively
- Handle purity boundaries - No IO monad; effects can happen anywhere (maintain discipline)
- Test equivalence - Property-based testing with
test.check
Type System Mapping
Primitive Types
| Haskell | Clojure | Notes |
|---|---|---|
Int |
Long |
64-bit signed integer (default) |
Integer |
BigInteger |
Arbitrary precision (use N suffix: 42N) |
Float |
Float |
32-bit float (rare) |
Double |
Double |
64-bit float (default: 3.14) |
Bool |
Boolean |
true / false |
Char |
Character |
Single character (rare, use strings) |
String |
String |
Java String (immutable) |
Text |
String |
Both map to Java String |
() (unit) |
nil |
Void/no meaningful return |
Collection Types
| Haskell | Clojure | Notes |
|---|---|---|
[a] (list) |
(list 1 2 3) or '(1 2 3) |
Linked list (rarely used) |
[a] |
[1 2 3] |
Preferred: Vector (indexed access) |
Map k v |
{:key val} |
Hash map (fast lookup) |
Set a |
#{1 2 3} |
Hash set (unique elements) |
(a, b) |
[a b] |
Tuple as vector |
(a, b, c) |
[a b c] |
Multi-element tuple |
| Infinite list | Lazy seq: (iterate inc 0) |
Must be explicit |
Composite Types
| Haskell | Clojure | Notes |
|---|---|---|
data User = User { name :: String, age :: Int } |
(defrecord User [name age]) |
Named record type |
type UserId = Int |
(def UserId Long) |
Type alias (documentation only) |
newtype Email = Email String |
Plain string or custom validation | No compile-time wrapper |
data Color = Red | Green | Blue |
#{:red :green :blue} or spec |
Enum as keyword set |
data Result a = Ok a | Err String |
{:ok val} or {:error msg} |
Tagged map pattern |
Optional and Error Types
Haskell:
-- Maybe for optional values
findUser :: UserId -> Maybe User
findUser uid = Map.lookup uid users
-- Either for errors
divide :: Double -> Double -> Either String Double
divide _ 0 = Left "Division by zero"
divide a b = Right (a / b)
Clojure:
;; nil for optional values
(defn find-user [user-id]
(get users user-id)) ; Returns nil if not found
;; Tagged maps for errors (idiomatic)
(defn divide [a b]
(if (zero? b)
{:error "Division by zero"}
{:ok (/ a b)}))
;; Or throw exceptions
(defn divide! [a b]
(when (zero? b)
(throw (ex-info "Division by zero" {:a a :b b})))
(/ a b))
Why this translation:
- Haskell's Maybe forces explicit handling; Clojure's nil is pervasive (less safe)
- Either → tagged maps is idiomatic Clojure for recoverable errors
- Exceptions are acceptable in Clojure for exceptional cases
- Use
ex-infofor exceptions with data
Idiom Translation
Pattern: Type Classes → Protocols
Haskell:
-- Type class for serialization
class Serializable a where
serialize :: a -> ByteString
deserialize :: ByteString -> Maybe a
-- Instance for User
instance Serializable User where
serialize (User name age) = encode (name, age)
deserialize bs = case decode bs of
Just (name, age) -> Just (User name age)
Nothing -> Nothing
-- Use
let json = serialize user
Clojure:
;; Protocol for serialization
(defprotocol Serializable
(serialize [this])
(deserialize [this bytes]))
;; Extend for User record
(defrecord User [name age])
(extend-protocol Serializable
User
(serialize [user]
(json/generate-string user))
(deserialize [_ bytes]
(json/parse-string bytes true)))
;; Or inline with defrecord
(defrecord User [name age]
Serializable
(serialize [this]
(json/generate-string this)))
;; Use
(serialize user)
Why this translation:
- Type classes define polymorphic interfaces; protocols do the same in Clojure
- Protocols are more dynamic: can extend existing types at runtime
- No compile-time type checking in Clojure
extend-protocolextends multiple types at once
Pattern: Monadic Do-Notation → Threading Macros
Haskell:
-- Maybe monad chaining
processUser :: UserId -> Maybe Result
processUser uid = do
user <- findUser uid
profile <- getProfile user
settings <- getSettings profile
return (computeResult settings)
-- Or with bind
processUser' uid =
findUser uid >>= getProfile >>= getSettings >>= return . computeResult
Clojure:
;; Using some-> (thread-first, stop on nil)
(defn process-user [user-id]
(some-> user-id
find-user
get-profile
get-settings
compute-result))
;; Or with when-let for explicit nil handling
(defn process-user [user-id]
(when-let [user (find-user user-id)]
(when-let [profile (get-profile user)]
(when-let [settings (get-settings profile)]
(compute-result settings)))))
;; Or using monads library (cats, algo.monads)
(require '[cats.core :as m])
(require '[cats.monad.maybe :as maybe])
(defn process-user [user-id]
(m/mlet [user (maybe/maybe (find-user user-id))
profile (maybe/maybe (get-profile user))
settings (maybe/maybe (get-settings profile))]
(m/return (compute-result settings))))
Why this translation:
some->is idiomatic Clojure for Maybe-like chaining- Stops threading at first nil
- For complex error handling, tagged maps or exceptions are more common
- Monads library exists but not idiomatic for most Clojure code
Pattern: Function Composition
Haskell:
-- Right-to-left composition
processData :: [Int] -> Int
processData = sum . filter even . map (*2)
-- Or with $
processData' xs = sum $ filter even $ map (*2) xs
-- Or with &
import Data.Function ((&))
processData'' xs =
xs
& map (*2)
& filter even
& sum
Clojure:
;; Thread-last (->>), left-to-right
(defn process-data [xs]
(->> xs
(map #(* % 2))
(filter even?)
(reduce +)))
;; Or comp (right-to-left like Haskell)
(def process-data
(comp
#(reduce + %)
#(filter even? %)
#(map (fn [x] (* x 2)) %)))
;; But ->> is more idiomatic
Why this translation:
- Haskell's
.composes right-to-left:(f . g . h) x = f (g (h x)) - Clojure's
compalso composes right-to-left - But
->>(thread-last) is more idiomatic and reads left-to-right - Use
->>for data transformations
Pattern: Pattern Matching
Haskell:
-- Pattern matching on ADT
data Shape = Circle Double
| Rectangle Double Double
| Triangle Double Double Double
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rectangle w h) = w * h
area (Triangle a b c) =
let s = (a + b + c) / 2
in sqrt (s * (s - a) * (s - b) * (s - c))
-- Pattern matching on lists
sumList :: [Int] -> Int
sumList [] = 0
sumList (x:xs) = x + sumList xs
Clojure:
;; Use maps with :type key for tagged unions
(defn area [shape]
(case (:type shape)
:circle (let [{:keys [radius]} shape]
(* Math/PI radius radius))
:rectangle (let [{:keys [width height]} shape]
(* width height))
:triangle (let [{:keys [a b c]} shape
s (/ (+ a b c) 2)]
(Math/sqrt (* s (- s a) (- s b) (- s c))))))
;; Or use multimethods for polymorphism
(defmulti area :type)
(defmethod area :circle [{:keys [radius]}]
(* Math/PI radius radius))
(defmethod area :rectangle [{:keys [width height]}]
(* width height))
(defmethod area :triangle [{:keys [a b c]}]
(let [s (/ (+ a b c) 2)]
(Math/sqrt (* s (- s a) (- s b) (- s c)))))
;; Pattern matching on lists
(defn sum-list [xs]
(if (empty? xs)
0
(+ (first xs) (sum-list (rest xs)))))
;; Or idiomatic reduce
(defn sum-list [xs]
(reduce + 0 xs))
Why this translation:
- Haskell has first-class ADTs; Clojure uses maps with
:typekeys caseon type tag for simple dispatch- Multimethods for extensible polymorphism
- Pattern matching on lists uses
first/restinstead of: - Prefer higher-order functions (reduce, map) over explicit recursion
Pattern: Lazy Evaluation
Haskell:
-- Lazy by default
naturals :: [Integer]
naturals = [0..] -- Infinite list, no problem
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
-- Take only what you need
take 10 fibs -- [0,1,1,2,3,5,8,13,21,34]
Clojure:
;; Strict by default, lazy sequences explicit
(def naturals (iterate inc 0)) ; Lazy seq
(def fibs
(map first (iterate (fn [[a b]] [b (+ a b)]) [0 1])))
;; Or using lazy-seq
(defn fib-seq
([] (fib-seq 0 1))
([a b]
(lazy-seq (cons a (fib-seq b (+ a b))))))
(take 10 fibs) ; (0 1 1 2 3 5 8 13 21 34)
;; Force realization with doall
(doall (take 10 fibs)) ; Realizes all elements
Why this translation:
- Haskell: lazy by default, force with
seq/deepseq/ bang patterns - Clojure: strict by default, lazy with
lazy-seq,iterate,repeat,cycle - Use
iteratefor infinite sequences - Use
taketo consume only what's needed doallforces realization (opposite of Haskell's forcing strictness)
Pattern: List Comprehensions
Haskell:
-- List comprehension with guards
pythagorean :: Int -> [(Int, Int, Int)]
pythagorean n = [(a, b, c) | a <- [1..n],
b <- [a..n],
c <- [b..n],
a^2 + b^2 == c^2]
Clojure:
;; for comprehension
(defn pythagorean [n]
(for [a (range 1 (inc n))
b (range a (inc n))
c (range b (inc n))
:when (= (+ (* a a) (* b b)) (* c c))]
[a b c]))
Why this translation:
- Syntax is very similar
- Haskell uses
|, Clojure uses:whenfor filters - Generators bind with
<-in Haskell, bind directly in Clojure - Both support multiple generators and filters
Error Handling
Maybe → Nil
Haskell:
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just x
-- Use with pattern matching
case safeHead xs of
Nothing -> defaultValue
Just x -> processValue x
-- Or with maybe function
maybe defaultValue processValue (safeHead xs)
Clojure:
(defn safe-head [xs]
(first xs)) ; Returns nil for empty
;; Use with if-let
(if-let [x (safe-head xs)]
(process-value x)
default-value)
;; Or with some-> (nil-safe threading)
(some-> xs safe-head process-value)
;; Or with or for default
(or (safe-head xs) default-value)
Why this translation:
- Haskell forces explicit handling with Maybe
- Clojure uses nil pervasively (less safe but more convenient)
if-let,when-let,some->provide nil-safe operations- Use
orfor default values
Either → Tagged Maps or Exceptions
Haskell:
parseAge :: String -> Either String Int
parseAge s = case reads s of
[(n, "")] | n >= 0 -> Right n
_ -> Left "Invalid age"
-- Chain with do-notation
validateUser :: UserData -> Either String User
validateUser userData = do
age <- parseAge (ageString userData)
email <- validateEmail (emailString userData)
return (User (userName userData) age email)
Clojure:
;; Tagged maps (idiomatic for recoverable errors)
(defn parse-age [s]
(try
(let [n (Long/parseLong s)]
(if (>= n 0)
{:ok n}
{:error "Age must be non-negative"}))
(catch NumberFormatException _
{:error "Not a valid number"})))
;; Chain with helper
(defn ok? [result]
(contains? result :ok))
(defn validate-user [user-data]
(let [age-result (parse-age (:age user-data))]
(if (ok? age-result)
(let [email-result (validate-email (:email user-data))]
(if (ok? email-result)
{:ok {:name (:name user-data)
:age (:ok age-result)
:email (:ok email-result)}}
email-result))
age-result)))
;; Or exceptions (idiomatic for unrecoverable errors)
(defn parse-age! [s]
(let [n (Long/parseLong s)] ; Throws on invalid
(when (< n 0)
(throw (ex-info "Age must be non-negative" {:age n})))
n))
Why this translation:
- Either forces error handling; Clojure uses tagged maps or exceptions
- Tagged maps
{:ok val}/{:error msg}are common for recoverable errors - Exceptions are acceptable for programming errors or exceptional conditions
- No monadic chaining; use explicit conditionals or helper macros
Concurrency Patterns
STM → Refs with dosync
Haskell:
import Control.Concurrent.STM
type Account = TVar Int
transfer :: Account -> Account -> Int -> STM ()
transfer from to amount = do
fromBalance <- readTVar from
when (fromBalance < amount) retry
modifyTVar from (subtract amount)
modifyTVar to (+ amount)
-- Run transaction
main = do
account1 <- newTVarIO 1000
account2 <- newTVarIO 0
atomically $ transfer account1 account2 500
Clojure:
;; Refs for coordinated, synchronous updates
(def account1 (ref 1000))
(def account2 (ref 0))
(defn transfer [from to amount]
(dosync
(let [from-balance @from]
(when (< from-balance amount)
(throw (ex-info "Insufficient funds" {})))
(alter from - amount)
(alter to + amount))))
;; Use
(transfer account1 account2 500)
;; Or with commute for non-order-dependent updates
(dosync
(commute account1 - 500)
(commute account2 + 500))
Why this translation:
- Both use software transactional memory
- Haskell:
TVarwithatomically,readTVar,modifyTVar,retry - Clojure:
refwithdosync,@(deref),alter,commute - Haskell's
retryblocks until condition met; Clojure uses explicit checks
Async → Future / Pmap
Haskell:
import Control.Concurrent.Async
main = do
(result1, result2) <- concurrently
(fetchUrl "http://example.com/1")
(fetchUrl "http://example.com/2")
print (result1, result2)
-- Race: first to complete wins
winner <- race
(fetchFromServer1 key)
(fetchFromServer2 key)
Clojure:
;; future for async execution
(let [result1 (future (fetch-url "http://example.com/1"))
result2 (future (fetch-url "http://example.com/2"))]
(println [@result1 @result2])) ; @ blocks until ready
;; pmap for parallel map
(def results
(pmap fetch-url ["http://example.com/1"
"http://example.com/2"]))
;; No built-in race; use promises
(defn race [& fns]
(let [p (promise)]
(doseq [f fns]
(future (deliver p (f))))
@p))
(race #(fetch-from-server1 key)
#(fetch-from-server2 key))
Why this translation:
- Haskell
asynclibrary → Clojurefuture concurrently→ spawn futures, deref allrace→ promise with multiple futures (first delivery wins)mapConcurrently→pmap(parallel map)
Metaprogramming
Template Haskell → Macros
Haskell:
{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH
-- Generate function at compile time
$(do
let name = mkName "add5"
let body = [| \x -> x + 5 |]
[d| $(varP name) = $body |]
)
-- Use
result = add5 10 -- 15
Clojure:
;; Macros expand at compile time
(defmacro add-n [n]
`(fn [x#] (+ x# ~n)))
(def add5 (add-n 5))
(add5 10) ; => 15
;; Or simpler: just generate code
(defmacro defadder [name n]
`(defn ~name [x#]
(+ x# ~n)))
(defadder add5 5)
(add5 10) ; => 15
Why this translation:
- Both provide compile-time code generation
- Template Haskell is more powerful but complex
- Clojure macros are simpler, more accessible
- Quote/unquote syntax:
`(quote),~(unquote),~@(unquote-splice) - Auto-gensym with
#:x#generates unique symbol
Serialization
Aeson → Cheshire/Transit
Haskell:
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson
import GHC.Generics
data User = User
{ name :: Text
, email :: Text
, age :: Int
} deriving (Generic, Show)
instance FromJSON User
instance ToJSON User
-- Encode/decode
encodeUser :: User -> ByteString
encodeUser = encode
decodeUser :: ByteString -> Maybe User
decodeUser = decode
Clojure:
;; Using Cheshire for JSON
(require '[cheshire.core :as json])
;; Records for structured data
(defrecord User [name email age])
;; Encode
(defn encode-user [user]
(json/generate-string user))
;; Decode
(defn decode-user [json-str]
(json/parse-string json-str true)) ; true = keywordize keys
;; Or use Transit for Clojure types
(require '[cognitect.transit :as transit])
(import '[java.io ByteArrayOutputStream ByteArrayInputStream])
(defn to-transit [data]
(let [out (ByteArrayOutputStream.)]
(transit/write (transit/writer out :json) data)
(.toString out)))
(defn from-transit [s]
(transit/read
(transit/reader
(ByteArrayInputStream. (.getBytes s))
:json)))
Why this translation:
- Haskell Aeson uses Generic deriving; Clojure uses runtime serialization
- Cheshire for JSON (most common)
- Transit preserves Clojure data types (keywords, sets, etc.)
- No compile-time validation in Clojure
Build and Dependencies
Cabal/Stack → Leiningen/tools.deps
Haskell (Cabal):
name: my-app
version: 0.1.0.0
build-depends: base >= 4.14 && < 5
, text >= 1.2
, aeson >= 2.0
, containers
library
exposed-modules: MyApp
hs-source-dirs: src
Haskell (Stack):
resolver: lts-21.0
packages: [.]
extra-deps:
- some-package-1.0.0
Clojure (Leiningen):
(defproject my-app "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.11.1"]
[cheshire "5.12.0"]
[org.clojure/data.json "2.4.0"]])
Clojure (tools.deps):
{:deps
{org.clojure/clojure {:mvn/version "1.11.1"}
cheshire/cheshire {:mvn/version "5.12.0"}}}
Why this translation:
- Cabal/Stack manage packages and versions; Leiningen/tools.deps do the same
- Dependencies from Maven Central (Clojure) vs Hackage/Stackage (Haskell)
- Both support local dependencies and git dependencies
Testing
QuickCheck → test.check
Haskell:
import Test.QuickCheck
-- Property: reversing twice gives original
prop_reverse_involutive :: [Int] -> Bool
prop_reverse_involutive xs = reverse (reverse xs) == xs
-- Property: sorted list is sorted
prop_sort_sorted :: [Int] -> Bool
prop_sort_sorted xs = isSorted (sort xs)
-- Run
main = do
quickCheck prop_reverse_involutive
quickCheck prop_sort_sorted
Clojure:
(ns myapp.props-test
(:require [clojure.test :refer [deftest is]]
[clojure.test.check :as tc]
[clojure.test.check.generators :as gen]
[clojure.test.check.properties :as prop]
[clojure.test.check.clojure-test :refer [defspec]]))
;; Property: reversing twice gives original
(defspec reverse-involutive 100
(prop/for-all [v (gen/vector gen/small-integer)]
(= v (vec (reverse (reverse v))))))
;; Property: sorted output
(defspec sort-produces-sorted 100
(prop/for-all [v (gen/vector gen/small-integer)]
(let [sorted (sort v)]
(every? (fn [[a b]] (<= a b))
(partition 2 1 sorted)))))
Why this translation:
- Both are property-based testing libraries
- QuickCheck → test.check (same concepts)
quickCheck→defspecortc/quick-checkArbitrarygenerators →gen/generatorsforAll→prop/for-all
Common Pitfalls
1. Assuming Static Type Safety
;; ❌ No compile-time type errors
(defn add [a b]
(+ a b))
(add "hello" "world") ; Runtime error!
Fix: Use spec for runtime validation
(require '[clojure.spec.alpha :as s])
(s/def ::number number?)
(defn add [a b]
{:pre [(s/valid? ::number a) (s/valid? ::number b)]}
(+ a b))
2. Not Handling Nil Explicitly
;; ❌ Nil is not checked at compile time
(defn process [data]
(.toUpperCase (:name data))) ; NullPointerException if :name is nil
Fix: Use nil-safe operations
(defn process [data]
(some-> data :name .toUpperCase))
;; Or explicit check
(defn process [data]
(when-let [name (:name data)]
(.toUpperCase name)))
3. Forgetting Lazy Evaluation Differences
;; ❌ This realizes the entire sequence multiple times
(let [nums (map expensive-fn (range 1000))]
(+ (count nums) (first nums) (last nums)))
Fix: Force realization once
(let [nums (vec (map expensive-fn (range 1000)))]
(+ (count nums) (first nums) (last nums)))
4. Over-using Exceptions vs. Tagged Maps
;; ❌ Exceptions for control flow
(try
(divide a b)
(catch ArithmeticException e
:division-by-zero))
Fix: Use tagged maps for expected errors
(let [result (divide a b)]
(if (:error result)
(handle-error result)
(handle-success (:ok result))))
5. Not Leveraging REPL-Driven Development
;; ❌ Writing entire function without testing
(defn complex-algorithm [data]
;; 50 lines of code
)
Fix: Build incrementally in REPL
;; In REPL:
(def sample-data {...})
;; Step 1
(def step1 (parse-data sample-data))
;; Inspect step1
;; Step 2
(def step2 (transform step1))
;; Inspect step2
;; Combine into function after validation
(defn complex-algorithm [data]
(-> data parse-data transform))
Tooling
| Category | Haskell | Clojure | Notes |
|---|---|---|---|
| Build tool | Cabal, Stack | Leiningen, tools.deps | Package management |
| REPL | GHCi | lein repl, clj |
Interactive development |
| Package registry | Hackage, Stackage | Clojars, Maven Central | Dependency sources |
| Testing | HSpec, QuickCheck | clojure.test, test.check | Unit + property-based |
| Linting | HLint | clj-kondo, eastwood | Static analysis |
| Formatter | Ormolu, Brittany | cljfmt, zprint | Code formatting |
| Doc generation | Haddock | Codox | API documentation |
Examples
Example 1: Simple - Function with Pattern Matching
Before (Haskell):
-- Factorial with pattern matching
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
After (Clojure):
;; Factorial with cond
(defn factorial [n]
(if (zero? n)
1
(* n (factorial (dec n)))))
;; Or with recur for tail recursion
(defn factorial [n]
(loop [n n acc 1]
(if (zero? n)
acc
(recur (dec n) (* acc n)))))
Example 2: Medium - Map Transformation with Maybe
Before (Haskell):
import Data.Maybe (mapMaybe)
import qualified Data.Map as Map
-- Extract emails from user map, filtering out Nothing
getEmails :: Map.Map UserId User -> [Email]
getEmails users =
mapMaybe (userEmail . snd) (Map.toList users)
userEmail :: User -> Maybe Email
userEmail (User _ email _) = email
After (Clojure):
;; Extract emails, filtering out nil
(defn get-emails [users]
(->> users
vals
(map :email)
(filter some?)))
;; Or with keep (map + filter non-nil in one step)
(defn get-emails [users]
(keep :email (vals users)))
Example 3: Complex - Concurrent Processing with STM
Before (Haskell):
import Control.Concurrent.STM
import Control.Concurrent.Async
import qualified Data.Map as Map
type Cache = TVar (Map.Map Key Value)
-- Concurrent cache operations
updateCache :: Cache -> Key -> Value -> STM ()
updateCache cache key value = do
m <- readTVar cache
writeTVar cache (Map.insert key value m)
lookupCache :: Cache -> Key -> STM (Maybe Value)
lookupCache cache key = do
m <- readTVar cache
return (Map.lookup key m)
-- Process items concurrently and update cache
processItems :: Cache -> [Item] -> IO ()
processItems cache items = do
results <- mapConcurrently processItem items
atomically $ mapM_ (uncurry (updateCache cache)) results
processItem :: Item -> IO (Key, Value)
processItem item = do
value <- expensiveComputation item
return (itemKey item, value)
After (Clojure):
;; Using refs for coordinated updates
(def cache (ref {}))
;; Concurrent cache operations
(defn update-cache! [cache key value]
(dosync
(alter cache assoc key value)))
(defn lookup-cache [cache key]
(dosync
(get @cache key)))
;; Process items concurrently and update cache
(defn process-items! [cache items]
(let [results (pmap process-item items)]
(dosync
(doseq [[k v] results]
(alter cache assoc k v)))))
(defn process-item [item]
(let [value (expensive-computation item)]
[(:key item) value]))
;; Or using atoms for independent updates (simpler)
(def cache-atom (atom {}))
(defn update-cache-atom! [cache key value]
(swap! cache assoc key value))
(defn process-items-atom! [cache items]
(doseq [item (pmap process-item items)]
(let [[k v] item]
(swap! cache assoc k v))))
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational conversion patterns (APTV workflow, testing strategies)convert-clojure-haskell- Reverse conversion (Clojure → Haskell)lang-haskell-dev- Haskell language fundamentalslang-clojure-dev- Clojure language fundamentals
Cross-cutting pattern skills:
patterns-concurrency-dev- Concurrency patterns across languages (STM, async, actors)patterns-serialization-dev- Serialization patterns across languages (JSON, validation)patterns-metaprogramming-dev- Metaprogramming across languages (macros, Template Haskell)