返回 Skill 列表
extension
分类: 开发与工程无需 API Key

convert-elixir-haskell

Elixir和Haskell之间的双向转换。当在这两种语言之间迁移项目时使用。扩展了meta-convert-dev,增加了Elixir↔Haskell特定模式。在将Elixir项目迁移到Haskell、将BEAM actor模式翻译成使用IO Monad的纯函数式编程,或将动态OTP行为重构为静态类型等价物时使用。扩展了meta-convert-dev,增加了Elixir到Haskell特定模式。

person作者: jakexiaohubgithub

Elixir ↔ Haskell Conversion

Bidirectional conversion between Elixir and Haskell. This skill extends meta-convert-dev with Elixir↔Haskell specific type mappings, idiom translations, and transformation strategies for moving from BEAM's actor model to pure functional programming with strong static types.

This Skill Extends

  • 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: Elixir dynamic types → Haskell static types (Hindley-Milner)
  • Idiom translations: Actors/OTP → STM/async, pattern matching nuances, pipe vs composition
  • Error handling: Tagged tuples → Maybe/Either, supervision → explicit error handling
  • Async patterns: GenServer/Tasks → IO monad, async library, STM
  • Evaluation strategy: Strict (Elixir) → Lazy (Haskell) translation
  • Effects: Effects anywhere → IO monad boundary, pure core

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • Elixir language fundamentals - see lang-elixir-dev
  • Haskell language fundamentals - see lang-haskell-dev

Quick Reference

| Elixir | Haskell | Notes | |--------|---------|-------| | {:ok, value} | Right value | Either monad for results | | {:error, reason} | Left reason | Either monad for errors | | nil | Nothing | Maybe monad | | value | Just value | Maybe monad | | Enum.map/2 | fmap or map | Functor/list operations | | \|> | $ or & or . | Function application/composition | | def func(arg) | func :: Type -> Type<br>func arg = ... | Function with type signature | | GenServer | TVar + STM | Actor → transactional memory | | Task.async/1 | async | Concurrent execution | | receive do ... end | Pattern match on Chan | Message passing | | Map.t() | Map k v | Hash map | | %{key: value} | Map.fromList [("key", value)] | Map construction |


When Converting Code

  1. Analyze effects first - Identify where side effects occur in Elixir
  2. Map types explicitly - Create complete type mapping table from dynamic to static
  3. Separate pure from impure - Pure core with IO boundary
  4. Translate actors to alternatives - GenServer → STM, supervision → error handling
  5. Handle laziness - Elixir strict, Haskell lazy by default
  6. Test equivalence - Property-based testing for invariants

Type System Mapping

Primitive Types

| Elixir | Haskell | Notes | |--------|---------|-------| | integer() | Int / Integer | Int fixed-width, Integer arbitrary precision | | float() | Double | 64-bit float | | boolean() | Bool | True / False | | atom() | Custom ADT | :ok, :error → data constructors | | binary() / String.t() | Text / ByteString | Use Data.Text for UTF-8 | | charlist() | String | String is [Char] in Haskell | | pid() | ThreadId / Async | Process identifiers | | reference() | MVar / TVar | Reference types |

Collection Types

| Elixir | Haskell | Notes | |--------|---------|-------| | list() | [a] | Linked list | | tuple() | (a, b, ...) | Fixed-size tuple | | %{} (map) | Map k v | Requires Data.Map | | MapSet.t() | Set a | Requires Data.Set | | Keyword list | [(Text, a)] | List of pairs | | Range | [a..b] | List comprehension range |

Composite Types

| Elixir | Haskell | Notes | |--------|---------|-------| | Struct | data Type = Type { ... } | Record syntax | | {:ok, value} | Right value | Either String a | | {:error, reason} | Left reason | Either String a | | nil | Nothing | Maybe a | | Value | Just value | Maybe a | | Union types (spec) | data Type = A \| B | Sum type (ADT) | | GenServer state | TVar s | Shared mutable state | | Protocol | Type class | Polymorphism |

Function Types

| Elixir | Haskell | Notes | |--------|---------|-------| | (arg1, arg2 -> return) | arg1 -> arg2 -> return | Curried by default | | (() -> return) | IO return | Side-effecting function | | (a -> b) | a -> b | Pure function | | Anonymous fn | Lambda \x -> ... | Lambda syntax |


