Claude Code Plugins

Community-maintained marketplace

Feedback

convert-clojure-erlang

@aRustyDev/ai
1
0

Convert Clojure code to idiomatic Erlang. Use when migrating Clojure projects to Erlang, translating JVM-based functional code to BEAM, or refactoring Clojure codebases to leverage Erlang's fault tolerance and actor model. Extends meta-convert-dev with Clojure-to-Erlang specific patterns.

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-clojure-erlang
description Convert Clojure code to idiomatic Erlang. Use when migrating Clojure projects to Erlang, translating JVM-based functional code to BEAM, or refactoring Clojure codebases to leverage Erlang's fault tolerance and actor model. Extends meta-convert-dev with Clojure-to-Erlang specific patterns.

Convert Clojure to Erlang

Convert Clojure code to idiomatic Erlang, translating from JVM/Lisp-based functional programming to BEAM/Prolog-style functional programming with actors.

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: Clojure persistent data structures → Erlang immutable data
  • Idiom translations: Lisp-style functional → Prolog-style functional
  • Concurrency model: STM + core.async → Actor model (OTP processes)
  • Error handling: Exceptions → Let-it-crash philosophy
  • REPL workflow: REPL-driven development translation patterns (9th pillar)
  • Runtime platform: JVM → BEAM VM migration

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • Clojure language fundamentals - see lang-clojure-dev
  • Erlang language fundamentals - see lang-erlang-dev
  • Reverse conversion (Erlang → Clojure) - see convert-erlang-clojure
  • ClojureScript to Erlang - handle as two-step: ClojureScript → Clojure → Erlang

Quick Reference

Clojure Erlang Notes
(def x 42) X = 42. Variables are immutable in both
(defn f [x] body) f(X) -> Body. Function definition
{:key "value"} #{key => <<"value">>} Maps (keywords → atoms, strings → binaries)
[1 2 3] [1, 2, 3] Lists (vectors → lists)
#{:a :b} sets:from_list([a, b]) Sets
(atom 0) spawn(fun() -> loop(0) end) Mutable state → Process
(swap! a inc) Pid ! increment State update
@a Pid ! {self(), get}; receive {Pid, V} -> V end State read
(dosync ...) gen_server:call(...) Transaction → Synchronous call
(future ...) spawn(fun() -> ... end) Async execution
(promise) Message passing pattern Promise-like behavior

When Converting Code

  1. Analyze Clojure semantics - Understand STM, lazy sequences, JVM interop
  2. Map concurrency first - STM/refs/atoms/agents → gen_server/processes
  3. Preserve functional purity - Both are functional, maintain referential transparency
  4. Adopt Erlang idioms - Pattern matching, guards, let-it-crash
  5. Handle laziness explicitly - Clojure's lazy seqs → Erlang generators/streams
  6. Test equivalence - Property-based testing with PropEr matching test.check

Type System Mapping

Primitive Types

Clojure Erlang Notes
nil undefined or nil atom Erlang convention: undefined
true/false true/false Boolean atoms
42 42 Integers (arbitrary precision in Clojure → big integers in Erlang)
3.14 3.14 Floats
:keyword atom Keywords become atoms
"string" <<"binary">> or "list" Prefer binaries for efficiency
\c $c or <<"c">> Character → integer or binary
Symbol Atom Both are interned identifiers

Collection Types

Clojure Erlang Notes
[1 2 3] [1, 2, 3] Vectors → Lists (note: O(1) vs O(n) append)
'(1 2 3) [1, 2, 3] Lists → Lists (direct mapping)
{:a 1 :b 2} #{a => 1, b => 2} Maps → Maps
#{:a :b :c} sets:from_list([a, b, c]) or #{a, b, c} (MapSet pattern) Sets
Lazy seq Generator fun or lists:seq/2 Explicit laziness needed
(range) lists:seq(0, ...) or generator Infinite sequences require explicit handling
(iterate f x) Generator function Create custom generator

Composite Types

