xiaohongshu
xiaohongshu
convert-erlang-roc
Convert Erlang code to idiomatic Roc. Use when migrating Erlang projects to Roc, translating BEAM/OTP patterns to functional patterns, or refactoring Erlang codebases. Extends meta-convert-dev with Erlang-to-Roc specific patterns.
Full skill instructions
Convert Erlang code to idiomatic Roc. This skill extends meta-convert-dev with Erlang-to-Roc specific type mappings, idiom translations, and architectural patterns for moving from process-based concurrency to pure functional programming.
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.
meta-convert-devlang-erlang-devlang-roc-devconvert-roc-erlang| Erlang | Roc | Notes |
|---|---|---|
atom() | [TagName] | Atoms become tags |
integer() | I64 / U64 | Specify signedness |
float() | F64 | 64-bit float |
binary() | List U8 | Byte sequences |
list() | List a | Homogeneous lists |
tuple() | (a, b, c) | Fixed-size tuples |
map() | Dict k v | Key-value maps |
{ok, Value} | Ok(value) | Success result |
{error, Reason} | Err(reason) | Error result |
pid() | - | No direct equivalent (use platform) |
fun(() -> T) | ({} -> T) | Zero-arg function |
undefined | None in tag union | Optional values |
| Erlang Concept | Roc Approach | Key Insight |
|---|---|---|
| Process with state | Record + functions operating on record | Data and behavior separated, no hidden state |
| Message passing | Function parameters and results | Explicit data flow, no mailboxes |
| Spawn process | Platform task | Effects are platform capability, not language feature |
| gen_server | Pure state machine + platform Task | Business logic pure, I/O delegated to platform |
| Supervisor | Platform-level concern | Fault tolerance handled by host, not application |
| Hot code reload | Platform capability | Not a language feature in Roc |
| Distributed Erlang | Platform networking | Distribution is platform responsibility |
| Erlang Model | Roc Model | Conceptual Translation |
|---|---|---|
| Lightweight processes | Platform Tasks | Concurrency is platform capability |
| Process mailbox | Function composition | Messages become function parameters |
| Selective receive | Pattern matching on values | Match on data, not messages in mailbox |
| Process monitoring | Result types | Failure becomes explicit error values |
| Links and trapping exits | Error propagation with Result | Explicit error handling replaces process signals |
| Erlang | Roc | Notes |
|---|---|---|
atom() | [Tag] or Str | Atoms as tags for enums, Str for dynamic atoms |
integer() | I64 | Default signed 64-bit |
integer() | U64 | Unsigned variant |
integer() (small) | I32 / U32 | For smaller values |
integer() (big) | I128 / U128 | For very large values |
float() | F64 | 64-bit floating point |
boolean() | Bool | Direct mapping |
binary() | List U8 | Byte sequence as list |
bitstring() | List U8 | Byte-aligned only in Roc |
reference() | - | No direct equivalent |
pid() | - | Processes don't exist in Roc |
port() | - | Platform handles I/O |
fun() | Function types | See function mappings below |
| Erlang | Roc | Notes |
|---|---|---|
list() | List a | Homogeneous lists |
[T] notation | List T | Type-safe, uniform elements |
tuple() | (A, B, C) | Fixed-size tuples |
{A, B, C} | (A, B, C) | Direct structural mapping |
map() | Dict k v | Key-value dictionary |
#{K => V} | Dict K V | Must have Hash + Eq for keys |
sets:set() | Set a | Unique values |
ordsets:set() | Set a | Roc sets are always ordered |
queue:queue() | List a | Use list operations |
array:array() | List a | Lists in Roc are efficient |
| Erlang | Roc | Notes |
|---|---|---|
-record(name, {field :: type()}) | { field : Type } | Records become record types |
#name{field = Value} | { field: value } | Record literals |
Tagged tuple {tag, Value} | Tag(value) | Tags with payloads |
| Union types (spec) | [Tag1, Tag2, Tag3] | Tag unions |
-type name() :: spec. | Name : Type | Type alias |
-opaque name() :: spec. | Opaque type Name := Type | Hidden implementation |
| Erlang | Roc | Notes |
|---|---|---|
fun(() -> R) | ({} -> R) | Zero-argument function |
fun((A) -> R) | (A -> R) | Single argument |
fun((A, B) -> R) | (A, B -> R) | Multiple arguments |
fun((A, ...) -> R) | - | Roc doesn't support varargs |
| Erlang | Roc | Notes |
|---|---|---|
{ok, Value} | Ok(value) | Success case |
{error, Reason} | Err(reason) | Error case |
ok atom | Ok({}) | Success with no value |
{ok, V} | {error, R} | Result V R | Result type |
| Exception throw | Err variant | No exceptions, use Result |
Erlang:
-module(math_utils).
-export([add/2, square/1]).
add(A, B) -> A + B.
square(N) -> N * N.
Roc:
interface MathUtils
exposes [add, square]
imports []
add : I64, I64 -> I64
add = \a, b -> a + b
square : I64 -> I64
square = \n -> n * n
Why this translation:
exposesErlang:
process_result({ok, Data}) ->
{success, Data};
process_result({error, Reason}) ->
{failure, Reason};
process_result(unknown) ->
{failure, unknown_result}.
Roc:
processResult : [Ok Data, Err Reason, Unknown] -> [Success Data, Failure Reason]
processResult = \result ->
when result is
Ok(data) -> Success(data)
Err(reason) -> Failure(reason)
Unknown -> Failure(UnknownResult)
Why this translation:
Erlang:
sum([]) -> 0;
sum([H|T]) -> H + sum(T).
map(_, []) -> [];
map(F, [H|T]) -> [F(H) | map(F, T)].
filter(_, []) -> [];
filter(Pred, [H|T]) ->
case Pred(H) of
true -> [H | filter(Pred, T)];
false -> filter(Pred, T)
end.
Roc:
sum : List I64 -> I64
sum = \list ->
List.walk(list, 0, Num.add)
map : List a, (a -> b) -> List b
map = \list, fn ->
List.map(list, fn)
filter : List a, (a -> Bool) -> List a
filter = \list, pred ->
List.keepIf(list, pred)
Why this translation:
List.walk is fold/reduceErlang:
-record(user, {
name :: string(),
age :: integer(),
email :: string()
}).
create_user(Name, Age, Email) ->
#user{name=Name, age=Age, email=Email}.
update_age(#user{} = User, NewAge) ->
User#user{age=NewAge}.
get_name(#user{name=Name}) ->
Name.
Roc:
User : {
name : Str,
age : U32,
email : Str,
}
createUser : Str, U32, Str -> User
createUser = \name, age, email ->
{ name, age, email }
updateAge : User, U32 -> User
updateAge = \user, newAge ->
{ user & age: newAge }
getName : User -> Str
getName = \{ name } ->
name
Why this translation:
#record{} vs { record & })Erlang:
-module(counter_server).
-behaviour(gen_server).
-export([start_link/0, increment/0, get_count/0]).
-export([init/1, handle_call/3, handle_cast/2]).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
increment() ->
gen_server:cast(?MODULE, increment).
get_count() ->
gen_server:call(?MODULE, get_count).
init([]) ->
{ok, 0}.
handle_call(get_count, _From, Count) ->
{reply, Count, Count}.
handle_cast(increment, Count) ->
{noreply, Count + 1}.
Roc:
# Pure state machine - no processes
State : I64
init : State
init = 0
increment : State -> State
increment = \count ->
count + 1
getCount : State -> I64
getCount = \count ->
count
# If you need effects, use platform Tasks
# Platform would provide state management primitives
Why this translation:
Erlang:
divide(_, 0) ->
{error, division_by_zero};
divide(A, B) ->
{ok, A / B}.
safe_divide(A, B) ->
case divide(A, B) of
{ok, Result} -> Result;
{error, _} -> 0
end.
Roc:
divide : F64, F64 -> Result F64 [DivisionByZero]
divide = \a, b ->
if b == 0 then
Err(DivisionByZero)
else
Ok(a / b)
safeDivide : F64, F64 -> F64
safeDivide = \a, b ->
divide(a, b)
|> Result.withDefault(0)
Why this translation:
withDefault) are idiomaticErlang:
find_user(Id, Users) ->
case lists:keyfind(Id, #user.id, Users) of
false -> undefined;
User -> User
end.
get_email(undefined) -> "no email";
get_email(#user{email=Email}) -> Email.
Roc:
findUser : U64, List User -> [Some User, None]
findUser = \id, users ->
users
|> List.findFirst(\user -> user.id == id)
|> Result.map(Some)
|> Result.withDefault(None)
getEmail : [Some User, None] -> Str
getEmail = \maybeUser ->
when maybeUser is
Some({ email }) -> email
None -> "no email"
Why this translation:
undefined maps to tag union with Nonefalse from failed search becomes NoneErlang:
squares(List) ->
[X * X || X <- List].
evens(List) ->
[X || X <- List, X rem 2 == 0].
pairs(List1, List2) ->
[{X, Y} || X <- List1, Y <- List2].
Roc:
squares : List I64 -> List I64
squares = \list ->
List.map(list, \x -> x * x)
evens : List I64 -> List I64
evens = \list ->
List.keepIf(list, \x -> x % 2 == 0)
pairs : List a, List b -> List (a, b)
pairs = \list1, list2 ->
List.joinMap(list1, \x ->
List.map(list2, \y -> (x, y))
)
Why this translation:
joinMap (flatMap)Erlang's concurrency is built on lightweight processes with message passing. Roc has no built-in concurrency - it's all platform-provided.
Erlang:
% Spawn a worker process
Pid = spawn(fun() -> worker_loop() end),
% Send message
Pid ! {self(), work, Data},
% Receive response
receive
{Pid, result, Result} -> Result
after 5000 ->
timeout
end.
worker_loop() ->
receive
{From, work, Data} ->
Result = process(Data),
From ! {self(), result, Result},
worker_loop();
stop ->
ok
end.
Roc:
# No processes - pure functions operating on data
processWork : Data -> Result
processWork = \data ->
# Pure computation
transform(data)
# If concurrent work is needed, platform provides Tasks
# Platform interface might look like:
doWork : Data -> Task Result []
doWork = \data ->
# Platform handles execution
Task.fromResult(processWork(data))
# Multiple concurrent tasks (platform-dependent)
doMultipleWork : List Data -> Task (List Result) []
doMultipleWork = \dataList ->
dataList
|> List.map(doWork)
|> Task.sequence # Platform parallelizes
Why this approach:
Erlang:
-module(my_supervisor).
-behaviour(supervisor).
init([]) ->
SupFlags = #{
strategy => one_for_one,
intensity => 5,
period => 60
},
ChildSpecs = [
#{
id => worker1,
start => {worker, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker
}
],
{ok, {SupFlags, ChildSpecs}}.
Roc:
# Roc doesn't have supervision trees
# Instead, errors are explicit via Result types
# Platform handles process-level concerns
# Application code propagates errors explicitly
doWorkflow : Input -> Result Output [WorkerFailed, ValidationFailed]
doWorkflow = \input ->
validated = validate!(input)
processed = processData!(validated)
saved = saveResult!(processed)
Ok(saved)
# Platform provides retry/recovery if needed
withRetry : Task a err, U32 -> Task a err
withRetry = \task, maxAttempts ->
# Platform-provided retry logic
Task.retry(task, maxAttempts)
Why this translation:
Erlang:
% Connect to remote node
net_adm:ping('node2@hostname'),
% Spawn on remote node
Pid = spawn('node2@hostname', worker, loop, []),
% Send to remote process
{worker, 'node2@hostname'} ! Message,
% RPC call
rpc:call('node2@hostname', module, function, [Args]).
Roc:
# No distributed Erlang equivalent
# Platform provides networking as I/O capability
# Hypothetical platform networking API
sendRequest : Str, Request -> Task Response [NetworkErr]
sendRequest = \url, request ->
# Platform handles HTTP/networking
Http.post(url, request)
# Distributed work requires platform support
# Not a language feature
Why this approach:
Erlang Philosophy:
% Let it crash - supervisor will restart
process_data(Data) ->
validate(Data), % May throw
transform(Data), % May throw
save(Data). % May throw
Roc Philosophy:
# Make errors explicit with Result types
processData : Data -> Result Success [ValidationErr, TransformErr, SaveErr]
processData = \data ->
validated = validate!(data)
transformed = transform!(validated)
saved = save!(transformed)
Ok(saved)
Key Differences:
| Erlang Pattern | Roc Pattern | Notes |
|---|---|---|
throw(Error) | Err(error) | No exceptions, use Result |
exit(Reason) | Err(reason) | Process exit becomes error value |
error(Reason) | Err(reason) | Runtime error becomes Result |
try...catch | when result is Ok/Err | Pattern match on Result |
| Supervisor restart | Platform responsibility | Not in application code |
| Process link | Error propagation via Result | No process links |
| Monitor/demonitor | - | No monitoring in Roc |
Erlang:
-module(calculator).
-export([add/2, subtract/2]).
-export_type([result/0]).
-type result() :: {ok, number()} | {error, atom()}.
add(A, B) -> {ok, A + B}.
subtract(A, B) -> {ok, A - B}.
Roc:
interface Calculator
exposes [Result, add, subtract]
imports []
Result : [Ok F64, Err [InvalidInput]]
add : F64, F64 -> Result
add = \a, b -> Ok(a + b)
subtract : F64, F64 -> Result
subtract = \a, b -> Ok(a - b)
Why this translation:
-module becomes interface-export becomes exposes-export_type types also go in exposesErlang Application:
my_app/
├── src/
│ ├── my_app.erl
│ ├── my_app_sup.erl
│ └── my_worker.erl
├── include/
│ └── my_app.hrl
└── ebin/
Roc Application:
my-app/
├── main.roc # Entry point
├── Worker.roc # Worker module
└── Types.roc # Shared types
Key Differences:
main.roc)Erlang OTP Application:
% Application behavior
-module(my_app).
-behaviour(application).
start(_Type, _Args) ->
my_app_sup:start_link().
stop(_State) ->
ok.
Roc Application:
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/..."
}
import pf.Stdout
import pf.Task exposing [Task]
main : Task {} []
main =
Stdout.line!("Hello, World!")
Why this approach:
┌─────────────────────────────────┐
│ Erlang on BEAM │
│ │
│ • Processes │
│ • Schedulers │
│ • Message passing │
│ • Hot code reload │
│ • Distribution │
└─────────────────────────────────┘
⬇
┌─────────────────────────────────┐
│ Roc Application (Pure) │
│ │
│ • Pure functions │
│ • Data transformations │
│ • Business logic │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ Platform + Host │
│ │
│ • Task execution │
│ • I/O operations │
│ • Concurrency (if provided) │
│ • Memory management │
└─────────────────────────────────┘
Erlang EUnit:
-module(calculator_tests).
-include_lib("eunit/include/eunit.hrl").
add_test() ->
?assertEqual({ok, 5}, calculator:add(2, 3)).
subtract_test() ->
?assertEqual({ok, 1}, calculator:subtract(3, 2)).
Roc expect:
# Inline tests with expect
add : I64, I64 -> I64
add = \a, b -> a + b
expect add(2, 3) == 5
expect add(0, 0) == 0
expect add(-1, 1) == 0
# Top-level expects run with `roc test`
expect
result = add(2, 3)
result == 5
Why this translation:
expect statementsroc testErlang PropEr:
prop_reverse_twice() ->
?FORALL(List, list(integer()),
lists:reverse(lists:reverse(List)) =:= List).
Roc:
# Roc doesn't have built-in property testing yet
# For now, write explicit test cases
expect
list = [1, 2, 3, 4, 5]
reversed = List.reverse(list)
doubleReversed = List.reverse(reversed)
doubleReversed == list
# Future: property testing libraries may emerge
Trying to translate processes directly: Erlang processes don't exist in Roc. Redesign around pure functions and platform Tasks.
Missing the paradigm shift: Erlang is concurrent-first, Roc is pure-first. Separate computation from effects.
Assuming mutable state: Erlang has process state, Roc uses immutable data. State changes are new values.
Ignoring the platform boundary: In Roc, all I/O goes through the platform. Don't expect direct system calls.
Translating supervisors: Supervision is platform-level. Don't try to implement restart logic in application code.
Dynamic typing habits: Erlang allows any(), Roc requires explicit types. Use tag unions for variants.
Hot code reload: Erlang supports this, Roc doesn't. Not a conversion concern.
Binary pattern matching: Erlang's binary patterns are powerful, Roc works with List U8. May need rethinking.
Distributed Erlang features: node clustering, global registry, etc. - these are BEAM features, not Roc capabilities.
Atom literals everywhere: Erlang uses atoms liberally, Roc needs explicit tag unions or strings.
| Purpose | Erlang | Roc | Notes |
|---|---|---|---|
| Build tool | rebar3, mix | roc CLI | Roc has single tool |
| Package manager | hex.pm | Platform URLs | No package registry yet |
| Testing | EUnit, CT, PropEr | roc test | Built-in testing |
| REPL | erl shell | - | No Roc REPL yet |
| Formatter | erlfmt | roc format | Automatic formatting |
| Documentation | EDoc | Comments | No doc tool yet |
| Debugger | debugger | - | No debugger yet |
| Profiling | fprof, eprof | - | Platform-specific |
Before (Erlang):
-module(color).
-export([to_string/1]).
to_string(red) -> "Red";
to_string(green) -> "Green";
to_string(blue) -> "Blue";
to_string({rgb, R, G, B}) ->
io_lib:format("RGB(~p, ~p, ~p)", [R, G, B]).
After (Roc):
interface Color
exposes [Color, toString]
imports []
Color : [Red, Green, Blue, Rgb U8 U8 U8]
toString : Color -> Str
toString = \color ->
when color is
Red -> "Red"
Green -> "Green"
Blue -> "Blue"
Rgb(r, g, b) -> "RGB(\(Num.toStr(r)), \(Num.toStr(g)), \(Num.toStr(b)))"
Before (Erlang):
-module(bank_account).
-export([new/0, deposit/2, withdraw/2, balance/1]).
-record(account, {
balance = 0 :: integer()
}).
new() -> #account{}.
deposit(#account{balance=Balance} = Account, Amount) when Amount > 0 ->
{ok, Account#account{balance=Balance + Amount}};
deposit(_, _) ->
{error, invalid_amount}.
withdraw(#account{balance=Balance} = Account, Amount)
when Amount > 0, Amount =< Balance ->
{ok, Account#account{balance=Balance - Amount}};
withdraw(#account{balance=Balance}, Amount) when Amount > Balance ->
{error, insufficient_funds};
withdraw(_, _) ->
{error, invalid_amount}.
balance(#account{balance=Balance}) ->
Balance.
After (Roc):
interface BankAccount
exposes [Account, new, deposit, withdraw, balance]
imports []
Account : { balance : U64 }
new : Account
new = { balance: 0 }
deposit : Account, U64 -> Result Account [InvalidAmount]
deposit = \account, amount ->
if amount > 0 then
Ok({ account & balance: account.balance + amount })
else
Err(InvalidAmount)
withdraw : Account, U64 -> Result Account [InvalidAmount, InsufficientFunds]
withdraw = \account, amount ->
if amount == 0 then
Err(InvalidAmount)
else if amount > account.balance then
Err(InsufficientFunds)
else
Ok({ account & balance: account.balance - amount })
balance : Account -> U64
balance = \account ->
account.balance
Before (Erlang):
-module(task_queue).
-behaviour(gen_server).
-export([start_link/0, add_task/1, get_next/0, count/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
-record(state, {
tasks = [] :: list(),
processed = 0 :: integer()
}).
%% API
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
add_task(Task) ->
gen_server:cast(?MODULE, {add, Task}).
get_next() ->
gen_server:call(?MODULE, get_next).
count() ->
gen_server:call(?MODULE, count).
%% Callbacks
init([]) ->
{ok, #state{}}.
handle_call(get_next, _From, #state{tasks=[]} = State) ->
{reply, empty, State};
handle_call(get_next, _From, #state{tasks=[H|T], processed=P} = State) ->
NewState = State#state{tasks=T, processed=P+1},
{reply, {ok, H}, NewState};
handle_call(count, _From, #state{tasks=Tasks, processed=P} = State) ->
{reply, {length(Tasks), P}, State}.
handle_cast({add, Task}, #state{tasks=Tasks} = State) ->
NewState = State#state{tasks=Tasks ++ [Task]},
{noreply, NewState}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
After (Roc):
interface TaskQueue
exposes [Queue, empty, addTask, getNext, count]
imports []
Queue task : {
tasks : List task,
processed : U64,
}
empty : Queue task
empty = {
tasks: [],
processed: 0,
}
addTask : Queue task, task -> Queue task
addTask = \queue, task ->
{ queue & tasks: List.append(queue.tasks, task) }
getNext : Queue task -> Result (Queue task, task) [Empty]
getNext = \queue ->
when queue.tasks is
[] -> Err(Empty)
[first, ..rest] ->
newQueue = {
tasks: rest,
processed: queue.processed + 1,
}
Ok((newQueue, first))
count : Queue task -> { pending : U64, processed : U64 }
count = \queue ->
{
pending: List.len(queue.tasks),
processed: queue.processed,
}
# Usage example:
expect
queue = empty
queue1 = addTask(queue, "task1")
queue2 = addTask(queue1, "task2")
result = getNext(queue2)
when result is
Ok((queue3, task)) ->
task == "task1" && count(queue3).pending == 1
Err(Empty) -> Bool.false
# Note: This is a pure data structure
# If you need concurrent access, platform provides that capability
Hot Code Reload: Erlang's live code update has no Roc equivalent. Requires restart.
Distributed Features: BEAM's clustering, global names, distributed process groups - not available in Roc.
Process Isolation: Erlang's per-process memory isolation doesn't map to Roc's data structures.
Selective Receive: Erlang's mailbox pattern matching doesn't exist - Roc uses function parameters.
Binary Pattern Matching: Erlang's bit-level patterns are more powerful than Roc's List U8.
Dynamic Code: Erlang's ability to load/call modules dynamically doesn't exist in statically-typed Roc.
Process Monitoring: Links, monitors, trapping exits - these are BEAM features, not portable to Roc.
For more examples and patterns, see:
meta-convert-dev - Foundational patterns with cross-language exampleslang-erlang-dev - Erlang development patterns and OTPlang-roc-dev - Roc development patterns and platform modelCross-cutting pattern skills:
patterns-concurrency-dev - Processes vs actors vs tasks across languagespatterns-serialization-dev - Encoding/decoding across languagespatterns-metaprogramming-dev - Code generation approachesxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience