Agents convert-clojure-erlang
Bidirectional conversion between Clojure and Erlang. Use when migrating projects between these languages in either direction. Extends meta-convert-dev with Clojure↔Erlang specific patterns. 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.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-clojure-erlang" ~/.claude/skills/arustydev-agents-convert-clojure-erlang && rm -rf "$T"
content/skills/convert-clojure-erlang/SKILL.mdClojure ↔ Erlang Conversion
Convert Clojure code to idiomatic Erlang, translating from JVM/Lisp-based functional programming to BEAM/Prolog-style functional programming with actors.
This Skill Extends
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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 - ClojureScript to Erlang - handle as two-step: ClojureScript → Clojure → Erlang
Quick Reference
| Clojure | Erlang | Notes |
|---|---|---|
| | Variables are immutable in both |
| | Function definition |
| | Maps (keywords → atoms, strings → binaries) |
| | Lists (vectors → lists) |
| | Sets |
| | Mutable state → Process |
| | State update |
| | State read |
| | Transaction → Synchronous call |
| | Async execution |
| Message passing pattern | Promise-like behavior |
When Converting Code
- Analyze Clojure semantics - Understand STM, lazy sequences, JVM interop
- Map concurrency first - STM/refs/atoms/agents → gen_server/processes
- Preserve functional purity - Both are functional, maintain referential transparency
- Adopt Erlang idioms - Pattern matching, guards, let-it-crash
- Handle laziness explicitly - Clojure's lazy seqs → Erlang generators/streams
- Test equivalence - Property-based testing with PropEr matching test.check
Type System Mapping
Primitive Types
| Clojure | Erlang | Notes |
|---|---|---|
| or atom | Erlang convention: |
/ | / | Boolean atoms |
| | Integers (arbitrary precision in Clojure → big integers in Erlang) |
| | Floats |
| | Keywords become atoms |
| or | Prefer binaries for efficiency |
| or | Character → integer or binary |
| Symbol | Atom | Both are interned identifiers |
Collection Types
| Clojure | Erlang | Notes |
|---|---|---|
| | Vectors → Lists (note: O(1) vs O(n) append) |
| | Lists → Lists (direct mapping) |
| | Maps → Maps |
| or (MapSet pattern) | Sets |
| Lazy seq | Generator fun or | Explicit laziness needed |
| or generator | Infinite sequences require explicit handling |
| Generator function | Create custom generator |
Composite Types
| Clojure | Erlang | Notes |
|---|---|---|
| or map with type tag | Records or typed maps |
| Module with constructor | Encapsulation via module |
| 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 (
) instead of explicitwhenif - 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->
→assoc
or map update syntaxmaps:put
→update
or pattern match + updatemaps:update_with
→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
map totake
,lists:filter/2
,lists:map/2lists:sublist/2
requires custom implementation (no stdlib equivalent)group-by- 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
is similar to Clojure's[H|T][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
→ tail-recursive function with accumulatorloop/recur- 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
function to realize valuestake - 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
coordination becomes manual two-phase approach or single coordinatordosync- 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
becomes send-receive patternswap!
(deref) becomes query message@atom
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
blocks become spawned processesgo
(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
and{ok, Value}
are Erlang conventions{error, Reason}
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 () | Start shell ( or ) | Both provide interactive environments |
| Send form to REPL (editor integration) | Compile module () | Erlang reloads entire modules |
| Hot reload namespace | Hot code loading () | Erlang's hot swapping is production-ready |
| REPL-driven design | REPL-driven design | Both support incremental development |
| or | Documentation lookup |
| Decompile with | 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)
→ module calls (always qualified):require
→ direct function calls (no import mechanism, use full module name):refer- 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 |
|---|---|---|
| Build tool | Equivalent to Leiningen/tools.deps |
| Alternative build | Makefile-based build system |
| Static analysis | Type checking for Erlang |
| Property testing | Equivalent to test.check |
| Integration testing | More comprehensive than EUnit |
| Runtime inspection | GUI tool for debugging, profiling |
| 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:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Similar conversion (JVM LISP → BEAM LISP)convert-clojure-elixir
- Dynamic FP → Static FP patternsconvert-clojure-haskell
- Imperative → Actor model patternsconvert-python-erlang
- Clojure development patternslang-clojure-dev
- Erlang development patternslang-erlang-dev
Cross-cutting pattern skills:
- Async, channels, processes across languagespatterns-concurrency-dev
- JSON, validation across languagespatterns-serialization-dev
- Macros, parse transforms across languagespatterns-metaprogramming-dev