Skip to content
Back to skills

Convert Roc Erlang

ASecurity

Convert Roc code to idiomatic Erlang. Use when migrating Roc projects to Erlang/OTP, translating functional patterns to process-based architectures, or refactoring Roc codebases. Extends meta-convert-dev with Roc-to-Erlang specific patterns.

  • 8 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added February 8, 2026
developmentshelltestingdebuggingrefactoringgitapiperformancedocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add aRustyDev/ai --skill convert-roc-erlang --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Convert Roc Erlang?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Convert Roc Erlang
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/arustydev-convert-roc-erlang/badge)](https://www.skillsdirectory.com/skills/arustydev-convert-roc-erlang)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: convert-roc-erlang
description: Convert Roc code to idiomatic Erlang. Use when migrating Roc projects to Erlang/OTP, translating functional patterns to process-based architectures, or refactoring Roc codebases. Extends meta-convert-dev with Roc-to-Erlang specific patterns.
---

# Convert Roc to Erlang

Convert Roc code to idiomatic Erlang. This skill extends `meta-convert-dev` with Roc-to-Erlang specific type mappings, idiom translations, and architectural patterns for moving from pure functional programming to process-based concurrency.

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

## This Skill Does NOT Cover

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

---

## Quick Reference

| Roc | Erlang | Notes |
|-----|--------|-------|
| `[Tag]` | `atom()` | Tags become atoms |
| `I64` / `U64` | `integer()` | Erlang integers are arbitrary precision |
| `F64` | `float()` | 64-bit float |
| `List U8` | `binary()` | Byte sequences |
| `List a` | `list()` | Heterogeneous possible |
| `(a, b, c)` | `{a, b, c}` | Fixed-size tuples |
| `Dict k v` | `map()` or `#{k => v}` | Key-value maps |
| `Ok(value)` | `{ok, Value}` | Success result |
| `Err(reason)` | `{error, Reason}` | Error result |
| `Task a err` | Process/gen_server | Platform tasks become processes |
| `{} -> T` | `fun(() -> T)` | Zero-arg function |
| `None` in tag union | `undefined` | Optional values |

## When Converting Code

1. **Identify pure vs effectful** - Pure functions translate directly, effects need processes
2. **Design process architecture** - Tasks become gen_server or simple processes
3. **Map static types to dynamic patterns** - Use tagged tuples and specs
4. **Implement supervision** - Add supervision trees for fault tolerance
5. **Extract platform logic** - Platform becomes OTP behaviors
6. **Test equivalence** - Verify behavior matches despite different architecture

---

## Paradigm Translation

### Mental Model Shift: Pure Functions + Platform → Processes + Message Passing

| Roc Concept | Erlang Approach | Key Insight |
|-------------|-----------------|-------------|
| Pure function with data | Function or process state | Processes add lifecycle management |
| Function composition | Message passing or function calls | Choose based on concurrency needs |
| Platform task | Process with message loop | Effects require process model |
| Task chaining | Sequential message sends | Or gen_server calls |
| Result type | Tagged tuples `{ok, Val}` / `{error, Reason}` | Explicit error tuples |
| Tag unions | Tagged tuples and pattern matching | Multiple atoms as tags |
| Platform capabilities | OTP behaviors (gen_server, supervisor) | Framework provides structure |

### Concurrency Mental Model

| Roc Model | Erlang Model | Conceptual Translation |
|-----------|--------------|------------------------|
| Platform Tasks | Lightweight processes | Tasks become spawned processes |
| Task composition | Message passing | Chain via messages or calls |
| Pure data flow | Explicit message protocols | Messages are data flow |
| Platform error handling | Supervision trees | Let it crash philosophy |
| Platform-managed lifecycle | Process lifecycle + monitors | Explicit lifecycle management |

---

## Type System Mapping

### Primitive Types

| Roc | Erlang | Notes |
|-----|--------|-------|
| `Bool` | `true` / `false` | Boolean atoms |
| `I8`, `I16`, `I32`, `I64`, `I128` | `integer()` | Arbitrary precision in Erlang |
| `U8`, `U16`, `U32`, `U64`, `U128` | `integer()` | Erlang doesn't distinguish signed/unsigned |
| `F32`, `F64` | `float()` | 64-bit floating point |
| `Str` | `string()` / `binary()` | Use binary for efficiency |
| `List U8` | `binary()` | Byte sequences |

### Collection Types

| Roc | Erlang | Notes |
|-----|--------|-------|
| `List a` | `[a]` | Erlang lists can be heterogeneous |
| `Dict k v` | `#{k => v}` | Maps (Erlang 17+) |
| `Set a` | `sets:set(a)` or `gb_sets:set(a)` | Standard library modules |
| `(A, B, C)` | `{A, B, C}` | Direct tuple mapping |

### Composite Types

| Roc | Erlang | Notes |
|-----|--------|-------|
| `{ field : Type }` | `-record(name, {field :: type()})` | Records with type specs |
| `{ field : Type }` | `#{field => Type}` | Or maps for flexibility |
| `[Tag]` | `atom()` | Single tag becomes atom |
| `[Tag1, Tag2]` | Tagged tuple or atom | Multiple tags use tuples |
| `[Tag(A)]` | `{tag, A}` | Tag with payload |
| `Result a e` | `{ok, A} \| {error, E}` | Standard error convention |

### Function Types

| Roc | Erlang | Notes |
|-----|--------|-------|
| `{} -> R` | `fun(() -> R)` | Zero-argument function |
| `A -> R` | `fun((A) -> R)` | Single argument |
| `A, B -> R` | `fun((A, B) -> R)` | Multiple arguments |
| Generic `<a>` | Dynamic typing | No generics needed |

### Error Types

| Roc | Erlang | Notes |
|-----|--------|-------|
| `Ok(value)` | `{ok, Value}` | Success case |
| `Err(reason)` | `{error, Reason}` | Error case |
| `Result V R` | `{ok, V} \| {error, R}` | Result pattern |
| Tag unions for errors | Multiple error atoms/tuples | Explicit error variants |

---

## Idiom Translation

### Pattern 1: Simple Pure Function

**Roc:**
```roc
interface MathUtils
    exposes [add, square]
    imports []

add : I64, I64 -> I64
add = \a, b -> a + b

square : I64 -> I64
square = \n -> n * n
```

**Erlang:**
```erlang
-module(math_utils).
-export([add/2, square/1]).

-spec add(integer(), integer()) -> integer().
add(A, B) -> A + B.

-spec square(integer()) -> integer().
square(N) -> N * N.
```

**Why this translation:**
- Roc interfaces become Erlang modules
- `exposes` maps to `-export`
- Type signatures become `-spec` declarations
- Lambda syntax becomes function clauses
- No process needed for pure functions

### Pattern 2: Pattern Matching on Tags

**Roc:**
```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)
```

**Erlang:**
```erlang
-spec process_result({ok, Data} | {error, Reason} | unknown) ->
    {success, Data} | {failure, Reason}.
process_result({ok, Data}) ->
    {success, Data};
process_result({error, Reason}) ->
    {failure, Reason};
process_result(unknown) ->
    {failure, unknown_result}.
```

**Why this translation:**
- Roc tags map to Erlang atoms and tagged tuples
- `when` becomes multiple function clauses
- Pattern matching syntax is similar
- Tag payloads become tuple elements

### Pattern 3: List Processing

**Roc:**
```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)
```

**Erlang:**
```erlang
-spec sum([integer()]) -> integer().
sum(List) ->
    lists:foldl(fun(X, Acc) -> X + Acc end, 0, List).

-spec map([A], fun((A) -> B)) -> [B].
map(List, Fn) ->
    lists:map(Fn, List).

-spec filter([A], fun((A) -> boolean())) -> [A].
filter(List, Pred) ->
    lists:filter(Pred, List).
```

**Why this translation:**
- `List.walk` becomes `lists:foldl`
- `List.map` becomes `lists:map`
- `List.keepIf` becomes `lists:filter`
- Higher-order functions translate directly
- Erlang lists module provides standard operations

### Pattern 4: Records

**Roc:**
```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
```

**Erlang:**
```erlang
-record(user, {
    name :: string(),
    age :: non_neg_integer(),
    email :: string()
}).

-spec create_user(string(), non_neg_integer(), string()) -> #user{}.
create_user(Name, Age, Email) ->
    #user{name=Name, age=Age, email=Email}.

-spec update_age(#user{}, non_neg_integer()) -> #user{}.
update_age(User, NewAge) ->
    User#user{age=NewAge}.

-spec get_name(#user{}) -> string().
get_name(#user{name=Name}) ->
    Name.
```

**Why this translation:**
- Roc records map to Erlang records
- Record update syntax is similar
- Pattern matching on records works similarly
- Use type specs for record field types

### Pattern 5: Pure State Functions → gen_server

**Roc:**
```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

# Platform would provide state management primitives
```

**Erlang:**
```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, handle_info/2,
         terminate/2, code_change/3]).

%% API
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

increment() ->
    gen_server:cast(?MODULE, increment).

get_count() ->
    gen_server:call(?MODULE, get_count).

%% gen_server callbacks
init([]) ->
    {ok, 0}.

handle_call(get_count, _From, Count) ->
    {reply, Count, Count};
handle_call(_Request, _From, State) ->
    {reply, ok, State}.

handle_cast(increment, Count) ->
    {noreply, Count + 1};
handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.
```

**Why this translation:**
- Pure state functions become gen_server state machine
- State is maintained by process, not passed as parameter
- Function calls become gen_server:call or cast
- OTP provides supervision and fault tolerance
- Process lifecycle adds start_link, terminate

### Pattern 6: Result Type → Tagged Tuples

**Roc:**
```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)
```

**Erlang:**
```erlang
-spec divide(float(), float()) -> {ok, float()} | {error, division_by_zero}.
divide(_A, 0.0) ->
    {error, division_by_zero};
divide(A, B) ->
    {ok, A / B}.

-spec safe_divide(float(), float()) -> float().
safe_divide(A, B) ->
    case divide(A, B) of
        {ok, Result} -> Result;
        {error, _} -> 0.0
    end.
```

**Why this translation:**
- Roc Result type maps to `{ok, Value}` / `{error, Reason}`
- Pattern matching on result works like case
- `Result.withDefault` becomes case with fallback
- Error tags become atoms

### Pattern 7: Optional Values

**Roc:**
```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"
```

**Erlang:**
```erlang
-spec find_user(non_neg_integer(), [user()]) -> user() | undefined.
find_user(Id, Users) ->
    case lists:keyfind(Id, #user.id, Users) of
        #user{} = User -> User;
        false -> undefined
    end.

-spec get_email(user() | undefined) -> string().
get_email(undefined) ->
    "no email";
get_email(#user{email=Email}) ->
    Email.
```

**Why this translation:**
- Roc None maps to Erlang `undefined` atom
- Some(value) becomes the value directly
- Pattern matching on undefined is explicit
- Alternatively use `{ok, Value}` / `error` pattern

### Pattern 8: List Operations

**Roc:**
```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))
    )
```

**Erlang:**
```erlang
-spec squares([integer()]) -> [integer()].
squares(List) ->
    [X * X || X <- List].

-spec evens([integer()]) -> [integer()].
evens(List) ->
    [X || X <- List, X rem 2 == 0].

-spec pairs([A], [B]) -> [{A, B}].
pairs(List1, List2) ->
    [{X, Y} || X <- List1, Y <- List2].
```

**Why this translation:**
- Roc map/filter operations become list comprehensions
- List comprehensions are idiomatic in Erlang
- `joinMap` (flatMap) becomes nested comprehension
- More concise than explicit map/filter calls

---

## Concurrency Patterns

### Roc Task Model → Erlang Process Model

Roc has no built-in concurrency - it's platform-provided. Erlang's concurrency is core to the language via lightweight processes.

**Roc:**
```roc
# No processes - pure functions
processWork : Data -> Result
processWork = \data ->
    transform(data)

# Platform provides Tasks
doWork : Data -> Task Result []
doWork = \data ->
    Task.fromResult(processWork(data))

# Multiple concurrent tasks (platform-dependent)
doMultipleWork : List Data -> Task (List Result) []
doMultipleWork = \dataList ->
    dataList
    |> List.map(doWork)
    |> Task.sequence
```

**Erlang:**
```erlang
% Direct process spawning
process_work(Data) ->
    transform(Data).

do_work(Data) ->
    % Spawn process to do work
    Self = self(),
    spawn(fun() ->
        Result = process_work(Data),
        Self ! {result, Result}
    end),
    receive
        {result, Result} -> Result
    end.

% Multiple concurrent processes
do_multiple_work(DataList) ->
    Self = self(),
    % Spawn workers
    _Pids = [spawn(fun() ->
        Result = process_work(Data),
        Self ! {result, Result}
    end) || Data <- DataList],
    % Collect results
    [receive {result, R} -> R end || _ <- DataList].
```

**Why this approach:**
- Roc Tasks are abstracted by platform
- Erlang exposes processes directly
- spawn creates lightweight processes
- Message passing is explicit
- Erlang gives fine-grained control

### Platform Effects → OTP Behaviors

**Roc:**
```roc
# Hypothetical platform API
doWorkflow : Input -> Result Output [WorkerFailed, ValidationFailed]
doWorkflow = \input ->
    validated = validate!(input)
    processed = processData!(validated)
    saved = saveResult!(processed)
    Ok(saved)
```

**Erlang:**
```erlang
% gen_server for stateful workflow
-module(workflow_server).
-behaviour(gen_server).

-export([start_link/0, execute/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

execute(Input) ->
    gen_server:call(?MODULE, {execute, Input}).

init([]) ->
    {ok, #{}}.

handle_call({execute, Input}, _From, State) ->
    Result = do_workflow(Input),
    {reply, Result, State}.

do_workflow(Input) ->
    case validate(Input) of
        {ok, Validated} ->
            case process_data(Validated) of
                {ok, Processed} ->
                    save_result(Processed);
                {error, Reason} ->
                    {error, {worker_failed, Reason}}
            end;
        {error, Reason} ->
            {error, {validation_failed, Reason}}
    end.

% ... other callbacks
```

**Why this translation:**
- Roc platform Tasks become gen_server
- Error handling is explicit with tagged tuples
- Process provides lifecycle management
- Supervision can be added for fault tolerance

### Supervision → Explicit Supervision Trees

**Roc:**
```roc
# Platform handles process-level concerns
# Application code doesn't deal with supervision
```

**Erlang:**
```erlang
-module(my_supervisor).
-behaviour(supervisor).

-export([start_link/0, init/1]).

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

init([]) ->
    SupFlags = #{
        strategy => one_for_one,
        intensity => 5,
        period => 60
    },

    ChildSpecs = [
        #{
            id => worker1,
            start => {worker_module, start_link, []},
            restart => permanent,
            shutdown => 5000,
            type => worker,
            modules => [worker_module]
        }
    ],

    {ok, {SupFlags, ChildSpecs}}.
```

**Why this approach:**
- Roc platforms hide supervision
- Erlang exposes supervision trees
- Design supervision hierarchy explicitly
- Configure restart strategies
- Implement "let it crash" philosophy

---

## Error Handling

### Result Types → Let It Crash + Tagged Tuples

**Roc Philosophy:**
```roc
# 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)
```

**Erlang Philosophy:**
```erlang
% Let it crash - supervisor will restart
process_data(Data) ->
    Validated = validate(Data),      % May crash
    Transformed = transform(Validated),     % May crash
    save(Transformed).         % May crash

% Or use tagged tuples for recoverable errors
process_data_safe(Data) ->
    case validate(Data) of
        {ok, Validated} ->
            case transform(Validated) of
                {ok, Transformed} ->
                    save(Transformed);
                {error, Reason} ->
                    {error, {transform_error, Reason}}
            end;
        {error, Reason} ->
            {error, {validation_error, Reason}}
    end.
```

**Key Differences:**
- Roc: Explicit error propagation via Result
- Erlang: Let it crash OR explicit error tuples
- Roc: Fault tolerance via platform
- Erlang: Fault tolerance via process isolation + supervisors

### Error Pattern Translation

| Roc Pattern | Erlang Pattern | Notes |
|-------------|----------------|-------|
| `Err(error)` | `{error, Reason}` or crash | Choose based on recoverability |
| `Ok(value)` | `{ok, Value}` | Standard success pattern |
| `Result.try` (!) | `case` or crash | Chain error handling |
| Tag unions for errors | Atoms or tagged tuples | Multiple error types |
| Platform retry | Explicit retry or supervisor restart | No automatic retry |

---

## Module System

### Roc Interface → Erlang Module

**Roc:**
```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)
```

**Erlang:**
```erlang
-module(calculator).
-export([add/2, subtract/2]).
-export_type([result/0]).

-type result() :: {ok, float()} | {error, invalid_input}.

-spec add(float(), float()) -> result().
add(A, B) -> {ok, A + B}.

-spec subtract(float(), float()) -> result().
subtract(A, B) -> {ok, A - B}.
```

**Why this translation:**
- `interface` becomes `-module`
- `exposes` becomes `-export`
- Type exports use `-export_type`
- Type definitions use `-type`
- Specs provide type signatures

### Application Structure

**Roc Application:**
```
my-app/
├── main.roc          # Entry point
├── Worker.roc        # Worker module
└── Types.roc         # Shared types
```

**Erlang OTP Application:**
```
my_app/
├── src/
│   ├── my_app.app.src    # Application resource file
│   ├── my_app_app.erl    # Application behavior
│   ├── my_app_sup.erl    # Top-level supervisor
│   └── my_worker.erl     # Worker gen_server
├── include/
│   └── my_app.hrl        # Header files
└── rebar.config          # Build configuration
```

**Key Differences:**
- Roc: Single entry point
- Erlang: Application behavior + supervisor tree
- Roc: Platform provides I/O
- Erlang: OTP behaviors structure application
- Erlang adds supervision hierarchy

---

## Platform Architecture

### Roc Application + Platform → OTP Application

**Roc:**
```roc
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!")
```

**Erlang:**
```erlang
% my_app.app.src
{application, my_app, [
    {description, "My Application"},
    {vsn, "1.0.0"},
    {registered, []},
    {mod, {my_app_app, []}},
    {applications, [kernel, stdlib]},
    {env, []}
]}.

% my_app_app.erl
-module(my_app_app).
-behaviour(application).

-export([start/2, stop/1]).

start(_Type, _Args) ->
    io:format("Hello, World!~n"),
    my_app_sup:start_link().

stop(_State) ->
    ok.

% my_app_sup.erl
-module(my_app_sup).
-behaviour(supervisor).

-export([start_link/0, init/1]).

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

init([]) ->
    SupFlags = #{strategy => one_for_one, intensity => 5, period => 60},
    ChildSpecs = [],
    {ok, {SupFlags, ChildSpecs}}.
```

**Why this approach:**
- Roc separates platform from application
- Erlang uses application behavior
- OTP requires supervisor even if empty
- Application file defines metadata
- Erlang exposes more lifecycle control

---

## Testing Strategy

### Roc expect → EUnit

**Roc:**
```roc
add : I64, I64 -> I64
add = \a, b -> a + b

expect add(2, 3) == 5
expect add(0, 0) == 0
expect add(-1, 1) == 0

expect
    result = add(2, 3)
    result == 5
```

**Erlang:**
```erlang
-module(calculator_tests).
-include_lib("eunit/include/eunit.hrl").

add_test() ->
    ?assertEqual(5, calculator:add(2, 3)).

add_zero_test() ->
    ?assertEqual(0, calculator:add(0, 0)).

add_negative_test() ->
    ?assertEqual(0, calculator:add(-1, 1)).

add_complex_test() ->
    Result = calculator:add(2, 3),
    ?assertEqual(5, Result).
```

**Why this translation:**
- Roc inline expects become EUnit test functions
- Test function names end with `_test`
- `?assertEqual` provides assertion
- Tests run with `rebar3 eunit`

---

## Common Pitfalls

1. **Trying to keep everything pure**: Erlang embraces processes and side effects. Don't avoid them.

2. **Not using OTP behaviors**: Raw processes are harder to supervise. Use gen_server, gen_statem, supervisor.

3. **Ignoring dynamic typing**: Erlang is dynamically typed. Use specs and dialyzer, but don't expect compile-time type safety.

4. **Over-engineering for fault tolerance**: Not everything needs to be a process. Pure functions can stay functions.

5. **Translating Result chains literally**: Erlang's error handling is often simpler with let-it-crash. Only use explicit error handling when recovery is possible.

6. **Forgetting about distribution**: Erlang makes distribution easy. Consider whether your application should be distributed.

7. **Not thinking about hot code reload**: Erlang supports hot code reloading. Design with code_change in mind.

8. **Assuming immutability everywhere**: While Erlang data is immutable, process state is mutable via message passing.

9. **Missing the message passing idiom**: Don't just call functions - think about message protocols between processes.

10. **Not using ETS for shared state**: For shared read-heavy state, ETS is more efficient than message passing.

---

## Tooling

| Purpose | Roc | Erlang | Notes |
|---------|-----|--------|-------|
| Build tool | `roc` CLI | rebar3 | Erlang has mature build ecosystem |
| Package manager | Platform URLs | hex.pm + rebar3 | Hex is standard package registry |
| Testing | `roc test` | EUnit, CT, PropEr | Multiple testing frameworks |
| REPL | - | `erl` shell | Interactive development |
| Formatter | `roc format` | erlfmt, rebar3 fmt | Multiple formatters available |
| Documentation | Comments | EDoc | Generate HTML docs |
| Debugger | - | debugger, observer | GUI debugging tools |
| Profiling | - | fprof, eprof | Multiple profiling tools |
| Static analysis | Type system | dialyzer | Gradual typing via specs |
| Release building | Platform | relx (via rebar3) | Production releases |

---

## Examples

### Example 1: Simple - Pure Function Translation

**Before (Roc):**
```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)))"
```

**After (Erlang):**
```erlang
-module(color).
-export([to_string/1]).

-type color() :: red | green | blue | {rgb, byte(), byte(), byte()}.

-spec to_string(color()) -> string().
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]).
```

### Example 2: Medium - State Machine with Error Handling

**Before (Roc):**
```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
```

**After (Erlang):**
```erlang
-module(bank_account).
-behaviour(gen_server).

%% API
-export([start_link/0, deposit/1, withdraw/1, balance/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

-record(state, {
    balance = 0 :: non_neg_integer()
}).

%%% API

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

deposit(Amount) ->
    gen_server:call(?MODULE, {deposit, Amount}).

withdraw(Amount) ->
    gen_server:call(?MODULE, {withdraw, Amount}).

balance() ->
    gen_server:call(?MODULE, balance).

%%% gen_server callbacks

init([]) ->
    {ok, #state{}}.

handle_call({deposit, Amount}, _From, State) when Amount > 0 ->
    NewState = State#state{balance = State#state.balance + Amount},
    {reply, {ok, NewState#state.balance}, NewState};
handle_call({deposit, _}, _From, State) ->
    {reply, {error, invalid_amount}, State};

handle_call({withdraw, Amount}, _From, State) when Amount =< 0 ->
    {reply, {error, invalid_amount}, State};
handle_call({withdraw, Amount}, _From, State) when Amount > State#state.balance ->
    {reply, {error, insufficient_funds}, State};
handle_call({withdraw, Amount}, _From, State) ->
    NewState = State#state{balance = State#state.balance - Amount},
    {reply, {ok, NewState#state.balance}, NewState};

handle_call(balance, _From, State) ->
    {reply, State#state.balance, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.
```

### Example 3: Complex - Pure State Machine to Full OTP Application

**Before (Roc):**
```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,
    }
```

**After (Erlang):**
```erlang
%% task_queue.erl - gen_server implementation
-module(task_queue).
-behaviour(gen_server).

%% API
-export([start_link/0, add_task/1, get_next/0, count/0, stop/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

-record(state, {
    tasks = [] :: [term()],
    processed = 0 :: non_neg_integer()
}).

%%% API Functions

-spec start_link() -> {ok, pid()} | {error, term()}.
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

-spec add_task(term()) -> ok.
add_task(Task) ->
    gen_server:cast(?MODULE, {add, Task}).

-spec get_next() -> {ok, term()} | {error, empty}.
get_next() ->
    gen_server:call(?MODULE, get_next).

-spec count() -> #{pending => non_neg_integer(), processed => non_neg_integer()}.
count() ->
    gen_server:call(?MODULE, count).

-spec stop() -> ok.
stop() ->
    gen_server:stop(?MODULE).

%%% gen_server Callbacks

init([]) ->
    {ok, #state{}}.

handle_call(get_next, _From, #state{tasks = []} = State) ->
    {reply, {error, empty}, State};
handle_call(get_next, _From, #state{tasks = [First | Rest], processed = P} = State) ->
    NewState = State#state{
        tasks = Rest,
        processed = P + 1
    },
    {reply, {ok, First}, NewState};

handle_call(count, _From, #state{tasks = Tasks, processed = P} = State) ->
    Result = #{
        pending => length(Tasks),
        processed => P
    },
    {reply, Result, State}.

handle_cast({add, Task}, #state{tasks = Tasks} = State) ->
    NewState = State#state{tasks = Tasks ++ [Task]},
    {noreply, NewState};
handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%% task_queue_sup.erl - Supervisor
-module(task_queue_sup).
-behaviour(supervisor).

-export([start_link/0, init/1]).

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

init([]) ->
    SupFlags = #{
        strategy => one_for_one,
        intensity => 5,
        period => 60
    },

    ChildSpecs = [
        #{
            id => task_queue,
            start => {task_queue, start_link, []},
            restart => permanent,
            shutdown => 5000,
            type => worker,
            modules => [task_queue]
        }
    ],

    {ok, {SupFlags, ChildSpecs}}.

%% task_queue_app.erl - Application
-module(task_queue_app).
-behaviour(application).

-export([start/2, stop/1]).

start(_Type, _Args) ->
    task_queue_sup:start_link().

stop(_State) ->
    ok.
```

---

## Limitations

### Areas Where Direct Translation Is Difficult

1. **Static Type Safety**: Roc's compile-time type checking doesn't exist in Erlang. Use specs and dialyzer for gradual typing.

2. **Platform Abstraction**: Roc platforms hide implementation details. In Erlang, you must design process architecture explicitly.

3. **Pure Functional Guarantees**: Roc guarantees purity. Erlang processes have side effects - embrace it.

4. **Zero-Cost Abstractions**: Roc compiles to native code. Erlang runs on BEAM VM with different performance characteristics.

5. **Automatic Memory Management**: Roc has predictable memory, Erlang uses per-process GC.

### Working Around Limitations

- **Instead of static types**: Use comprehensive specs and run dialyzer regularly
- **Instead of platform abstraction**: Design OTP application structure explicitly
- **Instead of purity**: Use processes for effects, keep business logic pure where practical
- **Instead of native performance**: Leverage Erlang's concurrency for throughput
- **Instead of predictable memory**: Design for process isolation and let GC handle each process

---

## See Also

For more examples and patterns, see:
- `meta-convert-dev` - Foundational patterns with cross-language examples
- `convert-erlang-roc` - Reverse conversion (Erlang → Roc)
- `lang-roc-dev` - Roc development patterns and platform model
- `lang-erlang-dev` - Erlang development patterns and OTP

Cross-cutting pattern skills:
- `patterns-concurrency-dev` - Pure functions vs actors vs tasks across languages
- `patterns-serialization-dev` - Encoding/decoding across languages
- `patterns-metaprogramming-dev` - Compile-time vs runtime code generation

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…