Agents convert-erlang-haskell
Translates Erlang concurrent functional code to Haskell pure functional code. Use when migrating BEAM-based systems, modernizing telecom infrastructure, or adopting stronger type systems. Extends meta-convert-dev with Erlang-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-erlang-haskell" ~/.claude/skills/arustydev-agents-convert-erlang-haskell && rm -rf "$T"
content/skills/convert-erlang-haskell/SKILL.mdErlang ↔ Haskell Conversion
Bidirectional conversion between Erlang and Haskell. This skill extends
meta-convert-dev with Erlang↔Haskell specific type mappings, idiom translations, and concurrency patterns for translating between these functional languages with fundamentally different type systems and runtime models.
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: Erlang dynamic types → Haskell static types
- Idiom translations: Erlang patterns → idiomatic Haskell
- Concurrency models: OTP behaviors → STM/async patterns
- Error handling: let-it-crash → Maybe/Either types
- Message passing: Process mailboxes → Channels/TQueue
- Supervision: Supervisor trees → immortal/distributed-process
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Erlang language fundamentals - see
lang-erlang-dev - Haskell language fundamentals - see
lang-haskell-dev - Elixir conversions - see
convert-elixir-haskell
Quick Reference
| Erlang | Haskell | Notes |
|---|---|---|
| or custom sum type | Atoms → Text or algebraic types |
| or | Unbounded vs. bounded |
| or | Precision choice |
| | or |
| | Homogeneous lists |
| or custom product type | Fixed-size tuples or records |
| | |
| or | Process identifiers |
| or | Mutable references |
| | First-class functions |
| or | Error handling |
Type System Mapping
Primitives
| Erlang | Haskell | Example Conversion |
|---|---|---|
| | Integer literals |
| | Floating point |
| | Boolean |
| | Absence of value |
| | Binary strings |
| or ADT | Symbolic constants |
Collections
| Erlang | Haskell | Notes |
|---|---|---|
| | Linked lists |
| | Success tuple |
| | Key-value maps |
| | List pattern matching |
Composite Types
| Erlang | Haskell | Example |
|---|---|---|
| | Records |
| | Sum types |
| | Function signatures |
| | Opaque types |
Process Types
| Erlang Concept | Haskell Equivalent | Library |
|---|---|---|
| | package |
| Custom type class + | , |
| | , |
| Message passing | , , | , |
Idiom Translation
Pattern: Pattern Matching
Erlang:
handle_result(ok) -> success; handle_result({error, Reason}) -> {failure, Reason}; handle_result(Other) -> {unknown, Other}.
Haskell:
data Result = Ok | Error String data Response = Success | Failure String | Unknown String handleResult :: Result -> Response handleResult Ok = Success handleResult (Error reason) = Failure reason
Why this translation: Haskell's pattern matching is structurally similar but requires explicit type constructors in algebraic data types.
Pattern: Message Passing (gen_server)
Erlang:
-module(counter). -behaviour(gen_server). handle_call(increment, _From, State) -> {reply, ok, State + 1}; handle_call(get, _From, State) -> {reply, State, State}.
Haskell:
module Counter where import Control.Concurrent.STM data CounterMsg = Increment | Get (TMVar Int) counter :: TVar Int -> CounterMsg -> STM () counter state Increment = modifyTVar' state (+1) counter state (Get reply) = readTVar state >>= putTMVar reply
Why this translation: Haskell uses Software Transactional Memory (STM) instead of actor message passing.
TVar provides lock-free mutable state.
Pattern: Supervision Trees
Erlang:
init([]) -> Children = [ {worker1, {worker, start_link, []}, permanent, 5000, worker, [worker]} ], {ok, {{one_for_one, 5, 10}, Children}}.
Haskell:
import Control.Immortal runSupervised :: IO () runSupervised = do thread <- createWithLabel "worker1" $ \_ -> worker onUnexpectedFinish thread $ \_ -> print "Worker crashed, restarting"
Why this translation:
immortal library provides automatic restart semantics. For full OTP-style supervision, use distributed-process or cloud-haskell.
Pattern: Spawn and Message Send
Erlang:
Pid = spawn(fun() -> loop() end), Pid ! {hello, self()}, receive {reply, Msg} -> io:format("Got: ~p~n", [Msg]) end.
Haskell:
import Control.Concurrent import Control.Concurrent.Chan spawnWorker :: IO () spawnWorker = do chan <- newChan replyChan <- newChan _ <- forkIO $ worker chan writeChan chan (Hello, replyChan) reply <- readChan replyChan putStrLn $ "Got: " ++ show reply
Why this translation: Haskell's
Chan provides FIFO message queues. Use async for lightweight process management.
Pattern: List Comprehensions
Erlang:
Doubles = [X*2 || X <- [1,2,3,4], X rem 2 =:= 0].
Haskell:
doubles :: [Int] doubles = [x*2 | x <- [1,2,3,4], even x]
Why this translation: Syntax is nearly identical. Haskell's comprehensions support multiple generators and guards.
Pattern: Error Handling
Erlang:
case file:read_file("data.txt") of {ok, Data} -> process(Data); {error, Reason} -> handle_error(Reason) end.
Haskell:
import qualified Data.ByteString as BS import Control.Exception readAndProcess :: IO () readAndProcess = do result <- try (BS.readFile "data.txt") :: IO (Either IOError BS.ByteString) case result of Right dat -> process dat Left err -> handleError err
Why this translation: Haskell uses exception handling via
try/catch or the Either monad. For pure code, prefer ExceptT transformer.
Pattern: Higher-Order Functions
Erlang:
lists:map(fun(X) -> X * 2 end, [1,2,3]). lists:foldl(fun(X, Acc) -> X + Acc end, 0, [1,2,3]).
Haskell:
map (*2) [1,2,3] foldl (+) 0 [1,2,3]
Why this translation: Haskell's curried functions eliminate need for explicit lambdas. Point-free style is idiomatic.
Pattern: Binary Pattern Matching
Erlang:
<<Version:8, Type:8, Payload/binary>> = Packet.
Haskell:
import qualified Data.Binary.Get as Get import Data.ByteString.Lazy (ByteString) import Data.Word (Word8) parsePacket :: ByteString -> (Word8, Word8, ByteString) parsePacket = Get.runGet $ do version <- Get.getWord8 typ <- Get.getWord8 payload <- Get.getRemainingLazyByteString return (version, typ, payload)
Why this translation: Haskell's
binary package provides parsing combinators. cereal and attoparsec are alternatives.
Pattern: Guards
Erlang:
abs(X) when X < 0 -> -X; abs(X) -> X.
Haskell:
abs' :: (Ord a, Num a) => a -> a abs' x | x < 0 = -x | otherwise = x
Why this translation: Syntax nearly identical. Haskell guards use
| instead of when.
Pattern: ETS Tables
Erlang:
Table = ets:new(cache, [set, public]), ets:insert(Table, {key, value}), [{key, Value}] = ets:lookup(Table, key).
Haskell:
import qualified Data.HashTable.IO as HT type HashTable k v = HT.BasicHashTable k v useCache :: IO () useCache = do table <- HT.new :: IO (HashTable String String) HT.insert table "key" "value" value <- HT.lookup table "key" print value
Why this translation: Mutable hash tables via
hashtables package. For pure code, use Data.Map.
Error Handling
Philosophy Shift
Erlang: "Let it crash" - processes fail, supervisors restart them. Haskell: "Make illegal states unrepresentable" - type system prevents errors at compile time.
Practical Translation
Erlang:
-spec divide(number(), number()) -> {ok, number()} | {error, divide_by_zero}. divide(_, 0) -> {error, divide_by_zero}; divide(X, Y) -> {ok, X / Y}.
Haskell:
data DivideError = DivideByZero divide :: Double -> Double -> Either DivideError Double divide _ 0 = Left DivideByZero divide x y = Right (x / y) -- Or using Maybe for simpler errors divide' :: Double -> Double -> Maybe Double divide' _ 0 = Nothing divide' x y = Just (x / y)
Exception Handling
Erlang:
try risky_operation() catch error:Reason -> {error, Reason} end.
Haskell:
import Control.Exception safeRisky :: IO (Either SomeException Result) safeRisky = try riskyOperation
Concurrency Patterns
1. Lightweight Processes
Erlang:
spawn(fun worker/0)
Haskell:
import Control.Concurrent.Async async worker -- Returns Async a
Pattern: Use
async for fire-and-forget, race for first-to-finish, concurrently for parallel composition.
2. Message Channels
Erlang:
Pid ! Message, receive Pattern -> handle(Pattern) end.
Haskell:
import Control.Concurrent.Chan writeChan chan message msg <- readChan chan
Alternatives:
(STM-based, composable)TQueue
(high-performance bounded channels)unagi-chan
3. Select-Style Multiplexing
Erlang:
receive {msg1, Data} -> handle_msg1(Data); {msg2, Data} -> handle_msg2(Data) after 1000 -> timeout end.
Haskell:
import Control.Concurrent.STM selectMessage :: TQueue Msg1 -> TQueue Msg2 -> IO Response selectMessage q1 q2 = atomically $ (handleMsg1 <$> readTQueue q1) `orElse` (handleMsg2 <$> readTQueue q2)
Pattern:
orElse provides non-deterministic choice. For timeouts, use registerDelay.
4. GenServer Equivalent
Erlang:
-module(kv_store). -behaviour(gen_server). -export([start_link/0, put/2, get/1]). -export([init/1, handle_call/3, handle_cast/2]). start_link() -> gen_server:start_link(?MODULE, [], []). init([]) -> {ok, #{}}. put(Pid, Key, Value) -> gen_server:cast(Pid, {put, Key, Value}). get(Pid, Key) -> gen_server:call(Pid, {get, Key}). handle_cast({put, Key, Value}, State) -> {noreply, State#{Key => Value}}. handle_call({get, Key}, _From, State) -> {reply, maps:get(Key, State, undefined), State}.
Haskell:
module KVStore where import Control.Concurrent.STM import qualified Data.Map.Strict as Map data KVStore k v = KVStore (TVar (Map.Map k v)) newKVStore :: STM (KVStore k v) newKVStore = KVStore <$> newTVar Map.empty put :: Ord k => KVStore k v -> k -> v -> STM () put (KVStore store) key value = modifyTVar' store (Map.insert key value) get :: Ord k => KVStore k v -> k -> STM (Maybe v) get (KVStore store) key = Map.lookup key <$> readTVar store -- Usage: -- store <- atomically newKVStore -- atomically $ put store "key" "value" -- value <- atomically $ get store "key"
5. Distributed Computing
Erlang:
{server, 'node@host'} ! Message.
Haskell (Cloud Haskell):
import Control.Distributed.Process send serverId message
Libraries:
: Erlang-style distributed computingdistributed-process
: Network backendnetwork-transport-tcp
: Full frameworkcloud-haskell
Memory & Ownership
Garbage Collection
Both Erlang and Haskell use GC, but differently:
| Aspect | Erlang | Haskell |
|---|---|---|
| GC Strategy | Per-process generational | Generational for whole heap |
| Latency | Microsecond pauses per process | Millisecond pauses (tunable) |
| Memory Model | Process-local heaps | Shared heap with immutability |
| Tuning | flags | GHC RTS options (, ) |
Mutable State
Erlang:
% Processes hold mutable state via recursion loop(State) -> receive {update, NewState} -> loop(NewState) end.
Haskell:
-- Explicit mutability via IORef or TVar import Data.IORef updateState :: IORef Int -> IO () updateState ref = modifyIORef' ref (+1)
Philosophy: Haskell makes mutation explicit in types (
IO, STM). Pure functions remain referentially transparent.
Common Pitfalls
1. Forgetting Type Signatures
Problem: Haskell infers types, but polymorphism can cause ambiguity.
Solution: Always write top-level type signatures.
-- Bad: Type defaulting may surprise you divide x y = x / y -- Good: Explicit constraints divide :: Double -> Double -> Double divide x y = x / y
2. Overusing Exceptions in Pure Code
Problem:
error, undefined break referential transparency.
Solution: Use
Maybe, Either, or ExceptT.
-- Bad: Exception in pure code head' [] = error "Empty list" -- Good: Explicit failure head' :: [a] -> Maybe a head' [] = Nothing head' (x:_) = Just x
3. Ignoring Laziness
Problem: Erlang is strict; Haskell is lazy. Space leaks possible.
Solution: Use strict data structures and functions when needed.
import qualified Data.Map.Strict as Map -- Not Data.Map import Data.List (foldl') -- Not foldl sum' :: [Int] -> Int sum' = foldl' (+) 0 -- Strict accumulator
4. Blocking in STM
Problem:
atomically blocks can cause deadlocks if misused.
Solution: Keep STM transactions short and pure.
-- Bad: IO inside STM (won't compile) atomically $ do x <- readTVar var putStrLn "Debug" -- ERROR! -- Good: IO outside STM x <- atomically $ readTVar var putStrLn $ "Value: " ++ show x
5. Misunderstanding Monads
Problem: Treating
IO like synchronous Erlang code.
Solution: Embrace do-notation and functors.
-- Haskell idiomatic contents <- readFile "file.txt" process contents
6. Not Using Newtype
Problem: Primitive obsession (using
String, Int everywhere).
Solution: Wrap primitives for type safety.
-- Bad type UserId = Int -- Good newtype UserId = UserId Int
7. Channel Deadlocks
Problem: Mixing
Chan with synchronous expectations.
Solution: Use
async for structured concurrency.
-- Prefer this over manual channel management result <- async computation wait result
8. Forgetting Stack/Cabal Configuration
Problem: Erlang's rebar3 manages deps; Haskell needs explicit config.
Solution: Use Stack or Cabal with curated package sets (Stackage).
# stack.yaml resolver: lts-22.0 packages: - . extra-deps: []
Tooling
Ecosystem Equivalents
| Erlang Tool | Haskell Equivalent | Purpose |
|---|---|---|
| or | Build system |
| | Static analysis |
| or | Unit testing |
| + | Property testing |
| , | Profiling |
| | Runtime monitoring |
| + static binaries | Release management |
| / | Package registry |
Development Workflow
# Erlang rebar3 new app myapp rebar3 compile rebar3 shell # Haskell stack new myapp stack build stack ghci
Migration Strategy
Step 1: Identify OTP Boundaries
- Map
,gen_server
,gen_statem
to Haskell equivalentssupervisor - Document message protocols
Step 2: Translate Core Logic
- Start with pure functions (easiest to translate)
- Convert
to type signatures-spec - Port pattern matching and guards
Step 3: Replace Concurrency Primitives
→spawnasync
→receive
orChanSTM- Supervision →
orimmortaldistributed-process
Step 4: Handle Binary Protocols
- Use
,binary
, orcerealattoparsec - Preserve wire format compatibility if needed
Step 5: Testing
- Port EUnit tests to Hspec
- Use QuickCheck for property-based testing
- Add type-driven tests (e.g.,
)should-not-typecheck
Step 6: Performance Tuning
- Profile with
+RTS -p - Use strict data structures
- Consider
for forcing evaluationdeepseq
Step 7: Deployment
- Build static binaries with
stack --docker - Use multi-stage Docker builds
- Consider GHC runtime flags (
,-N
,-H
)-A
Examples
Example 1: Simple HTTP Client
Erlang:
-module(http_client). -export([fetch/1]). fetch(Url) -> inets:start(), case httpc:request(get, {Url, []}, [], []) of {ok, {{_, 200, _}, _, Body}} -> {ok, Body}; {ok, {{_, Code, _}, _, _}} -> {error, Code}; {error, Reason} -> {error, Reason} end.
Haskell:
module HttpClient where import Network.HTTP.Simple import qualified Data.ByteString.Lazy.Char8 as L8 fetch :: String -> IO (Either String String) fetch url = do response <- httpLBS (parseRequest_ url) let status = getResponseStatusCode response return $ if status == 200 then Right (L8.unpack $ getResponseBody response) else Left ("HTTP " ++ show status)
Example 2: Concurrent File Processing
Erlang:
process_files(Files) -> Parent = self(), [spawn(fun() -> {ok, Data} = file:read_file(F), Parent ! {done, F, process(Data)} end) || F <- Files], collect(length(Files), []). collect(0, Acc) -> Acc; collect(N, Acc) -> receive {done, File, Result} -> collect(N-1, [{File, Result}|Acc]) end.
Haskell:
import Control.Concurrent.Async import qualified Data.ByteString as BS processFiles :: [FilePath] -> IO [(FilePath, Result)] processFiles files = forConcurrently files $ \file -> do dat <- BS.readFile file let result = process dat return (file, result)
Example 3: GenServer-Style State Machine
Erlang:
-module(door). -behaviour(gen_statem). locked(cast, {button, Code}, #{code := Code} = Data) -> {next_state, unlocked, Data}; locked(cast, {button, _}, Data) -> {keep_state, Data}. unlocked(cast, lock, Data) -> {next_state, locked, Data}.
Haskell:
{-# LANGUAGE LambdaCase #-} module Door where import Control.Concurrent.STM data State = Locked | Unlocked data Event = Button Int | Lock doorFSM :: TVar State -> Int -> Event -> STM () doorFSM state correctCode = \case Button code | code == correctCode -> writeTVar state Unlocked Button _ -> return () Lock -> writeTVar state Locked
See Also
: Erlang development patterns and OTP designlang-erlang-dev
: Haskell idioms, type-level programming, monad transformerslang-haskell-dev
: General principles for language translationmeta-convert-dev
: Similar conversion for Elixir (BEAM) to Haskellconvert-elixir-haskell