Claude Code Plugins

Community-maintained marketplace

Feedback

convert-erlang-roc

@aRustyDev/ai
1
0

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 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-erlang-roc
description 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.

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

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

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

Concurrency Mental Model

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

Type System Mapping

Primitive Types

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

Collection Types

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

Composite Types

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

Function Types

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

Error Types

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

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

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

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

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