Clojure Erlang Notes
defrecord -record(...) or map with type tag Records or typed maps
deftype Module with constructor Encapsulation via module
defprotocol -behavior(...) callback module Polymorphism
Namespace Module Direct mapping
Var Process or ETS table For mutable global state

Idiom Translation

Pattern: Function Definition

Clojure:

(defn factorial
  "Calculates factorial of n"
  [n]
  (if (<= n 1)
    1
    (* n (factorial (dec n)))))

Erlang:

% Calculates factorial of n
factorial(N) when N =< 1 -> 1;
factorial(N) -> N * factorial(N - 1).

Why this translation:

  • Erlang uses guards (when) instead of explicit if
  • Pattern matching in function clauses is more idiomatic than conditionals
  • Multiple function clauses replace Clojure's single function with conditional body

Pattern: Map Operations

Clojure:

(defn update-user [user]
  (-> user
      (assoc :updated-at (System/currentTimeMillis))
      (update :age inc)
      (dissoc :temp-field)))

Erlang:

update_user(User) ->
    User1 = maps:put(updated_at, erlang:system_time(millisecond), User),
    User2 = maps:update_with(age, fun(Age) -> Age + 1 end, User1),
    maps:remove(temp_field, User2).

% Or with pattern matching
update_user(#{age := Age} = User) ->
    User#{
        updated_at => erlang:system_time(millisecond),
        age => Age + 1
    }.

Why this translation:

  • Threading macro -> becomes sequential variable binding
  • assocmaps:put or map update syntax
  • updatemaps:update_with or pattern match + update
  • dissocmaps:remove

Pattern: Collection Processing

Clojure:

(defn process-users [users]
  (->> users
       (filter :active)
       (map #(update % :name str/upper-case))
       (take 10)
       (group-by :department)))

Erlang:

process_users(Users) ->
    Active = lists:filter(fun(#{active := A}) -> A end, Users),
    Uppercased = lists:map(
        fun(#{name := Name} = U) ->
            U#{name => string:uppercase(Name)}
        end,
        Active
    ),
    TopTen = lists:sublist(Uppercased, 10),
    group_by(department, TopTen).

group_by(Key, List) ->
    lists:foldl(
        fun(#{Key := Val} = Item, Acc) ->
            maps:update_with(Val, fun(Items) -> [Item | Items] end, [Item], Acc)
        end,
        #{},
        List
    ).

Why this translation:

  • Threading last ->> becomes explicit function composition
  • filter, map, take map to lists:filter/2, lists:map/2, lists:sublist/2
  • group-by requires custom implementation (no stdlib equivalent)
  • Clojure's lazy evaluation becomes eager in Erlang

Pattern: Destructuring

Clojure:

(defn greet [{:keys [name age] :or {age 0}}]
  (str "Hello " name ", you are " age))

(let [[first second & rest] items]
  (process first second rest))

Erlang:

greet(#{name := Name, age := Age}) ->
    lists:flatten(io_lib:format("Hello ~s, you are ~p", [Name, Age]));
greet(#{name := Name}) ->
    greet(#{name => Name, age => 0}).

process_items([First, Second | Rest]) ->
    do_process(First, Second, Rest).

Why this translation:

  • Map destructuring in function arguments is similar
  • Default values require separate function clause
  • List destructuring [H|T] is similar to Clojure's [first & rest]

Pattern: Recursion and Accumulation

Clojure:

(defn sum [coll]
  (reduce + 0 coll))

(defn sum-recursive [coll]
  (loop [items coll acc 0]
    (if (empty? items)
      acc
      (recur (rest items) (+ acc (first items))))))

Erlang:

% Using fold
sum(Coll) ->
    lists:foldl(fun(X, Acc) -> X + Acc end, 0, Coll).

% Using tail recursion
sum_recursive(Coll) ->
    sum_recursive(Coll, 0).

sum_recursive([], Acc) -> Acc;
sum_recursive([H|T], Acc) ->
    sum_recursive(T, Acc + H).

Why this translation:

  • reducelists:foldl
  • loop/recur → tail-recursive function with accumulator
  • Both optimize tail calls, so performance is similar

Pattern: Lazy Sequences

Clojure:

(defn fibonacci []
  (map first (iterate (fn [[a b]] [b (+ a b)]) [0 1])))

(take 10 (fibonacci))

Erlang:

% Generator pattern
fibonacci() ->
    fun Loop(A, B) ->
        fun() -> {A, Loop(B, A + B)} end
    end(0, 1).

take(0, _Generator) -> [];
take(N, Generator) ->
    {Value, Next} = Generator(),
    [Value | take(N - 1, Next)].

% Usage
Fib = fibonacci(),
take(10, Fib).

Why this translation:

  • Clojure's lazy sequences don't have direct equivalent
  • Generator pattern (function returning function) provides laziness
  • Explicit take function to realize values
  • Erlang processes can also model infinite sequences

Concurrency Model Translation

STM (Refs) → gen_server

Clojure:

(def account-a (ref 100))
(def account-b (ref 200))

(defn transfer [from to amount]
  (dosync
    (alter from - amount)
    (alter to + amount)))

(transfer account-a account-b 50)

Erlang:

% Using gen_server for coordinated state
-module(account_server).
-behaviour(gen_server).

start_link(Initial) ->
    gen_server:start_link(?MODULE, Initial, []).

transfer(From, To, Amount) ->
    % Withdraw from source
    ok = gen_server:call(From, {withdraw, Amount}),
    try
        % Deposit to destination
        ok = gen_server:call(To, {deposit, Amount})
    catch
        error:Reason ->
            % Rollback on failure
            gen_server:call(From, {deposit, Amount}),
            {error, Reason}
    end.

handle_call({withdraw, Amount}, _From, Balance) when Balance >= Amount ->
    {reply, ok, Balance - Amount};
handle_call({withdraw, _Amount}, _From, Balance) ->
    {reply, {error, insufficient_funds}, Balance};
handle_call({deposit, Amount}, _From, Balance) ->
    {reply, ok, Balance + Amount}.

Why this translation:

  • STM transactions become explicit message passing
  • dosync coordination becomes manual two-phase approach or single coordinator
  • Erlang emphasizes explicit rollback vs automatic retry
  • Alternative: Use a single coordinator gen_server managing both accounts

Atoms → Process Mailbox

Clojure:

(def counter (atom 0))

(swap! counter inc)
(swap! counter + 5)
@counter  ; => 6

Erlang:

% Process-based counter
start_counter(Initial) ->
    spawn(fun() -> counter_loop(Initial) end).

counter_loop(Count) ->
    receive
        {From, increment} ->
            From ! {ok, Count + 1},
            counter_loop(Count + 1);
        {From, {add, N}} ->
            From ! {ok, Count + N},
            counter_loop(Count + N);
        {From, get} ->
            From ! {ok, Count},
            counter_loop(Count);
        stop ->
            ok
    end.

% Client functions
increment(Pid) ->
    Pid ! {self(), increment},
    receive {ok, NewValue} -> NewValue end.

add(Pid, N) ->
    Pid ! {self(), {add, N}},
    receive {ok, NewValue} -> NewValue end.

get_value(Pid) ->
    Pid ! {self(), get},
    receive {ok, Value} -> Value end.

Why this translation:

  • Atoms are lightweight in Clojure but require process in Erlang
  • Message passing replaces direct state mutation
  • swap! becomes send-receive pattern
  • @atom (deref) becomes query message

core.async Channels → Erlang Processes/Messages

Clojure:

(require '[clojure.core.async :as async])

(defn producer [ch]
  (async/go
    (dotimes [i 10]
      (async/>! ch i))
    (async/close! ch)))

(defn consumer [ch]
  (async/go-loop []
    (when-let [value (async/<! ch)]
      (println "Got:" value)
      (recur))))

(let [ch (async/chan 10)]
  (producer ch)
  (consumer ch))

Erlang:

producer(ConsumerPid) ->
    spawn(fun() ->
        lists:foreach(
            fun(I) ->
                ConsumerPid ! {value, I}
            end,
            lists:seq(0, 9)
        ),
        ConsumerPid ! done
    end).

consumer() ->
    spawn(fun() -> consumer_loop() end).

consumer_loop() ->
    receive
        {value, Value} ->
            io:format("Got: ~p~n", [Value]),
            consumer_loop();
        done ->
            ok
    end.

% Usage
Consumer = consumer(),
producer(Consumer).

Why this translation:

  • Channels become direct message passing between processes
  • go blocks become spawned processes
  • <! (take) becomes receive
  • >! (put) becomes ! (send)
  • Channel closing becomes sentinel message (done)

Error Handling Translation

Exceptions → Let-It-Crash

Clojure:

(defn process-data [data]
  (try
    (validate-data data)
    (transform-data data)
    (save-data data)
    (catch Exception e
      (log-error e)
      {:error (.getMessage e)})))

Erlang (Anti-pattern - too defensive):

% DON'T DO THIS - too defensive
process_data(Data) ->
    try
        validate_data(Data),
        Transformed = transform_data(Data),
        save_data(Transformed)
    catch
        error:Reason ->
            log_error(Reason),
            {error, Reason}
    end.

Erlang (Idiomatic - let it crash):

% DO THIS - let supervisor handle failures
process_data(Data) ->
    validate_data(Data),  % Crash if invalid
    Transformed = transform_data(Data),  % Crash if transform fails
    save_data(Transformed).  % Crash if save fails

% Supervisor will restart the worker on crash
init([]) ->
    SupFlags = #{
        strategy => one_for_one,
        intensity => 5,
        period => 60
    },
    ChildSpecs = [
        #{
            id => worker,
            start => {worker, start_link, []},
            restart => permanent,
            shutdown => 5000,
            type => worker
        }
    ],
    {ok, {SupFlags, ChildSpecs}}.

Why this translation:

  • Clojure's exceptions become crashes in Erlang
  • Try-catch only for expected errors, not exceptional conditions
  • Supervisors handle process failure and restart
  • Defensive programming discouraged in favor of supervision trees

Error Tuples Pattern

Clojure:

(defn find-user [id users]
  (if-let [user (first (filter #(= id (:id %)) users))]
    {:ok user}
    {:error :not-found}))

Erlang:

find_user(Id, Users) ->
    case lists:keyfind(Id, #user.id, Users) of
        {user, Id, _, _} = User ->
            {ok, User};
        false ->
            {error, not_found}
    end.

% Or with list comprehension
find_user(Id, Users) ->
    case [U || #{id := UserId} = U <- Users, UserId =:= Id] of
        [User | _] -> {ok, User};
        [] -> {error, not_found}
    end.

Why this translation:

  • Both use tagged tuples for error handling
  • Erlang's pattern matching makes error handling cleaner
  • {ok, Value} and {error, Reason} are Erlang conventions

REPL-Driven Development Translation

Both Clojure and Erlang are REPL-centric languages, making workflow translation smoother than compiled → REPL conversions.

Development Workflow Comparison

Clojure Workflow Erlang Equivalent Translation Notes
Start REPL (lein repl) Start shell (erl or rebar3 shell) Both provide interactive environments
Send form to REPL (editor integration) Compile module (c(module)) Erlang reloads entire modules
Hot reload namespace Hot code loading (c:l(module)) Erlang's hot swapping is production-ready
REPL-driven design REPL-driven design Both support incremental development
(doc fn) h(module) or module:module_info() Documentation lookup
(source fn) Decompile with erlang:load_module/2 Less common in Erlang

Interactive Development Example

Clojure:

;; In REPL
user=> (defn process-user [user]
         (-> user
             (update :name str/upper-case)
             (assoc :processed true)))

user=> (process-user {:name "alice" :age 30})
;; => {:name "ALICE", :age 30, :processed true}

;; Refine in REPL
user=> (defn process-user [user]
         (-> user
             (update :name str/upper-case)
             (assoc :processed-at (System/currentTimeMillis))))

;; Test immediately
user=> (process-user {:name "alice"})

Erlang:

% In shell
1> c(user_processor).
{ok,user_processor}

2> user_processor:process_user(#{name => "alice", age => 30}).
#{name => "ALICE", age => 30, processed => true}

% Edit user_processor.erl, then reload
3> c(user_processor).
{ok,user_processor}

4> user_processor:process_user(#{name => "alice"}).
#{name => "ALICE",
  processed_at => 1704067200000}

Translation notes:

  • Clojure allows redefining individual functions in REPL
  • Erlang recompiles entire modules
  • Both support rapid iteration and testing
  • Erlang's hot code loading works in production (Clojure requires restart)

REPL Testing Patterns

Clojure:

;; Quick test in REPL
(defn validate-email [email]
  (re-matches #".+@.+\..+" email))

;; Test interactively
user=> (validate-email "test@example.com")
;; => "test@example.com"

user=> (validate-email "invalid")
;; => nil

Erlang:

% Quick test in shell
validate_email(Email) ->
    case re:run(Email, ".+@.+\\..+") of
        {match, _} -> {ok, Email};
        nomatch -> {error, invalid_email}
    end.

% Test interactively
1> c(validator).
{ok,validator}

2> validator:validate_email(<<"test@example.com">>).
{ok,<<"test@example.com">>}

3> validator:validate_email(<<"invalid">>).
{error,invalid_email}

Module System Translation

Clojure:

(ns myapp.user-service
  "User service with CRUD operations"
  (:require [clojure.string :as str]
            [myapp.database :as db]
            [myapp.validation :refer [validate-email]]))

(defn create-user [user-data]
  (let [validated (validate-email (:email user-data))]
    (db/insert :users validated)))

Erlang:

-module(user_service).
-export([create_user/1]).

% No docstring at module level, use comments
% User service with CRUD operations

create_user(UserData) ->
    Email = maps:get(email, UserData),
    case validation:validate_email(Email) of
        {ok, ValidEmail} ->
            database:insert(users, ValidEmail);
        {error, Reason} ->
            {error, Reason}
    end.

Translation notes:

  • Namespaces → Modules (one-to-one mapping)
  • :require → module calls (always qualified)
  • :refer → direct function calls (no import mechanism, use full module name)
  • Private functions: prefix with internal convention or don't export

Macros and Metaprogramming

Clojure Macros → Erlang Parse Transforms

Clojure:

(defmacro unless [condition & body]
  `(if (not ~condition)
     (do ~@body)))

(unless false
  (println "This runs"))

Erlang:

% Parse transforms are more complex, typically use macros instead
-define(UNLESS(Cond, Body),
    case (not Cond) of
        true -> Body;
        false -> ok
    end).

% Usage
?UNLESS(false, io:format("This runs~n")).

% For true metaprogramming, use parse transforms (advanced)
% See lang-erlang-dev for parse transform examples

Translation notes:

  • Clojure macros are more powerful and easier to write
  • Erlang macros are textual substitution (C-style)
  • Complex metaprogramming requires parse transforms (AST manipulation)
  • Most Clojure macro use cases can be solved with higher-order functions in Erlang

Build System and Dependencies

project.clj → rebar.config

Clojure (Leiningen):

(defproject myapp "0.1.0-SNAPSHOT"
  :description "My Clojure application"
  :dependencies [[org.clojure/clojure "1.11.1"]
                 [ring/ring-core "1.10.0"]
                 [cheshire "5.12.0"]]
  :main myapp.core)

Erlang (Rebar3):

{erl_opts, [debug_info]}.

{deps, [
    {cowboy, "2.10.0"},  % Ring equivalent for Erlang
    {jsx, "3.1.0"}       % JSON library (Cheshire equivalent)
]}.

{relx, [
    {release, {myapp, "0.1.0"}, [
        myapp,
        sasl
    ]},
    {mode, dev}
]}.

deps.edn → rebar.config

Clojure (tools.deps):

{:deps {org.clojure/clojure {:mvn/version "1.11.1"}
        ring/ring-core {:mvn/version "1.10.0"}
        cheshire/cheshire {:mvn/version "5.12.0"}}}

Erlang (Rebar3):

{deps, [
    {cowboy, "2.10.0"},
    {jsx, "3.1.0"}
]}.

Testing Strategy Translation

test.check → PropEr

Clojure:

(require '[clojure.test.check :as tc]
         '[clojure.test.check.generators :as gen]
         '[clojure.test.check.properties :as prop])

(def prop-reverse
  (prop/for-all [v (gen/vector gen/small-integer)]
    (= v (reverse (reverse v)))))

(tc/quick-check 100 prop-reverse)

Erlang:

-include_lib("proper/include/proper.hrl").

prop_reverse() ->
    ?FORALL(List, list(integer()),
            lists:reverse(lists:reverse(List)) =:= List).

% Run
proper:quickcheck(prop_reverse(), [{numtests, 100}]).

clojure.test → EUnit

Clojure:

(ns myapp.core-test
  (:require [clojure.test :refer [deftest is testing]]
            [myapp.core :as core]))

(deftest addition-test
  (testing "Basic addition"
    (is (= 4 (core/add 2 2)))
    (is (= 0 (core/add -1 1)))))

Erlang:

-module(core_tests).
-include_lib("eunit/include/eunit.hrl").

addition_test() ->
    ?assertEqual(4, core:add(2, 2)),
    ?assertEqual(0, core:add(-1, 1)).

Common Patterns

Polymorphism: Protocols → Behaviors

Clojure:

(defprotocol Storage
  (save [this data])
  (load [this id]))

(defrecord FileStorage [path]
  Storage
  (save [this data] (spit (:path this) data))
  (load [this id] (slurp (str (:path this) "/" id))))

(defrecord DbStorage [conn]
  Storage
  (save [this data] (db/insert (:conn this) data))
  (load [this id] (db/query (:conn this) id)))

Erlang:

% Define behavior
-module(storage).
-callback save(State :: term(), Data :: term()) -> {ok, term()} | {error, term()}.
-callback load(State :: term(), Id :: term()) -> {ok, term()} | {error, term()}.

% File storage implementation
-module(file_storage).
-behaviour(storage).
-export([save/2, load/2]).

save(#{path := Path}, Data) ->
    file:write_file(Path, term_to_binary(Data)).

load(#{path := Path}, Id) ->
    Filename = filename:join(Path, Id),
    case file:read_file(Filename) of
        {ok, Binary} -> {ok, binary_to_term(Binary)};
        Error -> Error
    end.

% DB storage implementation
-module(db_storage).
-behaviour(storage).
-export([save/2, load/2]).

save(#{conn := Conn}, Data) ->
    database:insert(Conn, Data).

load(#{conn := Conn}, Id) ->
    database:query(Conn, Id).

Performance Considerations

Lazy Evaluation

Issue: Clojure's lazy sequences are evaluated on-demand; Erlang is eager by default.

Impact: Memory usage and performance characteristics differ.

Solution:

% Use generator pattern for lazy evaluation
lazy_range(Start, End) when Start > End ->
    fun() -> done end;
lazy_range(Start, End) ->
    fun() -> {Start, lazy_range(Start + 1, End)} end.

take_lazy(0, _Gen) -> [];
take_lazy(_, Gen) when is_function(Gen, 0) ->
    case Gen() of
        done -> [];
        {Value, Next} -> [Value | take_lazy(N - 1, Next)]
    end.

Persistent Data Structures

Both Clojure and Erlang use persistent (immutable) data structures, but implementations differ:

  • Clojure: Hash Array Mapped Tries (HAMT) for vectors and maps
  • Erlang: Balanced trees for ordered sets, hash tables for maps

Performance:

  • Clojure vectors: O(log32 n) access and update
  • Erlang lists: O(n) access, O(1) prepend
  • Both maps: O(log n) average

Translation guideline:

  • Clojure vector → Erlang list (accept O(n) or use arrays for random access)
  • Frequent appends: Build reversed list, then reverse once at end

Common Pitfalls

1. Atom Table Exhaustion

Pitfall: Converting Clojure keywords to Erlang atoms dynamically

% BAD - can exhaust atom table
process_json(Json) ->
    maps:map(
        fun(K, V) -> {list_to_atom(binary_to_list(K)), V} end,
        Json
    ).

% GOOD - keep keys as binaries or use known atoms
process_json(Json) ->
    % Keep keys as binaries, or whitelist known atoms
    Json.

2. String vs Binary Confusion

Pitfall: Clojure strings become Erlang lists by default (inefficient)

% BAD - string as list
"hello" = [$h, $e, $l, $l, $o].  % Inefficient

% GOOD - use binaries
<<"hello">>.  % Efficient

3. STM Coordination Assumptions

Pitfall: Assuming automatic conflict resolution like Clojure's STM

% NO automatic retry in Erlang
% Must handle conflicts explicitly or use gen_server for coordination

4. Lazy Sequence Realization

Pitfall: Assuming lazy evaluation (Clojure default)

% Erlang is eager - realize sequences immediately
% Use generators for large or infinite sequences

5. Namespace Confusion

Pitfall: Expecting Clojure-style namespace aliasing

% NO automatic aliasing
% Must use full module names: module:function()
% Or use -import (rarely used in Erlang)

Tooling

Tool Purpose Notes
rebar3 Build tool Equivalent to Leiningen/tools.deps
erlang.mk Alternative build Makefile-based build system
dialyzer Static analysis Type checking for Erlang
PropEr Property testing Equivalent to test.check
Common Test Integration testing More comprehensive than EUnit
observer Runtime inspection GUI tool for debugging, profiling
recon Production debugging Runtime inspection and debugging

Examples

Example 1: Simple - Function with Pattern Matching

Before (Clojure):

(defn greet
  ([name] (greet name "Hello"))
  ([name greeting]
   (str greeting ", " name "!")))

(greet "Alice")          ;; => "Hello, Alice!"
(greet "Bob" "Hi")       ;; => "Hi, Bob!"

After (Erlang):

greet(Name) ->
    greet(Name, "Hello").

greet(Name, Greeting) ->
    lists:flatten(io_lib:format("~s, ~s!", [Greeting, Name])).

% Usage
greet("Alice").          % => "Hello, Alice!"
greet("Bob", "Hi").      % => "Hi, Bob!"

Example 2: Medium - Map Processing with Error Handling

Before (Clojure):

(defn validate-user [user]
  (let [errors (cond-> []
                 (empty? (:name user)) (conj :name-required)
                 (< (:age user 0) 18) (conj :age-too-young)
                 (not (re-matches #".+@.+" (:email user "")))
                   (conj :invalid-email))]
    (if (empty? errors)
      {:ok user}
      {:error errors})))

(defn create-user [user-data]
  (let [{:keys [ok error]} (validate-user user-data)]
    (if ok
      (db/insert ok)
      {:error error})))

After (Erlang):

validate_user(User) ->
    Errors = validate_user_fields(User, []),
    case Errors of
        [] -> {ok, User};
        _ -> {error, Errors}
    end.

validate_user_fields(#{name := Name, age := Age, email := Email} = User, Errors) ->
    Errors1 = case Name of
        <<>> -> [name_required | Errors];
        _ -> Errors
    end,
    Errors2 = case Age of
        A when A < 18 -> [age_too_young | Errors1];
        _ -> Errors1
    end,
    case re:run(Email, ".+@.+") of
        {match, _} -> Errors2;
        nomatch -> [invalid_email | Errors2]
    end;
validate_user_fields(_, Errors) ->
    [missing_fields | Errors].

create_user(UserData) ->
    case validate_user(UserData) of
        {ok, User} ->
            database:insert(User);
        {error, Errors} ->
            {error, Errors}
    end.

Example 3: Complex - Concurrent Worker Pool with Supervision

Before (Clojure):

(require '[clojure.core.async :as async])

(defn worker [id tasks results]
  (async/go-loop []
    (when-let [task (async/<! tasks)]
      (try
        (let [result (process-task task)]
          (async/>! results {:worker id :result result}))
        (catch Exception e
          (async/>! results {:worker id :error (.getMessage e)})))
      (recur))))

(defn start-worker-pool [n-workers]
  (let [tasks (async/chan 100)
        results (async/chan 100)
        workers (doall (map #(worker % tasks results) (range n-workers)))]
    {:tasks tasks
     :results results
     :workers workers}))

(defn submit-task [pool task]
  (async/>!! (:tasks pool) task))

(defn get-result [pool]
  (async/<!! (:results pool)))

;; Usage
(def pool (start-worker-pool 5))
(submit-task pool {:type :compute :data [1 2 3]})
(get-result pool)

After (Erlang):

-module(worker_pool).
-behaviour(supervisor).

%% API
-export([start_link/1, submit_task/2, get_results/1]).

%% Supervisor callbacks
-export([init/1]).

%% Worker functions
-export([worker_loop/2]).

%%% API Functions

start_link(NumWorkers) ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, NumWorkers).

submit_task(Task, ResultsPid) ->
    % Get random worker
    Workers = supervisor:which_children(?MODULE),
    {_, WorkerPid, _, _} = lists:nth(rand:uniform(length(Workers)), Workers),
    WorkerPid ! {task, Task, ResultsPid}.

get_results(ResultsPid) ->
    receive
        {result, Result} -> {ok, Result};
        {error, Reason} -> {error, Reason}
    after 5000 ->
        {error, timeout}
    end.

%%% Supervisor Callbacks

init(NumWorkers) ->
    SupFlags = #{
        strategy => one_for_one,
        intensity => 10,
        period => 60
    },

    WorkerSpecs = [
        #{
            id => {worker, Id},
            start => {?MODULE, worker_loop, [Id, self()]},
            restart => permanent,
            shutdown => 5000,
            type => worker
        } || Id <- lists:seq(1, NumWorkers)
    ],

    {ok, {SupFlags, WorkerSpecs}}.

%%% Worker Functions

worker_loop(Id, SupervisorPid) ->
    receive
        {task, Task, ResultsPid} ->
            try process_task(Task) of
                Result ->
                    ResultsPid ! {result, #{worker => Id, result => Result}},
                    worker_loop(Id, SupervisorPid)
            catch
                error:Reason ->
                    ResultsPid ! {error, #{worker => Id, error => Reason}},
                    worker_loop(Id, SupervisorPid)
            end;
        stop ->
            ok
    end.

process_task(#{type := compute, data := Data}) ->
    % Process task
    lists:sum(Data).

%%% Usage Example

% Start pool
{ok, PoolPid} = worker_pool:start_link(5).

% Submit task
ResultsPid = spawn(fun() ->
    receive
        {result, R} -> io:format("Got result: ~p~n", [R])
    end
end),

worker_pool:submit_task(#{type => compute, data => [1,2,3]}, ResultsPid).

See Also

For more examples and patterns, see:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • convert-clojure-elixir - Similar conversion (JVM LISP → BEAM LISP)
  • convert-clojure-haskell - Dynamic FP → Static FP patterns
  • convert-python-erlang - Imperative → Actor model patterns
  • lang-clojure-dev - Clojure development patterns
  • lang-erlang-dev - Erlang development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev - Async, channels, processes across languages
  • patterns-serialization-dev - JSON, validation across languages
  • patterns-metaprogramming-dev - Macros, parse transforms across languages