| name | convert-haskell-elixir |
| description | Convert Haskell code to idiomatic Elixir. Use when migrating Haskell projects to Elixir/BEAM, translating pure functional patterns to practical functional Elixir with OTP, or refactoring Haskell codebases for fault-tolerant distributed systems. Extends meta-convert-dev with Haskell-to-Elixir specific patterns covering static→dynamic types, lazy→strict evaluation, IO monad→effects, and type classes→protocols/behaviours. |
Convert Haskell to Elixir
Convert Haskell code to idiomatic Elixir. This skill extends meta-convert-dev with Haskell-to-Elixir specific type mappings, idiom translations, and patterns for migrating from pure lazy functional programming to practical strict functional programming on the BEAM.
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 types → Elixir types (static → dynamic with pattern matching)
- Idiom translations: Haskell patterns → idiomatic Elixir functional patterns
- Error handling: Maybe/Either/IO → with/case, {:ok, _}/{:error, _} tuples
- Lazy evaluation: Default lazy → explicit Stream, generators
- Concurrency: STM/async → GenServer, Task, Agent, OTP supervision
- Type classes: Functor/Monad/Applicative → protocols, behaviours
- IO monad: Pure/impure separation → pragmatic effects
- Purity philosophy: Haskell purity → Elixir practical FP
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - Haskell language fundamentals - see
lang-haskell-dev - Elixir language fundamentals - see
lang-elixir-dev - Reverse conversion (Elixir → Haskell) - see
convert-elixir-haskell - GHC-specific extensions (GADTs, Type Families) - no direct Elixir equivalent
Quick Reference
| Haskell | Elixir | Notes |
|---|---|---|
String |
String.t() or binary() |
UTF-8 binaries in Elixir |
Int |
integer() |
Arbitrary precision in Elixir |
Integer |
integer() |
Same representation |
Float/Double |
float() |
64-bit floats |
Bool |
boolean() |
true/false atoms |
Maybe a |
nil or {:ok, val} |
Pattern match on nil, use tuples for explicit optionality |
Either e a |
{:ok, val} / {:error, reason} |
Tagged tuples for error handling |
[a] |
[a] |
Lists (linked lists in both) |
(a, b) |
{a, b} |
Tuples |
Map k v |
%{key => value} |
Hash maps |
Set a |
MapSet.t() |
Set implementation |
data User = User Text Int |
defstruct name: "", age: 0 |
Structs for product types |
IO a |
Side effects allowed anywhere | No purity enforcement |
fmap / <$> |
Enum.map / Stream.map |
Functor-like operations |
>>= (bind) |
Pipe |> with pattern matching |
Monadic chaining |
class Typeclass |
defprotocol or @behaviour |
Polymorphism mechanisms |
When Converting Code
- Analyze source thoroughly - Understand Haskell's lazy evaluation and purity before writing Elixir
- Map types first - Haskell's static types need runtime pattern matching in Elixir
- Preserve semantics over syntax similarity
- Embrace dynamic typing - Use pattern matching and guards for type safety
- Handle infinite structures - Lazy lists → Stream for infinite sequences
- Adapt IO monad - Pure/impure separation → pragmatic functional style
- Test equivalence - Same inputs → same outputs, property-based tests
Type System Mapping
Primitive Types
| Haskell | Elixir | Notes |
|---|---|---|
Char |
Single-char string or integer | "a" or ?a (codepoint) |
String |
String.t() |
UTF-8 binary |
Int |
integer() |
Arbitrary precision |
Integer |
integer() |
Same as Int in Elixir |
Float |
float() |
64-bit IEEE 754 |
Double |
float() |
Same as Float in Elixir |
Bool |
boolean() |
:true / :false atoms |
() (unit) |
:ok or nil |
Unit type → atom |
Collection Types
| Haskell | Elixir | Notes |
|---|---|---|
[a] |
[a] |
Linked lists (lazy in Haskell, strict in Elixir) |
(a, b) |
{a, b} |
Tuples (strict in both) |
(a, b, c) |
{a, b, c} |
N-tuples |
Map k v |
%{k => v} |
Hash maps |
Set a |
MapSet.t(a) |
Set backed by map |
Array a |
No direct equivalent | Use :array module or lists |
Vector a |
No direct equivalent | Lists or tuples depending on use |
ByteString |
binary() |
Raw bytes |
Text |
String.t() |
UTF-8 text |
Composite Types (ADTs)
| Haskell | Elixir | Notes |
|---|---|---|
data Maybe a = Nothing | Just a |
nil or value, or {:ok, val} / nil |
Use pattern matching |
data Either e a = Left e | Right a |
{:error, reason} / {:ok, val} |
Tagged tuples |
data User = User Text Int |
defstruct name: "", age: 0 |
Structs for records |
newtype UserId = UserId Int |
Module-wrapped type or just use integer | No zero-cost newtypes |
| Sum types (enums) | Atoms or tagged tuples | data Color = Red | Green → :red, :green |
| Records with fields | Structs with named fields | %User{name: "Alice", age: 30} |
Type Classes → Protocols/Behaviours
| Haskell | Elixir | Notes |
|---|---|---|
class Eq a where (==) :: a -> a -> Bool |
Built-in == or defprotocol |
No explicit type classes |
class Ord a where compare :: a -> a -> Ordering |
Built-in comparison or protocol | Use guards for constraints |
class Show a where show :: a -> String |
String.Chars protocol |
to_string/1 |
class Functor f where fmap :: (a -> b) -> f a -> f b |
Enum module or Stream |
No Higher-Kinded Types |
class Applicative f |
No direct equivalent | Use pipelines and combinators |
class Monad m |
No direct equivalent | Use with, pipelines, GenServer for effects |
instance Show User |
defimpl String.Chars, for: User |
Protocol implementation |
Idiom Translation
Pattern 1: Maybe/Optional Handling
Haskell:
-- Using Maybe
findUser :: Int -> [User] -> Maybe User
findUser userId = find (\u -> userId == userId u)
-- Pattern matching
case findUser 1 users of
Nothing -> putStrLn "Not found"
Just user -> putStrLn $ "Found: " ++ name user
-- Using maybe function
maybe "default" name (findUser 1 users)
-- Functor operations
fmap name (findUser 1 users) -- Maybe String
Elixir:
# Using nil
def find_user(user_id, users) do
Enum.find(users, fn u -> u.id == user_id end)
end
# Pattern matching with case
case find_user(1, users) do
nil -> IO.puts("Not found")
user -> IO.puts("Found: #{user.name}")
end
# Using pattern matching with default
user = find_user(1, users) || %User{name: "default"}
# Or with explicit optional tuple
def find_user_safe(user_id, users) do
case Enum.find(users, fn u -> u.id == user_id end) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
# With pattern
case find_user_safe(1, users) do
{:ok, user} -> user.name
{:error, _} -> "default"
end
Why this translation:
- Haskell's Maybe makes optionality explicit in types
- Elixir uses nil for absent values, no compile-time checking
- For explicit error handling, use
{:ok, value}/{:error, reason}tuples - Pattern matching provides runtime safety in both languages
Pattern 2: Either/Error Handling
Haskell:
-- Either for error handling
divide :: Float -> Float -> Either String Float
divide _ 0 = Left "Division by zero"
divide x y = Right (x / y)
-- Pattern matching
case divide 10 2 of
Left err -> putStrLn $ "Error: " ++ err
Right result -> print result
-- Functor/Monad operations
fmap (*2) (divide 10 2) -- Either String Float
-- Chaining with bind
divide 10 2 >>= \x -> divide x 2 >>= \y -> Right (x + y)
-- Or with do-notation
calculation :: Either String Float
calculation = do
x <- divide 10 2
y <- divide x 2
return (x + y)
Elixir:
# Tagged tuples for error handling
def divide(_, 0), do: {:error, :division_by_zero}
def divide(x, y), do: {:ok, x / y}
# Pattern matching
case divide(10, 2) do
{:ok, result} -> IO.puts("Result: #{result}")
{:error, reason} -> IO.puts("Error: #{reason}")
end
# Transformation on success
case divide(10, 2) do
{:ok, value} -> {:ok, value * 2}
error -> error
end
# Chaining with 'with' (like do-notation)
def calculation do
with {:ok, x} <- divide(10, 2),
{:ok, y} <- divide(x, 2) do
{:ok, x + y}
else
{:error, reason} -> {:error, reason}
end
end
# Or using pipelines (assuming functions return tagged tuples)
def calculation_pipeline do
divide(10, 2)
|> bind(÷(&1, 2))
|> bind(&{:ok, &1 * 2})
end
defp bind({:ok, value}, func), do: func.(value)
defp bind(error, _), do: error
Why this translation:
- Haskell Either → Elixir
{:ok, value}/{:error, reason}tuples withexpression provides similar chaining to do-notation- Pattern matching on tuples replaces type-directed case analysis
- No automatic propagation—must handle errors explicitly
Pattern 3: Lazy Evaluation → Streams
Haskell:
-- Infinite lazy list
naturals :: [Integer]
naturals = [1..]
-- Take first 10
take 10 naturals -- [1,2,3,4,5,6,7,8,9,10]
-- Lazy Fibonacci
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
take 10 fibs -- [0,1,1,2,3,5,8,13,21,34]
-- Lazy operations (only evaluated as needed)
result :: Integer
result = head $ filter (> 1000) $ map (^2) naturals
-- Efficient: stops after finding first match
Elixir:
# Infinite stream
naturals = Stream.iterate(1, &(&1 + 1))
# Take first 10
Enum.take(naturals, 10) # [1,2,3,4,5,6,7,8,9,10]
# Lazy Fibonacci using Stream
defmodule Fib do
def stream do
Stream.unfold({0, 1}, fn {a, b} -> {a, {b, a + b}} end)
end
end
Enum.take(Fib.stream(), 10) # [0,1,1,2,3,5,8,13,21,34]
# Lazy operations (only evaluated when consumed)
result =
naturals
|> Stream.map(&(&1 * &1))
|> Stream.filter(&(&1 > 1000))
|> Enum.take(1)
|> hd()
# Efficient: stops after finding first match
Why this translation:
- Haskell lists are lazy by default; Elixir lists are strict
- Use
Streammodule for lazy operations in Elixir - Stream operations compose without intermediate lists
- Must call
Enum.*to materialize stream results
Pattern 4: Function Composition
Haskell:
-- Function composition (right to left)
processData :: String -> [String]
processData = sort . filter (not . null) . map trim . lines
-- Or with function application
processData' text = sort $ filter (not . null) $ map trim $ lines text
-- Point-free style
double :: Int -> Int
double = (*2)
addThenDouble :: Int -> Int
addThenDouble = (*2) . (+1)
Elixir:
# Pipe operator (left to right)
def process_data(text) do
text
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.filter(&(&1 != ""))
|> Enum.sort()
end
# Function capture
double = &(&1 * 2)
double.(5) # 10
# Composing with anonymous functions
add_then_double = fn x -> (x + 1) * 2 end
add_then_double.(5) # 12
# Or using pipelines
def add_then_double(x) do
x
|> Kernel.+(1)
|> Kernel.*(2)
end
Why this translation:
- Haskell
.composes right-to-left; Elixir|>pipes left-to-right - Elixir pipes are more readable for step-by-step transformations
- Function capture
&provides some point-free style - Elixir prefers named functions over anonymous composition
Pattern 5: Type Classes → Protocols
Haskell:
-- Define type class
class Serializable a where
serialize :: a -> String
deserialize :: String -> Maybe a
-- Implement for type
data User = User { name :: String, age :: Int }
instance Serializable User where
serialize (User n a) = n ++ "," ++ show a
deserialize str = case split ',' str of
[n, a] -> Just $ User n (read a)
_ -> Nothing
-- Use polymorphically
saveToFile :: Serializable a => a -> IO ()
saveToFile obj = writeFile "data.txt" (serialize obj)
Elixir:
# Define protocol
defprotocol Serializable do
@doc "Serialize value to string"
def serialize(value)
@doc "Deserialize string to value"
def deserialize(string)
end
# Implement for struct
defmodule User do
defstruct [:name, :age]
end
defimpl Serializable, for: User do
def serialize(%User{name: name, age: age}) do
"#{name},#{age}"
end
def deserialize(string) do
case String.split(string, ",") do
[name, age_str] ->
case Integer.parse(age_str) do
{age, _} -> {:ok, %User{name: name, age: age}}
:error -> {:error, :invalid_age}
end
_ ->
{:error, :invalid_format}
end
end
end
# Use with any type implementing protocol
def save_to_file(obj) do
File.write("data.txt", Serializable.serialize(obj))
end
Why this translation:
- Type classes → Protocols for polymorphism
- Haskell has compile-time dispatch; Elixir has runtime dispatch
- Protocols can be implemented for existing types (open extension)
- No type constraints at call site—runtime protocol check
Concurrency Patterns
Haskell STM → Elixir GenServer/Agent
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 -- Blocks until condition met
modifyTVar from (subtract amount)
modifyTVar to (+ amount)
main :: IO ()
main = do
account1 <- newTVarIO 1000
account2 <- newTVarIO 0
atomically $ transfer account1 account2 500
balances <- atomically $ do
b1 <- readTVar account1
b2 <- readTVar account2
return (b1, b2)
print balances -- (500, 500)
Elixir:
# Using Agent for simple state
defmodule Account do
use Agent
def start_link(initial_balance) do
Agent.start_link(fn -> initial_balance end)
end
def balance(pid) do
Agent.get(pid, & &1)
end
def transfer(from_pid, to_pid, amount) do
# Not atomic across agents - need GenServer or ETS for that
from_balance = Agent.get(from_pid, & &1)
if from_balance >= amount do
Agent.update(from_pid, &(&1 - amount))
Agent.update(to_pid, &(&1 + amount))
:ok
else
{:error, :insufficient_funds}
end
end
end
# Usage
{:ok, account1} = Account.start_link(1000)
{:ok, account2} = Account.start_link(0)
Account.transfer(account1, account2, 500)
{Account.balance(account1), Account.balance(account2)} # {500, 500}
# For true atomic transfers, use GenServer:
defmodule Bank do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def transfer(from_id, to_id, amount) do
GenServer.call(__MODULE__, {:transfer, from_id, to_id, amount})
end
@impl true
def init(_) do
{:ok, %{}}
end
@impl true
def handle_call({:transfer, from_id, to_id, amount}, _from, accounts) do
from_balance = Map.get(accounts, from_id, 0)
if from_balance >= amount do
accounts =
accounts
|> Map.update(from_id, 0, &(&1 - amount))
|> Map.update(to_id, 0, &(&1 + amount))
{:reply, :ok, accounts}
else
{:reply, {:error, :insufficient_funds}, accounts}
end
end
end
Why this translation:
- Haskell STM provides composable atomic transactions
- Elixir uses processes (Agent/GenServer) for state isolation
- Agent for simple get/update; GenServer for complex logic
- No built-in retry semantics—must implement manually
- OTP supervision provides fault tolerance STM doesn't
Haskell Async → Elixir Task
Haskell:
import Control.Concurrent.Async
main :: IO ()
main = do
-- Run two actions concurrently
(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)
-- Map concurrently
results <- mapConcurrently fetchUrl urls
Elixir:
# Run two actions concurrently
def main do
task1 = Task.async(fn -> fetch_url("http://example.com/1") end)
task2 = Task.async(fn -> fetch_url("http://example.com/2") end)
result1 = Task.await(task1)
result2 = Task.await(task2)
IO.inspect({result1, result2})
end
# Or using Task.async_stream for multiple URLs
def fetch_all(urls) do
urls
|> Task.async_stream(&fetch_url/1, max_concurrency: 10)
|> Enum.map(fn {:ok, result} -> result end)
end
# Race pattern (not built-in, use yield_many)
def race(func1, func2) do
task1 = Task.async(func1)
task2 = Task.async(func2)
case Task.yield_many([task1, task2], :infinity) do
[{task, {:ok, result}} | _] ->
# First one done
Task.shutdown(other_task(task, task1, task2))
result
_ ->
:no_result
end
end
Why this translation:
concurrently→ spawn multiple Tasks, await allrace→ yield_many and take first resultmapConcurrently→ Task.async_stream with max_concurrency- Task provides supervision tree integration
IO Monad → Pragmatic Effects
Haskell Pure/Impure Separation
Haskell:
-- Pure function
calculateTotal :: [Item] -> Double
calculateTotal items = sum $ map price items
-- Impure function (IO monad)
readConfig :: FilePath -> IO Config
readConfig path = do
contents <- readFile path
pure (parseConfig contents)
-- Cannot mix pure and IO without explicit lifting
main :: IO ()
main = do
config <- readConfig "config.json"
let total = calculateTotal (items config)
putStrLn $ "Total: " ++ show total
Elixir:
# Pure function (by convention)
def calculate_total(items) do
Enum.reduce(items, 0, fn item, acc -> acc + item.price end)
end
# Impure function (allowed anywhere, no marker)
def read_config(path) do
case File.read(path) do
{:ok, contents} -> parse_config(contents)
{:error, reason} -> {:error, reason}
end
end
# Mix freely
def main do
case read_config("config.json") do
{:ok, config} ->
total = calculate_total(config.items)
IO.puts("Total: #{total}")
{:error, reason} ->
IO.puts("Error: #{reason}")
end
end
Why this translation:
- Haskell enforces purity with IO type
- Elixir has no such enforcement—convention only
- Use naming conventions:
read_*,write_*,send_*for side effects - Pattern match on
{:ok, _}/{:error, _}for explicit error handling - Loss: No compile-time guarantee of purity
Error Handling Translation
Haskell Exceptions vs Elixir "Let It Crash"
Haskell:
import Control.Exception
-- Catching exceptions
readFileSafe :: FilePath -> IO (Maybe String)
readFileSafe path = do
result <- try (readFile path) :: IO (Either SomeException String)
case result of
Left _ -> return Nothing
Right contents -> return (Just contents)
-- Or with catch
readFileSafe' :: FilePath -> IO (Maybe String)
readFileSafe' path =
(Just <$> readFile path) `catch` \(_ :: SomeException) -> return Nothing
Elixir:
# Error tuples (preferred)
def read_file_safe(path) do
case File.read(path) do
{:ok, contents} -> {:ok, contents}
{:error, reason} -> {:error, reason}
end
end
# Try/rescue (avoid for control flow)
def read_file_with_rescue(path) do
try do
{:ok, File.read!(path)}
rescue
e in File.Error -> {:error, e.reason}
end
end
# "Let it crash" philosophy - don't catch, supervise
defmodule FileReader do
use GenServer
def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
end
def read(pid, path) do
GenServer.call(pid, {:read, path})
end
@impl true
def handle_call({:read, path}, _from, state) do
# If this crashes, supervisor restarts it
contents = File.read!(path)
{:reply, {:ok, contents}, state}
end
end
# Supervisor ensures restart on crash
children = [
{FileReader, []}
]
Supervisor.start_link(children, strategy: :one_for_one)
Why this translation:
- Haskell: Catch exceptions, handle explicitly
- Elixir: Let processes crash, supervisor restarts
- Use tagged tuples
{:ok, _}/{:error, _}for expected failures - Use
try/rescueonly for unexpected exceptions (rare) - Supervision trees provide fault isolation
Module System Translation
Haskell Modules
Haskell:
-- File: src/MyApp/User.hs
module MyApp.User
( User(..) -- Export type with all constructors
, createUser -- Export function
, validateEmail -- Export function
) where
import Data.Text (Text)
import qualified Data.Map as M
data User = User
{ name :: Text
, email :: Text
} deriving (Show, Eq)
createUser :: Text -> Text -> User
createUser n e = User n e
validateEmail :: Text -> Bool
validateEmail = undefined -- implementation
Elixir:
# File: lib/my_app/user.ex
defmodule MyApp.User do
@moduledoc """
User module for managing user data.
"""
defstruct [:name, :email]
@type t :: %__MODULE__{
name: String.t(),
email: String.t()
}
@doc """
Creates a new user.
"""
@spec create_user(String.t(), String.t()) :: t()
def create_user(name, email) do
%__MODULE__{name: name, email: email}
end
@doc """
Validates an email address.
"""
@spec validate_email(String.t()) :: boolean()
def validate_email(email) do
# implementation
String.contains?(email, "@")
end
# Private function (not exported)
defp internal_helper do
# ...
end
end
Why this translation:
- Haskell explicit exports → Elixir
def(public) /defp(private) - Type signatures →
@spec(optional, via Dialyzer) - Haskell type synonyms → Elixir
@type - Module documentation →
@moduledocand@doc
Build System Translation
Haskell (Cabal/Stack) → Elixir (Mix)
Haskell (package.yaml for Stack):
name: my-app
version: 0.1.0.0
dependencies:
- base >= 4.14 && < 5
- text
- aeson
- containers
library:
source-dirs: src
executables:
my-app:
main: Main.hs
source-dirs: app
dependencies:
- my-app
tests:
my-app-test:
main: Spec.hs
source-dirs: test
dependencies:
- my-app
- hspec
- QuickCheck
Elixir (mix.exs):
defmodule MyApp.MixProject do
use Mix.Project
def project do
[
app: :my_app,
version: "0.1.0",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
def application do
[
extra_applications: [:logger],
mod: {MyApp.Application, []}
]
end
defp deps do
[
{:jason, "~> 1.4"}, # Like aeson for JSON
{:plug_cowboy, "~> 2.6"} # HTTP server
]
end
end
Common commands:
| Haskell | Elixir | Purpose |
|---|---|---|
stack build |
mix compile |
Build project |
stack run |
mix run |
Run application |
stack test |
mix test |
Run tests |
stack ghci |
iex -S mix |
Interactive shell |
cabal install |
mix deps.get |
Get dependencies |
cabal repl |
iex -S mix |
REPL with project |
Testing Translation
Haskell HSpec/QuickCheck → Elixir ExUnit/StreamData
Haskell (HSpec + QuickCheck):
module MyApp.UserSpec where
import Test.Hspec
import Test.QuickCheck
import MyApp.User
spec :: Spec
spec = do
describe "createUser" $ do
it "creates a user with given name" $ do
let user = createUser "Alice" "alice@example.com"
name user `shouldBe` "Alice"
describe "validateEmail" $ do
it "returns True for valid email" $ do
validateEmail "user@example.com" `shouldBe` True
it "returns False for invalid email" $ do
validateEmail "invalid" `shouldBe` False
describe "property: reversing twice" $ do
it "returns original" $ property $ \xs ->
reverse (reverse xs) == (xs :: [Int])
Elixir (ExUnit + StreamData):
defmodule MyApp.UserTest do
use ExUnit.Case
use ExUnitProperties
alias MyApp.User
describe "create_user/2" do
test "creates a user with given name" do
user = User.create_user("Alice", "alice@example.com")
assert user.name == "Alice"
end
end
describe "validate_email/1" do
test "returns true for valid email" do
assert User.validate_email("user@example.com")
end
test "returns false for invalid email" do
refute User.validate_email("invalid")
end
end
describe "property: reversing twice" do
property "returns original" do
check all list <- list_of(integer()) do
assert Enum.reverse(Enum.reverse(list)) == list
end
end
end
end
Why this translation:
- HSpec → ExUnit (both BDD-style)
- QuickCheck → StreamData (property-based testing)
shouldBe→assertproperty→check all
Common Pitfalls
1. Expecting Compile-Time Type Safety
Problem: Elixir won't catch type errors at compile time
# No compile error, but runtime crash
def add_numbers(a, b), do: a + b
add_numbers(1, "hello") # Runtime error: ArithmeticError
Fix: Use guards, pattern matching, and Dialyzer specs
@spec add_numbers(number(), number()) :: number()
def add_numbers(a, b) when is_number(a) and is_number(b) do
a + b
end
# Dialyzer will warn if called incorrectly in other modules
2. Forgetting Lazy → Strict Evaluation
Problem: Infinite lists won't work without Stream
# This will never finish!
naturals = Enum.map(1..999_999_999_999, & &1)
Fix: Use Stream for lazy evaluation
naturals = Stream.iterate(1, &(&1 + 1))
Enum.take(naturals, 10) # Only computes first 10
3. Missing IO Monad Discipline
Problem: No compile-time separation of pure/impure
# Looks pure, but has side effect
def calculate(x) do
IO.puts("Calculating...") # Side effect!
x * 2
end
Fix: Use naming conventions and separate concerns
# Pure function
def calculate(x), do: x * 2
# Separate logging
def calculate_with_logging(x) do
IO.puts("Calculating...")
calculate(x)
end
4. Not Leveraging OTP for Concurrency
Problem: Trying to replicate STM with manual locking
# Bad: complex manual state management
def transfer(from, to, amount) do
# Race conditions, no supervision
end
Fix: Use GenServer, Agent, or supervised processes
# Good: OTP handles state and supervision
defmodule Bank do
use GenServer
# ... (see Concurrency section)
end
5. Ignoring "Let It Crash" Philosophy
Problem: Catching every error
# Overly defensive
def process(data) do
try do
parse(data)
rescue
_ -> {:error, :parse_error}
end
end
Fix: Let expected errors return tuples, unexpected errors crash
# Good: expected errors are tuples
def parse(data) do
case Jason.decode(data) do
{:ok, parsed} -> {:ok, parsed}
{:error, reason} -> {:error, reason}
end
end
# Supervise processes that might crash
children = [
{Worker, []}
]
Supervisor.start_link(children, strategy: :one_for_one)
Examples
Example 1: Simple - Maybe to Optional Tuple
Before (Haskell):
findUser :: Int -> [User] -> Maybe User
findUser userId = find (\u -> userId == userId u)
main :: IO ()
main = do
let users = [User "Alice" 1, User "Bob" 2]
case findUser 1 users of
Nothing -> putStrLn "User not found"
Just user -> putStrLn $ "Found: " ++ name user
After (Elixir):
def find_user(user_id, users) do
case Enum.find(users, fn u -> u.id == user_id end) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
def main do
users = [%User{name: "Alice", id: 1}, %User{name: "Bob", id: 2}]
case find_user(1, users) do
{:error, :not_found} -> IO.puts("User not found")
{:ok, user} -> IO.puts("Found: #{user.name}")
end
end
Example 2: Medium - Functor/Monad to Pipeline
Before (Haskell):
processUsers :: [User] -> IO [String]
processUsers users = do
let activeUsers = filter isActive users
let names = map name activeUsers
let uppercased = map (map toUpper) names
return $ sort uppercased
-- Or with function composition
processUsers' :: [User] -> IO [String]
processUsers' = return . sort . map (map toUpper) . map name . filter isActive
After (Elixir):
def process_users(users) do
users
|> Enum.filter(&is_active?/1)
|> Enum.map(& &1.name)
|> Enum.map(&String.upcase/1)
|> Enum.sort()
end
# Or more concise
def process_users(users) do
users
|> Enum.filter(&is_active?/1)
|> Enum.map(&(&1.name |> String.upcase()))
|> Enum.sort()
end
Example 3: Complex - STM Transaction to GenServer
Before (Haskell):
import Control.Concurrent.STM
import Control.Concurrent.Async
data BankState = BankState
{ accounts :: TVar (Map AccountId Int)
, transactions :: TVar [Transaction]
}
transfer :: BankState -> AccountId -> AccountId -> Int -> STM Bool
transfer state fromId toId amount = do
accts <- readTVar (accounts state)
txns <- readTVar (transactions state)
case (Map.lookup fromId accts, Map.lookup toId accts) of
(Just fromBal, Just toBal) | fromBal >= amount -> do
let accts' = Map.insert fromId (fromBal - amount) $
Map.insert toId (toBal + amount) accts
writeTVar (accounts state) accts'
writeTVar (transactions state) (Transaction fromId toId amount : txns)
return True
_ -> return False
main :: IO ()
main = do
state <- BankState <$> newTVarIO (Map.fromList [(1, 1000), (2, 500)])
<*> newTVarIO []
success <- atomically $ transfer state 1 2 300
if success
then putStrLn "Transfer successful"
else putStrLn "Transfer failed"
After (Elixir):
defmodule Bank do
use GenServer
# Client API
def start_link(initial_accounts) do
GenServer.start_link(__MODULE__, initial_accounts, name: __MODULE__)
end
def transfer(from_id, to_id, amount) do
GenServer.call(__MODULE__, {:transfer, from_id, to_id, amount})
end
def get_balance(account_id) do
GenServer.call(__MODULE__, {:get_balance, account_id})
end
# Server Callbacks
@impl true
def init(accounts) do
{:ok, %{accounts: accounts, transactions: []}}
end
@impl true
def handle_call({:transfer, from_id, to_id, amount}, _from, state) do
from_balance = Map.get(state.accounts, from_id, 0)
to_balance = Map.get(state.accounts, to_id, 0)
if from_balance >= amount do
accounts =
state.accounts
|> Map.put(from_id, from_balance - amount)
|> Map.put(to_id, to_balance + amount)
transaction = %{from: from_id, to: to_id, amount: amount}
transactions = [transaction | state.transactions]
{:reply, :ok, %{state | accounts: accounts, transactions: transactions}}
else
{:reply, {:error, :insufficient_funds}, state}
end
end
@impl true
def handle_call({:get_balance, account_id}, _from, state) do
balance = Map.get(state.accounts, account_id, 0)
{:reply, balance, state}
end
end
# Usage
defmodule Main do
def run do
{:ok, _pid} = Bank.start_link(%{1 => 1000, 2 => 500})
case Bank.transfer(1, 2, 300) do
:ok -> IO.puts("Transfer successful")
{:error, reason} -> IO.puts("Transfer failed: #{reason}")
end
IO.puts("Account 1: #{Bank.get_balance(1)}") # 700
IO.puts("Account 2: #{Bank.get_balance(2)}") # 800
end
end
See Also
For more examples and patterns, see:
meta-convert-dev- Foundational patterns with cross-language examplesconvert-clojure-elixir- Similar dynamic FP on JVM → BEAMconvert-erlang-elixir- Native BEAM language conversionslang-haskell-dev- Haskell development patternslang-elixir-dev- Elixir development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- STM, async, GenServer across languagespatterns-serialization-dev- Aeson, Jason, validation patternspatterns-metaprogramming-dev- Template Haskell, Elixir macros