Convert Erlang to Haskell
Convert Erlang code to idiomatic Haskell. This skill extends meta-convert-dev with Erlang-to-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
- •
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: 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 - •Reverse conversion (Haskell → Erlang) - see
convert-haskell-erlang - •Elixir conversions - see
convert-elixir-haskell
Quick Reference
| Erlang | Haskell | Notes |
|---|---|---|
atom() | Data.Text or custom sum type | Atoms → Text or algebraic types |
integer() | Integer or Int | Unbounded vs. bounded |
float() | Double or Float | Precision choice |
binary() | ByteString | Data.ByteString.Strict or .Lazy |
list() | [a] | Homogeneous lists |
tuple() | (a, b, ...) or custom product type | Fixed-size tuples or records |
map() | Map k v | Data.Map.Strict |
pid() | ThreadId or Async a | Process identifiers |
reference() | IORef or TVar | Mutable references |
fun() | a -> b | First-class functions |
ok | {error, Reason} | Either Error a or Maybe a | Error handling |
Type System Mapping
Primitives
| Erlang | Haskell | Example Conversion |
|---|---|---|
42 | 42 :: Integer | Integer literals |
3.14 | 3.14 :: Double | Floating point |
true | True :: Bool | Boolean |
undefined | Nothing :: Maybe a | Absence of value |
<<"hello">> | "hello" :: ByteString | Binary strings |
'atom' | "atom" :: Text or Atom ADT | Symbolic constants |
Collections
| Erlang | Haskell | Notes |
|---|---|---|
[1, 2, 3] | [1, 2, 3] :: [Int] | Linked lists |
{ok, Value} | Right Value :: Either Error a | Success tuple |
#{key => value} | Map.fromList [(key, value)] | Key-value maps |
[H|T] | (h:t) | List pattern matching |
Composite Types
| Erlang | Haskell | Example |
|---|---|---|
-record(user, {name, age}). | data User = User { userName :: Text, userAge :: Int } | Records |
-type result() :: ok | {error, term()}. | data Result = Ok | Error String | Sum types |
-spec add(integer(), integer()) -> integer(). | add :: Int -> Int -> Int | Function signatures |
-opaque handle(). | newtype Handle = Handle Int | Opaque types |
Process Types
| Erlang Concept | Haskell Equivalent | Library |
|---|---|---|
pid() | Async a | async package |
gen_server | Custom type class + TVar | stm, async |
supervisor | Supervisor | immortal, distributed-process |
| Message passing | Chan, TQueue, STM | stm, unagi-chan |
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:
- •
TQueue(STM-based, composable) - •
unagi-chan(high-performance bounded channels)
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:
- •
distributed-process: Erlang-style distributed computing - •
network-transport-tcp: Network backend - •
cloud-haskell: Full framework
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 | spawn_opt flags | GHC RTS options (-H, -A) |
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 |
|---|---|---|
rebar3 | stack or cabal | Build system |
dialyzer | ghc -Wall -Werror | Static analysis |
eunit | hspec or tasty | Unit testing |
common_test | hspec + QuickCheck | Property testing |
observer | threadscope, eventlog2html | Profiling |
recon | ekg | Runtime monitoring |
relx | docker + static binaries | Release management |
hex | hackage / stackage | 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,supervisorto Haskell equivalents - •Document message protocols
Step 2: Translate Core Logic
- •Start with pure functions (easiest to translate)
- •Convert
-specto type signatures - •Port pattern matching and guards
Step 3: Replace Concurrency Primitives
- •
spawn→async - •
receive→ChanorSTM - •Supervision →
immortalordistributed-process
Step 4: Handle Binary Protocols
- •Use
binary,cereal, orattoparsec - •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
deepseqfor forcing evaluation
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
- •
lang-erlang-dev: Erlang development patterns and OTP design - •
lang-haskell-dev: Haskell idioms, type-level programming, monad transformers - •
meta-convert-dev: General principles for language translation - •
convert-elixir-haskell: Similar conversion for Elixir (BEAM) to Haskell