Claude Code Plugins

Community-maintained marketplace

Feedback

convert-haskell-elixir

@aRustyDev/ai
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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

  1. Analyze source thoroughly - Understand Haskell's lazy evaluation and purity before writing Elixir
  2. Map types first - Haskell's static types need runtime pattern matching in Elixir
  3. Preserve semantics over syntax similarity
  4. Embrace dynamic typing - Use pattern matching and guards for type safety
  5. Handle infinite structures - Lazy lists → Stream for infinite sequences
  6. Adapt IO monad - Pure/impure separation → pragmatic functional style
  7. 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(&divide(&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
  • with expression 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 Stream module 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 all
  • race → yield_many and take first result
  • mapConcurrently → 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/rescue only 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 → @moduledoc and @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)
  • shouldBeassert
  • propertycheck 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 examples
  • convert-clojure-elixir - Similar dynamic FP on JVM → BEAM
  • convert-erlang-elixir - Native BEAM language conversions
  • lang-haskell-dev - Haskell development patterns
  • lang-elixir-dev - Elixir development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev - STM, async, GenServer across languages
  • patterns-serialization-dev - Aeson, Jason, validation patterns
  • patterns-metaprogramming-dev - Template Haskell, Elixir macros