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