Claude-skill-registry 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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-erlang-roc" ~/.claude/skills/majiayu000-claude-skill-registry-convert-erlang-roc && rm -rf "$T"
manifest: skills/data/convert-erlang-roc/SKILL.md
source content

Convert Erlang to Roc

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.

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: Erlang dynamic types → Roc static types
  • Paradigm translation: Process-based concurrency → Pure functional with Tasks
  • Idiom translations: OTP patterns → Roc functional patterns
  • Error handling: Let-it-crash + supervisors → Result types
  • Concurrency: Erlang processes → Roc platform Tasks
  • Module system: Erlang modules → Roc platform/application architecture

This Skill Does NOT Cover

  • General conversion methodology - see
    meta-convert-dev
  • Erlang language fundamentals - see
    lang-erlang-dev
  • Roc language fundamentals - see
    lang-roc-dev
  • Reverse conversion (Roc → Erlang) - see
    convert-roc-erlang

Quick Reference

ErlangRocNotes
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

When Converting Code

  1. Analyze BEAM semantics before writing Roc
  2. Identify process boundaries - these become platform interactions
  3. Map dynamic patterns to static types - use tag unions for variants
  4. Redesign supervision trees - Roc platforms handle failure differently
  5. Extract pure logic - separate computation from effects
  6. Test equivalence - verify behavior matches despite different architecture

Paradigm Translation

Mental Model Shift: BEAM Processes → Pure Functions + Platform Tasks

Erlang ConceptRoc ApproachKey Insight
Process with stateRecord + functions operating on recordData and behavior separated, no hidden state
Message passingFunction parameters and resultsExplicit data flow, no mailboxes
Spawn processPlatform taskEffects are platform capability, not language feature
gen_serverPure state machine + platform TaskBusiness logic pure, I/O delegated to platform
SupervisorPlatform-level concernFault tolerance handled by host, not application
Hot code reloadPlatform capabilityNot a language feature in Roc
Distributed ErlangPlatform networkingDistribution is platform responsibility

Concurrency Mental Model

Erlang ModelRoc ModelConceptual Translation
Lightweight processesPlatform TasksConcurrency is platform capability
Process mailboxFunction compositionMessages become function parameters
Selective receivePattern matching on valuesMatch on data, not messages in mailbox
Process monitoringResult typesFailure becomes explicit error values
Links and trapping exitsError propagation with ResultExplicit error handling replaces process signals

Type System Mapping

Primitive Types

ErlangRocNotes
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 typesSee function mappings below

Collection Types

ErlangRocNotes
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

Composite Types

ErlangRocNotes
-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

Function Types

ErlangRocNotes
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

Error Types

