Agents convert-elixir-haskell
Bidirectional conversion between Elixir and Haskell. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Elixir↔Haskell specific patterns. Use when migrating Elixir projects to Haskell, translating BEAM actor patterns to pure functional programming with IO monad, or refactoring dynamic OTP behaviors to static typed equivalents. Extends meta-convert-dev with Elixir-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-elixir-haskell" ~/.claude/skills/arustydev-agents-convert-elixir-haskell && rm -rf "$T"
content/skills/convert-elixir-haskell/SKILL.mdElixir ↔ Haskell Conversion
Bidirectional conversion between Elixir and Haskell. This skill extends
meta-convert-dev with Elixir↔Haskell specific type mappings, idiom translations, and transformation strategies for moving from BEAM's actor model to pure functional programming with strong static types.
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: Elixir dynamic types → Haskell static types (Hindley-Milner)
- Idiom translations: Actors/OTP → STM/async, pattern matching nuances, pipe vs composition
- Error handling: Tagged tuples → Maybe/Either, supervision → explicit error handling
- Async patterns: GenServer/Tasks → IO monad, async library, STM
- Evaluation strategy: Strict (Elixir) → Lazy (Haskell) translation
- Effects: Effects anywhere → IO monad boundary, pure core
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Elixir language fundamentals - see
lang-elixir-dev - Haskell language fundamentals - see
lang-haskell-dev
Quick Reference
| Elixir | Haskell | Notes |
|---|---|---|
| | Either monad for results |
| | Either monad for errors |
| | Maybe monad |
| | Maybe monad |
| or | Functor/list operations |
| or or | Function application/composition |
| <br> | Function with type signature |
| + | Actor → transactional memory |
| | Concurrent execution |
| Pattern match on | Message passing |
| | Hash map |
| | Map construction |
When Converting Code
- Analyze effects first - Identify where side effects occur in Elixir
- Map types explicitly - Create complete type mapping table from dynamic to static
- Separate pure from impure - Pure core with IO boundary
- Translate actors to alternatives - GenServer → STM, supervision → error handling
- Handle laziness - Elixir strict, Haskell lazy by default
- Test equivalence - Property-based testing for invariants
Type System Mapping
Primitive Types
| Elixir | Haskell | Notes |
|---|---|---|
| / | fixed-width, arbitrary precision |
| | 64-bit float |
| | / |
| Custom ADT | , → data constructors |
/ | / | Use for UTF-8 |
| | is in Haskell |
| / | Process identifiers |
| / | Reference types |
Collection Types
| Elixir | Haskell | Notes |
|---|---|---|
| | Linked list |
| | Fixed-size tuple |
(map) | | Requires |
| | Requires |
| Keyword list | | List of pairs |
| Range | | List comprehension range |
Composite Types
| Elixir | Haskell | Notes |
|---|---|---|
| Struct | | Record syntax |
| | |
| | |
| | |
| Value | | |
| Union types (spec) | | Sum type (ADT) |
| GenServer state | | Shared mutable state |
| Protocol | Type class | Polymorphism |
Function Types
| Elixir | Haskell | Notes |
|---|---|---|
| | Curried by default |
| | Side-effecting function |
| | Pure function |
| Anonymous fn | Lambda | Lambda syntax |
Idiom Translation
Pattern: Tagged Tuples → Either/Maybe
Elixir:
def divide(a, b) when b != 0, do: {:ok, a / b} def divide(_, 0), do: {:error, :division_by_zero} case divide(10, 2) do {:ok, result} -> IO.puts("Result: #{result}") {:error, reason} -> IO.puts("Error: #{reason}") end
Haskell:
divide :: Float -> Float -> Either String Float divide a 0 = Left "division by zero" divide a b = Right (a / b) case divide 10 2 of Right result -> putStrLn $ "Result: " ++ show result Left reason -> putStrLn $ "Error: " ++ reason -- Or with do-notation (Either monad) calculation :: Either String Float calculation = do a <- divide 10 2 b <- divide a 5 return (b * 2)
Why this translation:
- Elixir uses tagged tuples
/{:ok, value}
idiomatically{:error, reason} - Haskell's
type encodes the same semantics with stronger type safetyEither - Pattern matching works similarly in both
- Haskell's
monad allows chaining withEither
-notationdo
Pattern: Pipe Operator → Function Composition
Elixir:
result = [1, 2, 3, 4] |> Enum.filter(&(rem(&1, 2) == 0)) |> Enum.map(&(&1 * 2)) |> Enum.sum()
Haskell:
-- Point-free with composition result = sum . map (*2) . filter even $ [1, 2, 3, 4] -- Or with ($) for clarity result = sum $ map (*2) $ filter even [1, 2, 3, 4] -- Or with (&) for left-to-right (Data.Function) import Data.Function ((&)) result = [1, 2, 3, 4] & filter even & map (*2) & sum
Why this translation:
- Elixir's
passes result forward (left-to-right)|> - Haskell's
composes right-to-left:.(f . g) x = f (g x) - Use
for right-to-left with clarity, or$
for left-to-right& - Point-free style is idiomatic Haskell
Pattern: Pattern Matching with Guards
Elixir:
def classify(n) when n < 0, do: :negative def classify(0), do: :zero def classify(n) when n < 10, do: :small def classify(_), do: :large
Haskell:
-- Using guards classify :: Int -> String classify n | n < 0 = "negative" | n == 0 = "zero" | n < 10 = "small" | otherwise = "large" -- Or with case classify' :: Int -> String classify' n = case n of 0 -> "zero" _ | n < 0 -> "negative" | n < 10 -> "small" | otherwise -> "large"
Why this translation:
- Both languages support guard clauses
- Haskell uses
for guards instead of|when
is the catch-all (equivalent to Elixir'sotherwise
)_- Pattern matching on literals comes before guards in Haskell
Pattern: Enum Comprehensions → List Comprehensions
Elixir:
result = for x <- [1, 2, 3, 4, 5], y <- [1, 2, 3], x * y > 5, do: {x, y}
Haskell:
result = [(x, y) | x <- [1..5], y <- [1..3], x * y > 5]
Why this translation:
- Syntax is nearly identical
- Haskell's list comprehensions are more concise
- Filters come after generators in both
- Multiple generators work the same way
Pattern: Recursive List Processing
Elixir:
def sum([]), do: 0 def sum([head | tail]), do: head + sum(tail) def map([], _func), do: [] def map([head | tail], func), do: [func.(head) | map(tail, func)]
Haskell:
sum' :: [Int] -> Int sum' [] = 0 sum' (x:xs) = x + sum' xs map' :: (a -> b) -> [a] -> [b] map' _ [] = [] map' f (x:xs) = f x : map' f xs
Why this translation:
- Both use head/tail pattern matching (
vs[head | tail]
)(x:xs) - Base case (empty list) first in both
- Haskell requires type signatures (recommended in Elixir)
- Haskell's cons operator
is infix:
Pattern: With Statement → Do-Notation
Elixir:
def create_user(params) do with {:ok, validated} <- validate_params(params), {:ok, user} <- insert_user(validated), {:ok, email_sent} <- send_email(user) do {:ok, user} else {:error, reason} -> {:error, reason} end end
Haskell:
createUser :: Params -> IO (Either String User) createUser params = runExceptT $ do validated <- ExceptT $ return $ validateParams params user <- ExceptT $ insertUser validated emailSent <- ExceptT $ sendEmail user return user -- Or with Either monad directly createUser' :: Params -> Either String User createUser' params = do validated <- validateParams params user <- insertUser validated emailSent <- sendEmail user return user
Why this translation:
- Elixir's
chains operations that can failwith - Haskell's
-notation fordo
monad achieves the sameEither
transformer for mixingExceptT
withIOEither- Short-circuits on first
(error) automaticallyLeft
Error Handling
Elixir Error Model → Haskell Error Model
| Elixir Pattern | Haskell Pattern | Notes |
|---|---|---|
| | Success case |
| | Error case |
| | Absence of value |
| | Presence of value |
| | Runtime exception (avoid) |
| Supervisor restart | Explicit error handling | No supervision trees |
| / | Exception handling (rare) |
Pattern: Supervision → Explicit Error Handling
Elixir:
# Supervisor restarts failed processes defmodule MyApp.Supervisor do use Supervisor def start_link(_) do Supervisor.start_link(__MODULE__, :ok, name: __MODULE__) end def init(:ok) do children = [ {Worker, []} ] Supervisor.init(children, strategy: :one_for_one) end end
Haskell:
-- Explicit retry logic with error handling import Control.Exception (try, SomeException) import Control.Concurrent (threadDelay) retryWithBackoff :: Int -> IO a -> IO (Either SomeException a) retryWithBackoff 0 action = try action retryWithBackoff n action = do result <- try action case result of Right val -> return $ Right val Left _ -> do threadDelay (1000000 * 2^(5-n)) -- Exponential backoff retryWithBackoff (n-1) action -- Worker that can fail and be retried worker :: IO () worker = do result <- retryWithBackoff 5 dangerousOperation case result of Right val -> processSuccess val Left err -> logError err
Why this translation:
- Elixir: "Let it crash" philosophy with supervisor restart
- Haskell: Explicit error handling with retry logic
- No built-in supervision trees in Haskell
- Must handle failures explicitly or use exception handling
Pattern: Result Propagation
Elixir:
def process_pipeline(input) do with {:ok, validated} <- validate(input), {:ok, transformed} <- transform(validated), {:ok, result} <- store(transformed) do {:ok, result} end end
Haskell:
processPipeline :: Input -> Either String Result processPipeline input = do validated <- validate input transformed <- transform validated result <- store transformed return result -- Or with applicative for independent operations processPipeline' input = validate input >>= transform >>= store
Why this translation:
- Both short-circuit on first error
- Haskell's
monad provides same chainingEither
(bind) chains dependent operations>>=- More concise than nested
statementscase
Concurrency Patterns
Elixir Concurrency → Haskell Concurrency
| Elixir | Haskell | Notes |
|---|---|---|
| Process (lightweight) | | Haskell threads are OS threads |
| | Spawn concurrent thread |
| | Async computation |
| | Wait for async result |
| | Send to channel |
| | Receive from channel |
| + STM | Stateful server |
| / | Shared mutable state |
| Supervisor | Manual retry logic | No built-in supervision |
Pattern: GenServer → STM
Elixir:
defmodule Counter do use GenServer def start_link(initial) do GenServer.start_link(__MODULE__, initial, name: __MODULE__) end def increment do GenServer.call(__MODULE__, :increment) end def get do GenServer.call(__MODULE__, :get) end # Callbacks def init(initial), do: {:ok, initial} def handle_call(:increment, _from, state) do {:reply, state + 1, state + 1} end def handle_call(:get, _from, state) do {:reply, state, state} end end
Haskell:
import Control.Concurrent.STM type Counter = TVar Int createCounter :: Int -> IO Counter createCounter initial = newTVarIO initial increment :: Counter -> IO Int increment counter = atomically $ do current <- readTVar counter let new = current + 1 writeTVar counter new return new getCount :: Counter -> IO Int getCount counter = readTVarIO counter -- Usage main = do counter <- createCounter 0 result1 <- increment counter result2 <- increment counter final <- getCount counter print final -- 2
Why this translation:
- GenServer: Message-passing actor with state
- STM: Software Transactional Memory for safe concurrent mutations
- Both provide atomicity and state isolation
- STM is compositional (can combine transactions)
- No message queues in STM (direct state access)
Pattern: Task.async → Async
Elixir:
task1 = Task.async(fn -> fetch_user(1) end) task2 = Task.async(fn -> fetch_user(2) end) user1 = Task.await(task1) user2 = Task.await(task2)
Haskell:
import Control.Concurrent.Async main = do task1 <- async $ fetchUser 1 task2 <- async $ fetchUser 2 user1 <- wait task1 user2 <- wait task2 -- Or concurrently main = do (user1, user2) <- concurrently (fetchUser 1) (fetchUser 2) -- Map concurrently over list users <- mapConcurrently fetchUser [1..10]
Why this translation:
spawns concurrent computation, returns handleTask.async
library provides same semanticsasync
blocks until result availablewait
helper for pairsconcurrently- Similar error propagation (async throws exceptions)
Pattern: Message Passing → Channels
Elixir:
pid = spawn(fn -> receive do {:msg, value} -> IO.puts("Received: #{value}") end end) send(pid, {:msg, "hello"})
Haskell:
import Control.Concurrent import Control.Concurrent.Chan main = do chan <- newChan forkIO $ do msg <- readChan chan putStrLn $ "Received: " ++ msg writeChan chan "hello" threadDelay 100000 -- Wait for thread
Why this translation:
- Elixir: Process mailbox with pattern matching
- Haskell: Typed channels (Chan a)
- No pattern matching on messages (type-safe)
- Must use explicit channel types
for single-value handoff,MVar
for queuesChan
Evaluation Strategy Translation
Strict → Lazy Conversion Patterns
Elixir evaluates strictly (arguments evaluated before function call). Haskell evaluates lazily (arguments evaluated only when needed).
Elixir (strict):
# All elements processed immediately list = Enum.map([1, 2, 3, 4, 5], fn x -> expensive_computation(x) end) result = Enum.take(list, 2) # But we only need 2!
Haskell (lazy):
-- Only first 2 elements computed list = map expensiveComputation [1, 2, 3, 4, 5] result = take 2 list -- Lazy: only computes first 2
Key Differences:
| Aspect | Elixir (Strict) | Haskell (Lazy) |
|---|---|---|
| Evaluation | Immediate | On-demand |
| Infinite lists | Not possible | Natural |
| Side effects | Predictable order | Deferred (use IO) |
| Performance | Eager memory use | Space leaks possible |
Pattern: Forcing Strictness in Haskell
When you need strict evaluation:
-- Lazy fold can cause stack overflow badSum = foldl (+) 0 [1..1000000] -- Builds thunks -- Strict fold import Data.List (foldl') goodSum = foldl' (+) 0 [1..1000000] -- Forces evaluation -- Bang patterns {-# LANGUAGE BangPatterns #-} strictFunc !x = x + 1 -- x evaluated immediately
Pattern: Streams in Elixir → Lazy Lists in Haskell
Elixir:
# Stream for lazy evaluation Stream.iterate(0, &(&1 + 1)) |> Stream.map(&(&1 * 2)) |> Stream.filter(&(rem(&1, 2) == 0)) |> Enum.take(10)
Haskell:
-- Lists are lazy by default result = take 10 $ filter even $ map (*2) $ iterate (+1) 0
Why this translation:
- Elixir: Explicit
for lazinessStream - Haskell: All lists are lazy
- Both use similar pipeline patterns
- Haskell infinite lists are natural
Effects and IO Boundary
Separating Pure from Impure
Elixir (effects anywhere):
def process_user(id) do # Mix of pure and impure user = Repo.get(User, id) # IO: Database name = String.upcase(user.name) # Pure Logger.info("Processing #{name}") # IO: Logging %{user | name: name} # Pure end
Haskell (pure core with IO boundary):
-- Pure functions uppercaseName :: User -> User uppercaseName user = user { userName = T.toUpper (userName user) } -- IO boundary processUser :: Int -> IO User processUser userId = do user <- getUser userId -- IO: Database let updated = uppercaseName user -- Pure logInfo $ "Processing " <> userName updated -- IO: Logging return updated -- Type signature shows effects -- :: Int -> User (pure) -- :: Int -> IO User (has IO effects)
Why this translation:
- Elixir: Effects can appear anywhere
- Haskell: Type system tracks effects (
type)IO - Pure functions don't use
typeIO - Easier to reason about effects in Haskell
- Must explicitly lift pure values into
withIOreturn
Pattern: Database Queries
Elixir (Ecto):
def get_active_users do from(u in User, where: u.active == true) |> Repo.all() end
Haskell (persistent or esqueleto):
import Database.Persist import Database.Persist.Sql getActiveUsers :: SqlPersistM [Entity User] getActiveUsers = selectList [UserActive ==. True] [] -- In IO context main :: IO () main = runSqlite "database.db" $ do users <- getActiveUsers liftIO $ mapM_ print users
Why this translation:
- Both use type-safe query builders
- Haskell: Explicit monad for database operations
is the DB monadSqlPersistM
to perform IO in DB contextliftIO
Common Pitfalls
-
Forgetting Lazy Evaluation: Haskell lists are lazy. Use strict functions (
) when needed to avoid space leaks.foldl' -
Mixing IO and Pure: In Haskell, functions must declare
in type signature. Can't perform IO in pure functions.IO -
Pattern Match Exhaustiveness: Haskell compiler warns about non-exhaustive patterns. Elixir allows partial patterns.
-
Trying to Mutate State: No mutation in Haskell. Use STM/MVar for shared state or pass new state explicitly.
-
Ignoring Type Inference Limitations: Haskell can't always infer types. Add explicit type signatures at module boundaries.
-
Translating Supervision Literally: No supervision trees. Use explicit retry logic, exception handling, or libraries like
.retry -
Assuming Strict Evaluation: List operations are lazy.
doesn't execute until values are forced.map
Tooling
| Tool | Purpose | Notes |
|---|---|---|
/ | Build tool | Project structure and dependencies |
| Compiler | Glasgow Haskell Compiler |
| REPL | Interactive development |
| Linter | Suggests improvements |
| Testing | BDD-style testing framework |
| Property testing | Equivalent to StreamData |
| Concurrency | Task-like async operations |
| STM | Transactional memory for concurrency |
| JSON | JSON encoding/decoding |
Examples
Example 1: Simple - Function with Pattern Matching
Before (Elixir):
defmodule Math do def factorial(0), do: 1 def factorial(n) when n > 0, do: n * factorial(n - 1) end result = Math.factorial(5) # 120
After (Haskell):
module Math where factorial :: Int -> Int factorial 0 = 1 factorial n | n > 0 = n * factorial (n - 1) -- Usage result = factorial 5 -- 120
Example 2: Medium - Result Types and Error Handling
Before (Elixir):
defmodule UserService do def create_user(email, age) do with {:ok, valid_email} <- validate_email(email), {:ok, valid_age} <- validate_age(age) do {:ok, %User{email: valid_email, age: valid_age}} end end defp validate_email(email) do if String.contains?(email, "@") do {:ok, email} else {:error, :invalid_email} end end defp validate_age(age) do if age >= 18 do {:ok, age} else {:error, :too_young} end end end
After (Haskell):
module UserService where import Data.Text (Text) import qualified Data.Text as T data User = User { userEmail :: Text , userAge :: Int } deriving (Show) data UserError = InvalidEmail | TooYoung deriving (Show) createUser :: Text -> Int -> Either UserError User createUser email age = do validEmail <- validateEmail email validAge <- validateAge age return $ User validEmail validAge validateEmail :: Text -> Either UserError Text validateEmail email | "@" `T.isInfixOf` email = Right email | otherwise = Left InvalidEmail validateAge :: Int -> Either UserError Int validateAge age | age >= 18 = Right age | otherwise = Left TooYoung
Example 3: Complex - GenServer to STM with Concurrent Access
Before (Elixir):
defmodule BankAccount do use GenServer # Client API def start_link(initial_balance) do GenServer.start_link(__MODULE__, initial_balance) end def deposit(pid, amount) do GenServer.call(pid, {:deposit, amount}) end def withdraw(pid, amount) do GenServer.call(pid, {:withdraw, amount}) end def balance(pid) do GenServer.call(pid, :balance) end # Server Callbacks def init(initial_balance), do: {:ok, initial_balance} def handle_call({:deposit, amount}, _from, balance) do new_balance = balance + amount {:reply, {:ok, new_balance}, new_balance} end def handle_call({:withdraw, amount}, _from, balance) do if balance >= amount do new_balance = balance - amount {:reply, {:ok, new_balance}, new_balance} else {:reply, {:error, :insufficient_funds}, balance} end end def handle_call(:balance, _from, balance) do {:reply, balance, balance} end end # Usage {:ok, account} = BankAccount.start_link(1000) {:ok, new_balance} = BankAccount.deposit(account, 500) {:ok, after_withdrawal} = BankAccount.withdraw(account, 200) balance = BankAccount.balance(account)
After (Haskell):
module BankAccount where import Control.Concurrent.STM import Control.Monad (when) type Balance = Int type Account = TVar Balance data BankError = InsufficientFunds deriving (Show, Eq) createAccount :: Balance -> IO Account createAccount initial = newTVarIO initial deposit :: Account -> Balance -> IO Balance deposit account amount = atomically $ do current <- readTVar account let newBalance = current + amount writeTVar account newBalance return newBalance withdraw :: Account -> Balance -> IO (Either BankError Balance) withdraw account amount = atomically $ do current <- readTVar account if current >= amount then do let newBalance = current - amount writeTVar account newBalance return $ Right newBalance else return $ Left InsufficientFunds getBalance :: Account -> IO Balance getBalance = readTVarIO -- Atomic transfer between accounts transfer :: Account -> Account -> Balance -> STM (Either BankError ()) transfer from to amount = do fromBalance <- readTVar from if fromBalance >= amount then do modifyTVar from (subtract amount) modifyTVar to (+ amount) return $ Right () else return $ Left InsufficientFunds -- Usage main :: IO () main = do account <- createAccount 1000 newBalance <- deposit account 500 withdrawResult <- withdraw account 200 balance <- getBalance account print balance -- 1300 -- Multiple accounts with atomic transfer account1 <- createAccount 1000 account2 <- createAccount 0 result <- atomically $ transfer account1 account2 500 case result of Right _ -> putStrLn "Transfer successful" Left InsufficientFunds -> putStrLn "Insufficient funds"
See Also
For more examples and patterns, see:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Similar dynamic→static, practical→pure transitionconvert-clojure-haskell
- BEAM→native, actors→STMconvert-erlang-haskell
- Elixir development patternslang-elixir-dev
- Haskell development patternslang-haskell-dev
Cross-cutting pattern skills:
- Actors, STM, async patterns across languagespatterns-concurrency-dev
- JSON, validation across languagespatterns-serialization-dev
- Macros (Elixir) vs Template Haskellpatterns-metaprogramming-dev