Idiom Translation

Pattern: Tagged Tuples → Either/Maybe

Elixir:

def divide(a, b) when b != 0, do: {:ok, a / b}
def divide(_, 0), do: {:error, :division_by_zero}

case divide(10, 2) do
  {:ok, result} -> IO.puts("Result: #{result}")
  {:error, reason} -> IO.puts("Error: #{reason}")
end

Haskell:

divide :: Float -> Float -> Either String Float
divide a 0 = Left "division by zero"
divide a b = Right (a / b)

case divide 10 2 of
  Right result -> putStrLn $ "Result: " ++ show result
  Left reason -> putStrLn $ "Error: " ++ reason

-- Or with do-notation (Either monad)
calculation :: Either String Float
calculation = do
  a <- divide 10 2
  b <- divide a 5
  return (b * 2)

Why this translation:

  • Elixir uses tagged tuples {:ok, value} / {:error, reason} idiomatically
  • Haskell's Either type encodes the same semantics with stronger type safety
  • Pattern matching works similarly in both
  • Haskell's Either monad allows chaining with do-notation

Pattern: Pipe Operator → Function Composition

Elixir:

result =
  [1, 2, 3, 4]
  |> Enum.filter(&(rem(&1, 2) == 0))
  |> Enum.map(&(&1 * 2))
  |> Enum.sum()

Haskell:

-- Point-free with composition
result = sum . map (*2) . filter even $ [1, 2, 3, 4]

-- Or with ($) for clarity
result = sum $ map (*2) $ filter even [1, 2, 3, 4]

-- Or with (&) for left-to-right (Data.Function)
import Data.Function ((&))

result = [1, 2, 3, 4]
       & filter even
       & map (*2)
       & sum

Why this translation:

  • Elixir's |> passes result forward (left-to-right)
  • Haskell's . composes right-to-left: (f . g) x = f (g x)
  • Use $ for right-to-left with clarity, or & for left-to-right
  • Point-free style is idiomatic Haskell

Pattern: Pattern Matching with Guards

Elixir:

def classify(n) when n < 0, do: :negative
def classify(0), do: :zero
def classify(n) when n < 10, do: :small
def classify(_), do: :large

Haskell:

-- Using guards
classify :: Int -> String
classify n
  | n < 0     = "negative"
  | n == 0    = "zero"
  | n < 10    = "small"
  | otherwise = "large"

-- Or with case
classify' :: Int -> String
classify' n = case n of
  0 -> "zero"
  _ | n < 0   -> "negative"
    | n < 10  -> "small"
    | otherwise -> "large"

Why this translation:

  • Both languages support guard clauses
  • Haskell uses | for guards instead of when
  • otherwise is the catch-all (equivalent to Elixir's _)
  • Pattern matching on literals comes before guards in Haskell

Pattern: Enum Comprehensions → List Comprehensions

Elixir:

result = for x <- [1, 2, 3, 4, 5],
             y <- [1, 2, 3],
             x * y > 5,
             do: {x, y}

Haskell:

result = [(x, y) | x <- [1..5], y <- [1..3], x * y > 5]

Why this translation:

  • Syntax is nearly identical
  • Haskell's list comprehensions are more concise
  • Filters come after generators in both
  • Multiple generators work the same way

Pattern: Recursive List Processing

Elixir:

def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)

def map([], _func), do: []
def map([head | tail], func), do: [func.(head) | map(tail, func)]

Haskell:

sum' :: [Int] -> Int
sum' [] = 0
sum' (x:xs) = x + sum' xs

map' :: (a -> b) -> [a] -> [b]
map' _ [] = []
map' f (x:xs) = f x : map' f xs

Why this translation:

  • Both use head/tail pattern matching ([head | tail] vs (x:xs))
  • Base case (empty list) first in both
  • Haskell requires type signatures (recommended in Elixir)
  • Haskell's cons operator : is infix

Pattern: With Statement → Do-Notation

Elixir:

def create_user(params) do
  with {:ok, validated} <- validate_params(params),
       {:ok, user} <- insert_user(validated),
       {:ok, email_sent} <- send_email(user) do
    {:ok, user}
  else
    {:error, reason} -> {:error, reason}
  end
end