ErlangRocNotes
{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

Idiom Translation

Pattern 1: Simple Function Conversion

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:

  • Erlang modules become Roc interfaces
  • Exported functions go in
    exposes
  • Type signatures are inferred but can be explicit
  • Function definitions use lambda syntax

Pattern 2: Pattern Matching on Tagged Tuples

Erlang:

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 tagged tuples map to Roc tags
  • Pattern matching syntax is similar
  • Tag unions make all cases explicit
  • Type system ensures exhaustiveness

Pattern 3: List Processing

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:

  • Roc provides built-in list functions
  • List.walk
    is fold/reduce
  • Explicit recursion not needed for common operations
  • Higher-order functions are idiomatic

Pattern 4: Records to Records

Erlang:

-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:

  • Erlang records map directly to Roc records
  • Record update syntax is similar (
    #record{}
    vs
    { record & }
    )
  • Pattern matching on records works similarly
  • Roc records are structural, not nominal

Pattern 5: gen_server State Machine → Pure State Functions

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:

  • gen_server becomes pure state functions
  • No process lifecycle - just data transformation
  • State is explicit parameter and return value
  • Effects would be handled by platform, not shown here
  • Platform provides concurrency if needed

Pattern 6: Error Handling with Result

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:

  • Erlang error tuples map to Roc Result type
  • Pattern matching on Result works like case
  • Result combinators (
    withDefault
    ) are idiomatic
  • Compile-time exhaustiveness checking

Pattern 7: Optional Values

Erlang:

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:

  • Erlang
    undefined
    maps to tag union with None
  • false
    from failed search becomes None
  • Pattern matching on option types is explicit
  • Type system prevents forgetting to handle None case

Pattern 8: List Comprehensions

Erlang:

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:

  • Comprehensions become map/filter operations
  • Nested comprehensions use
    joinMap
    (flatMap)
  • More verbose but explicit
  • Type signatures make intent clear

Concurrency Patterns

Erlang Process Model vs Roc Task Model

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:

  • Roc applications don't manage processes
  • Concurrency is a platform capability
  • Business logic stays pure
  • Platform provides Task-based effects

Supervision Trees → Error Handling

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:

  • Supervision is platform responsibility, not application code
  • Errors are explicit Result values
  • No automatic restart - retry is explicit
  • Crash recovery happens at platform/host level

Distributed Erlang → Platform Networking

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:

  • Distribution is platform capability, not language
  • No node clustering built-in
  • Network calls are explicit I/O via Tasks
  • Platform defines distribution model

Error Handling

Let It Crash → Explicit Result Types

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: Crash and restart via supervisor
  • Roc: Explicit error propagation via Result
  • Erlang: Fault tolerance via process isolation
  • Roc: Fault tolerance via platform (if needed)

Error Pattern Translation

Erlang PatternRoc PatternNotes
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 restartPlatform responsibilityNot in application code
Process linkError propagation via ResultNo process links
Monitor/demonitor-No monitoring in Roc

Module System

Erlang Module → Roc Interface

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
    exposes
  • Type definitions use Roc syntax

Application Structure

Erlang 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:

  • Roc: Single entry point (
    main.roc
    )
  • No supervision tree in application code
  • Platform provides I/O capabilities
  • Simpler directory structure

Platform Architecture

OTP Application → Roc Application + Platform

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:

  • Roc separates platform (I/O) from application (logic)
  • Platform provides lifecycle, not application
  • No application behavior callback
  • Platform handles startup/shutdown

BEAM Runtime → Platform + Host

┌─────────────────────────────────┐
│   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            │
└─────────────────────────────────┘

Testing Strategy

EUnit → Roc expect

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:

  • Roc uses inline
    expect
    statements
  • No test framework needed
  • Tests live with code
  • Run with
    roc test

Property-Based Testing

Erlang 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

Common Pitfalls

  1. Trying to translate processes directly: Erlang processes don't exist in Roc. Redesign around pure functions and platform Tasks.

  2. Missing the paradigm shift: Erlang is concurrent-first, Roc is pure-first. Separate computation from effects.

  3. Assuming mutable state: Erlang has process state, Roc uses immutable data. State changes are new values.

  4. Ignoring the platform boundary: In Roc, all I/O goes through the platform. Don't expect direct system calls.

  5. Translating supervisors: Supervision is platform-level. Don't try to implement restart logic in application code.

  6. Dynamic typing habits: Erlang allows

    any()
    , Roc requires explicit types. Use tag unions for variants.

  7. Hot code reload: Erlang supports this, Roc doesn't. Not a conversion concern.

  8. Binary pattern matching: Erlang's binary patterns are powerful, Roc works with List U8. May need rethinking.

  9. Distributed Erlang features: node clustering, global registry, etc. - these are BEAM features, not Roc capabilities.

  10. Atom literals everywhere: Erlang uses atoms liberally, Roc needs explicit tag unions or strings.


Tooling

PurposeErlangRocNotes
Build toolrebar3, mix
roc
CLI
Roc has single tool
Package managerhex.pmPlatform URLsNo package registry yet
TestingEUnit, CT, PropEr
roc test
Built-in testing
REPL
erl
shell
-No Roc REPL yet
Formattererlfmt
roc format
Automatic formatting
DocumentationEDocCommentsNo doc tool yet
Debuggerdebugger-No debugger yet
Profilingfprof, eprof-Platform-specific

Examples

Example 1: Simple - Function with Pattern Matching

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)))"

Example 2: Medium - State Machine with Error Handling

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

Example 3: Complex - gen_server Reimagined as Pure State Machine

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

Limitations

Areas Where Direct Translation Is Difficult

  1. Hot Code Reload: Erlang's live code update has no Roc equivalent. Requires restart.

  2. Distributed Features: BEAM's clustering, global names, distributed process groups - not available in Roc.

  3. Process Isolation: Erlang's per-process memory isolation doesn't map to Roc's data structures.

  4. Selective Receive: Erlang's mailbox pattern matching doesn't exist - Roc uses function parameters.

  5. Binary Pattern Matching: Erlang's bit-level patterns are more powerful than Roc's List U8.

  6. Dynamic Code: Erlang's ability to load/call modules dynamically doesn't exist in statically-typed Roc.

  7. Process Monitoring: Links, monitors, trapping exits - these are BEAM features, not portable to Roc.

Working Around Limitations

  • Instead of hot reload: Design for fast restart or use platform-provided mechanism
  • Instead of distribution: Use explicit networking via platform HTTP/TCP
  • Instead of processes: Use pure functions + platform Tasks
  • Instead of selective receive: Structure data for pattern matching
  • Instead of binary patterns: Work with List U8 and helper functions
  • Instead of dynamic code: Use tag unions for known variants
  • Instead of process monitoring: Use Result types for error handling

See Also

For more examples and patterns, see:

  • meta-convert-dev
    - Foundational patterns with cross-language examples
  • lang-erlang-dev
    - Erlang development patterns and OTP
  • lang-roc-dev
    - Roc development patterns and platform model

Cross-cutting pattern skills:

  • patterns-concurrency-dev
    - Processes vs actors vs tasks across languages
  • patterns-serialization-dev
    - Encoding/decoding across languages
  • patterns-metaprogramming-dev
    - Code generation approaches