Haskell:

createUser :: Params -> IO (Either String User)
createUser params = runExceptT $ do
  validated <- ExceptT $ return $ validateParams params
  user <- ExceptT $ insertUser validated
  emailSent <- ExceptT $ sendEmail user
  return user

-- Or with Either monad directly
createUser' :: Params -> Either String User
createUser' params = do
  validated <- validateParams params
  user <- insertUser validated
  emailSent <- sendEmail user
  return user

Why this translation:

  • Elixir's with chains operations that can fail
  • Haskell's do-notation for Either monad achieves the same
  • ExceptT transformer for mixing IO with Either
  • Short-circuits on first Left (error) automatically

Error Handling

Elixir Error Model → Haskell Error Model

| Elixir Pattern | Haskell Pattern | Notes | |----------------|-----------------|-------| | {:ok, value} | Right value | Success case | | {:error, reason} | Left reason | Error case | | nil | Nothing | Absence of value | | value | Just value | Presence of value | | raise Exception | error "message" | Runtime exception (avoid) | | Supervisor restart | Explicit error handling | No supervision trees | | try...rescue | catch / try | Exception handling (rare) |

Pattern: Supervision → Explicit Error Handling

Elixir:

# Supervisor restarts failed processes
defmodule MyApp.Supervisor do
  use Supervisor

  def start_link(_) do
    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def init(:ok) do
    children = [
      {Worker, []}
    ]
    Supervisor.init(children, strategy: :one_for_one)
  end
end

Haskell:

-- Explicit retry logic with error handling
import Control.Exception (try, SomeException)
import Control.Concurrent (threadDelay)

retryWithBackoff :: Int -> IO a -> IO (Either SomeException a)
retryWithBackoff 0 action = try action
retryWithBackoff n action = do
  result <- try action
  case result of
    Right val -> return $ Right val
    Left _ -> do
      threadDelay (1000000 * 2^(5-n))  -- Exponential backoff
      retryWithBackoff (n-1) action

-- Worker that can fail and be retried
worker :: IO ()
worker = do
  result <- retryWithBackoff 5 dangerousOperation
  case result of
    Right val -> processSuccess val
    Left err -> logError err

Why this translation:

  • Elixir: "Let it crash" philosophy with supervisor restart
  • Haskell: Explicit error handling with retry logic
  • No built-in supervision trees in Haskell
  • Must handle failures explicitly or use exception handling

Pattern: Result Propagation

Elixir:

def process_pipeline(input) do
  with {:ok, validated} <- validate(input),
       {:ok, transformed} <- transform(validated),
       {:ok, result} <- store(transformed) do
    {:ok, result}
  end
end

Haskell:

processPipeline :: Input -> Either String Result
processPipeline input = do
  validated <- validate input
  transformed <- transform validated
  result <- store transformed
  return result

-- Or with applicative for independent operations
processPipeline' input =
  validate input >>= transform >>= store

Why this translation:

  • Both short-circuit on first error
  • Haskell's Either monad provides same chaining
  • >>= (bind) chains dependent operations
  • More concise than nested case statements

Concurrency Patterns

Elixir Concurrency → Haskell Concurrency

| Elixir | Haskell | Notes | |--------|---------|-------| | Process (lightweight) | ThreadId | Haskell threads are OS threads | | spawn/1 | forkIO | Spawn concurrent thread | | Task.async/1 | async | Async computation | | Task.await/1 | wait | Wait for async result | | send/2 | writeChan | Send to channel | | receive do ... end | readChan | Receive from channel | | GenServer | TVar + STM | Stateful server | | Agent | MVar / TVar | Shared mutable state | | Supervisor | Manual retry logic | No built-in supervision |

Pattern: GenServer → STM

Elixir:

defmodule Counter do
  use GenServer

  def start_link(initial) do
    GenServer.start_link(__MODULE__, initial, name: __MODULE__)
  end

  def increment do
    GenServer.call(__MODULE__, :increment)
  end

  def get do
    GenServer.call(__MODULE__, :get)
  end

  # Callbacks
  def init(initial), do: {:ok, initial}

  def handle_call(:increment, _from, state) do
    {:reply, state + 1, state + 1}
  end

  def handle_call(:get, _from, state) do
    {:reply, state, state}
  end
end

Haskell:

import Control.Concurrent.STM

type Counter = TVar Int

createCounter :: Int -> IO Counter
createCounter initial = newTVarIO initial

increment :: Counter -> IO Int
increment counter = atomically $ do
  current <- readTVar counter
  let new = current + 1
  writeTVar counter new
  return new

getCount :: Counter -> IO Int
getCount counter = readTVarIO counter

-- Usage
main = do
  counter <- createCounter 0
  result1 <- increment counter
  result2 <- increment counter
  final <- getCount counter
  print final  -- 2

Why this translation:

  • GenServer: Message-passing actor with state
  • STM: Software Transactional Memory for safe concurrent mutations
  • Both provide atomicity and state isolation
  • STM is compositional (can combine transactions)
  • No message queues in STM (direct state access)

Pattern: Task.async → Async

Elixir:

task1 = Task.async(fn -> fetch_user(1) end)
task2 = Task.async(fn -> fetch_user(2) end)

user1 = Task.await(task1)
user2 = Task.await(task2)

Haskell:

import Control.Concurrent.Async

main = do
  task1 <- async $ fetchUser 1
  task2 <- async $ fetchUser 2

  user1 <- wait task1
  user2 <- wait task2

-- Or concurrently
main = do
  (user1, user2) <- concurrently (fetchUser 1) (fetchUser 2)

-- Map concurrently over list
users <- mapConcurrently fetchUser [1..10]

Why this translation:

  • Task.async spawns concurrent computation, returns handle
  • async library provides same semantics
  • wait blocks until result available
  • concurrently helper for pairs
  • Similar error propagation (async throws exceptions)

Pattern: Message Passing → Channels

Elixir:

pid = spawn(fn ->
  receive do
    {:msg, value} -> IO.puts("Received: #{value}")
  end
end)

send(pid, {:msg, "hello"})

Haskell:

import Control.Concurrent
import Control.Concurrent.Chan

main = do
  chan <- newChan

  forkIO $ do
    msg <- readChan chan
    putStrLn $ "Received: " ++ msg

  writeChan chan "hello"
  threadDelay 100000  -- Wait for thread

Why this translation:

  • Elixir: Process mailbox with pattern matching
  • Haskell: Typed channels (Chan a)
  • No pattern matching on messages (type-safe)
  • Must use explicit channel types
  • MVar for single-value handoff, Chan for queues

Evaluation Strategy Translation

Strict → Lazy Conversion Patterns

Elixir evaluates strictly (arguments evaluated before function call). Haskell evaluates lazily (arguments evaluated only when needed).

Elixir (strict):

# All elements processed immediately
list = Enum.map([1, 2, 3, 4, 5], fn x -> expensive_computation(x) end)
result = Enum.take(list, 2)  # But we only need 2!

Haskell (lazy):

-- Only first 2 elements computed
list = map expensiveComputation [1, 2, 3, 4, 5]
result = take 2 list  -- Lazy: only computes first 2

Key Differences:

| Aspect | Elixir (Strict) | Haskell (Lazy) | |--------|----------------|----------------| | Evaluation | Immediate | On-demand | | Infinite lists | Not possible | Natural | | Side effects | Predictable order | Deferred (use IO) | | Performance | Eager memory use | Space leaks possible |

Pattern: Forcing Strictness in Haskell

When you need strict evaluation:

-- Lazy fold can cause stack overflow
badSum = foldl (+) 0 [1..1000000]  -- Builds thunks

-- Strict fold
import Data.List (foldl')
goodSum = foldl' (+) 0 [1..1000000]  -- Forces evaluation

-- Bang patterns
{-# LANGUAGE BangPatterns #-}
strictFunc !x = x + 1  -- x evaluated immediately

Pattern: Streams in Elixir → Lazy Lists in Haskell

Elixir:

# Stream for lazy evaluation
Stream.iterate(0, &(&1 + 1))
|> Stream.map(&(&1 * 2))
|> Stream.filter(&(rem(&1, 2) == 0))
|> Enum.take(10)

Haskell:

-- Lists are lazy by default
result = take 10
       $ filter even
       $ map (*2)
       $ iterate (+1) 0

Why this translation:

  • Elixir: Explicit Stream for laziness
  • Haskell: All lists are lazy
  • Both use similar pipeline patterns
  • Haskell infinite lists are natural

Effects and IO Boundary

Separating Pure from Impure

Elixir (effects anywhere):

def process_user(id) do
  # Mix of pure and impure
  user = Repo.get(User, id)  # IO: Database
  name = String.upcase(user.name)  # Pure
  Logger.info("Processing #{name}")  # IO: Logging
  %{user | name: name}  # Pure
end

Haskell (pure core with IO boundary):

-- Pure functions
uppercaseName :: User -> User
uppercaseName user = user { userName = T.toUpper (userName user) }

-- IO boundary
processUser :: Int -> IO User
processUser userId = do
  user <- getUser userId  -- IO: Database
  let updated = uppercaseName user  -- Pure
  logInfo $ "Processing " <> userName updated  -- IO: Logging
  return updated

-- Type signature shows effects
-- :: Int -> User  (pure)
-- :: Int -> IO User  (has IO effects)

Why this translation:

  • Elixir: Effects can appear anywhere
  • Haskell: Type system tracks effects (IO type)
  • Pure functions don't use IO type
  • Easier to reason about effects in Haskell
  • Must explicitly lift pure values into IO with return

Pattern: Database Queries

Elixir (Ecto):

def get_active_users do
  from(u in User, where: u.active == true)
  |> Repo.all()
end

Haskell (persistent or esqueleto):

import Database.Persist
import Database.Persist.Sql

getActiveUsers :: SqlPersistM [Entity User]
getActiveUsers = selectList [UserActive ==. True] []

-- In IO context
main :: IO ()
main = runSqlite "database.db" $ do
  users <- getActiveUsers
  liftIO $ mapM_ print users

Why this translation:

  • Both use type-safe query builders
  • Haskell: Explicit monad for database operations
  • SqlPersistM is the DB monad
  • liftIO to perform IO in DB context

Common Pitfalls

  1. Forgetting Lazy Evaluation: Haskell lists are lazy. Use strict functions (foldl') when needed to avoid space leaks.

  2. Mixing IO and Pure: In Haskell, functions must declare IO in type signature. Can't perform IO in pure functions.

  3. Pattern Match Exhaustiveness: Haskell compiler warns about non-exhaustive patterns. Elixir allows partial patterns.

  4. Trying to Mutate State: No mutation in Haskell. Use STM/MVar for shared state or pass new state explicitly.

  5. Ignoring Type Inference Limitations: Haskell can't always infer types. Add explicit type signatures at module boundaries.

  6. Translating Supervision Literally: No supervision trees. Use explicit retry logic, exception handling, or libraries like retry.

  7. Assuming Strict Evaluation: List operations are lazy. map doesn't execute until values are forced.


Tooling

| Tool | Purpose | Notes | |------|---------|-------| | stack / cabal | Build tool | Project structure and dependencies | | ghc | Compiler | Glasgow Haskell Compiler | | ghci | REPL | Interactive development | | hlint | Linter | Suggests improvements | | hspec | Testing | BDD-style testing framework | | QuickCheck | Property testing | Equivalent to StreamData | | async | Concurrency | Task-like async operations | | stm | STM | Transactional memory for concurrency | | aeson | JSON | JSON encoding/decoding |


Examples

Example 1: Simple - Function with Pattern Matching

Before (Elixir):

defmodule Math do
  def factorial(0), do: 1
  def factorial(n) when n > 0, do: n * factorial(n - 1)
end

result = Math.factorial(5)  # 120

After (Haskell):

module Math where

factorial :: Int -> Int
factorial 0 = 1
factorial n | n > 0 = n * factorial (n - 1)

-- Usage
result = factorial 5  -- 120

Example 2: Medium - Result Types and Error Handling

Before (Elixir):

defmodule UserService do
  def create_user(email, age) do
    with {:ok, valid_email} <- validate_email(email),
         {:ok, valid_age} <- validate_age(age) do
      {:ok, %User{email: valid_email, age: valid_age}}
    end
  end

  defp validate_email(email) do
    if String.contains?(email, "@") do
      {:ok, email}
    else
      {:error, :invalid_email}
    end
  end

  defp validate_age(age) do
    if age >= 18 do
      {:ok, age}
    else
      {:error, :too_young}
    end
  end
end

After (Haskell):

module UserService where

import Data.Text (Text)
import qualified Data.Text as T

data User = User
  { userEmail :: Text
  , userAge :: Int
  } deriving (Show)

data UserError
  = InvalidEmail
  | TooYoung
  deriving (Show)

createUser :: Text -> Int -> Either UserError User
createUser email age = do
  validEmail <- validateEmail email
  validAge <- validateAge age
  return $ User validEmail validAge

validateEmail :: Text -> Either UserError Text
validateEmail email
  | "@" `T.isInfixOf` email = Right email
  | otherwise = Left InvalidEmail

validateAge :: Int -> Either UserError Int
validateAge age
  | age >= 18 = Right age
  | otherwise = Left TooYoung

Example 3: Complex - GenServer to STM with Concurrent Access

Before (Elixir):

defmodule BankAccount do
  use GenServer

  # Client API
  def start_link(initial_balance) do
    GenServer.start_link(__MODULE__, initial_balance)
  end

  def deposit(pid, amount) do
    GenServer.call(pid, {:deposit, amount})
  end

  def withdraw(pid, amount) do
    GenServer.call(pid, {:withdraw, amount})
  end

  def balance(pid) do
    GenServer.call(pid, :balance)
  end

  # Server Callbacks
  def init(initial_balance), do: {:ok, initial_balance}

  def handle_call({:deposit, amount}, _from, balance) do
    new_balance = balance + amount
    {:reply, {:ok, new_balance}, new_balance}
  end

  def handle_call({:withdraw, amount}, _from, balance) do
    if balance >= amount do
      new_balance = balance - amount
      {:reply, {:ok, new_balance}, new_balance}
    else
      {:reply, {:error, :insufficient_funds}, balance}
    end
  end

  def handle_call(:balance, _from, balance) do
    {:reply, balance, balance}
  end
end

# Usage
{:ok, account} = BankAccount.start_link(1000)
{:ok, new_balance} = BankAccount.deposit(account, 500)
{:ok, after_withdrawal} = BankAccount.withdraw(account, 200)
balance = BankAccount.balance(account)

After (Haskell):

module BankAccount where

import Control.Concurrent.STM
import Control.Monad (when)

type Balance = Int
type Account = TVar Balance

data BankError
  = InsufficientFunds
  deriving (Show, Eq)

createAccount :: Balance -> IO Account
createAccount initial = newTVarIO initial

deposit :: Account -> Balance -> IO Balance
deposit account amount = atomically $ do
  current <- readTVar account
  let newBalance = current + amount
  writeTVar account newBalance
  return newBalance

withdraw :: Account -> Balance -> IO (Either BankError Balance)
withdraw account amount = atomically $ do
  current <- readTVar account
  if current >= amount
    then do
      let newBalance = current - amount
      writeTVar account newBalance
      return $ Right newBalance
    else
      return $ Left InsufficientFunds

getBalance :: Account -> IO Balance
getBalance = readTVarIO

-- Atomic transfer between accounts
transfer :: Account -> Account -> Balance -> STM (Either BankError ())
transfer from to amount = do
  fromBalance <- readTVar from
  if fromBalance >= amount
    then do
      modifyTVar from (subtract amount)
      modifyTVar to (+ amount)
      return $ Right ()
    else
      return $ Left InsufficientFunds

-- Usage
main :: IO ()
main = do
  account <- createAccount 1000
  newBalance <- deposit account 500
  withdrawResult <- withdraw account 200
  balance <- getBalance account

  print balance  -- 1300

  -- Multiple accounts with atomic transfer
  account1 <- createAccount 1000
  account2 <- createAccount 0
  result <- atomically $ transfer account1 account2 500
  case result of
    Right _ -> putStrLn "Transfer successful"
    Left InsufficientFunds -> putStrLn "Insufficient funds"

See Also

For more examples and patterns, see:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • convert-clojure-haskell - Similar dynamic→static, practical→pure transition
  • convert-erlang-haskell - BEAM→native, actors→STM
  • lang-elixir-dev - Elixir development patterns
  • lang-haskell-dev - Haskell development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev - Actors, STM, async patterns across languages
  • patterns-serialization-dev - JSON, validation across languages
  • patterns-metaprogramming-dev - Macros (Elixir) vs Template Haskell