--- name: convert-roc-elixir description: Convert Roc code to idiomatic Elixir. Use when migrating Roc platform-based applications to Elixir/BEAM, translating statically-typed functional code to dynamic functional style, or refactoring compile-time verified patterns to leverage Elixir's actor model and OTP. Extends meta-convert-dev with Roc-to-Elixir specific patterns. --- # Convert Roc to Elixir Convert Roc code to idiomatic Elixir. This skill extends `meta-convert-dev` with Roc-to-Elixir specific type mappings, idiom translations, and tooling for translating from statically-typed platform-based architecture to dynamically-typed BEAM runtime with OTP. ## 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's static types → Elixir's dynamic types with optional specs - **Idiom translations**: Compile-time verified patterns → runtime pattern matching - **Error handling**: Result type with exhaustive matching → tagged tuples with case - **Concurrency models**: Platform-managed Tasks → BEAM processes and OTP - **Platform architecture**: Platform/application separation → Mix application with OTP tree - **Paradigm shift**: Static functional with structural types → dynamic functional with protocols ## This Skill Does NOT Cover - General conversion methodology - see `meta-convert-dev` - Roc language fundamentals - see `lang-roc-dev` - Elixir language fundamentals - see `lang-elixir-dev` - Reverse conversion (Elixir → Roc) - see `convert-elixir-roc` - Roc platform development - focus is on Roc applications to Elixir/OTP --- ## Quick Reference | Roc | Elixir | Notes | |-----|--------|-------| | `Str` | `String.t()` | UTF-8 strings (binary in Elixir) | | `I64` / `U64` | `integer()` | Arbitrary precision in Elixir | | `F64` | `float()` | 64-bit floating point | | `Bool` | `boolean()` | true/false atoms | | `[Some a, None]` | `{:ok, a} \| nil` | Optional values | | `Result a err` | `{:ok, a} \| {:error, err}` | Result pattern | | `List a` | `[a]` | Lists (different impl: indexed vs linked) | | `{ field : Type }` | `%{field: value}` or `defstruct` | Records → maps or structs | | `[TagA, TagB]` | `:tag_a \| :tag_b` | Tag unions → atoms | | `TagA(payload)` | `{:tag_a, payload}` | Tags with data → tuples | | `\x -> x` | `fn x -> x end` | Anonymous functions | | `func : a -> b` | `@spec func(a) :: b` | Type signatures → specs | | `when x is` | `case x do` | Pattern matching | | `Task ok err` | `GenServer` or `Task` | Effects → processes/tasks | --- ## When Converting Code 1. **Analyze source thoroughly** - Understand Roc's platform model and static guarantees 2. **Map types first** - Convert static type signatures to @specs and guards 3. **Preserve semantics** - Functional purity mostly translates, add runtime validation 4. **Embrace BEAM** - Platform Tasks → OTP processes for concurrency and fault tolerance 5. **Adopt Elixir idioms** - Pattern matching, with statements, pipe operator, protocols 6. **Handle optionality** - Tag unions → tagged tuples, add nil handling 7. **Test equivalence** - Same inputs → same outputs, add property tests for static invariants 8. **Add supervision** - Roc's platform restart → OTP supervision trees --- ## Type System Mapping ### Primitive Types | Roc | Elixir | Notes | |-----|--------|-------| | `Str` | `String.t()` | Both UTF-8, Elixir on binaries | | `I8` / `I16` / `I32` / `I64` / `I128` | `integer()` | Elixir: arbitrary precision integers | | `U8` / `U16` / `U32` / `U64` / `U128` | `non_neg_integer()` | Use guards for unsigned semantics | | `F32` / `F64` | `float()` | 64-bit double precision | | `Dec` | `Decimal.t()` (library) | Use `decimal` package for precision | | `Bool` | `boolean()` | `true` / `false` atoms | | `Num *` (inferred) | `number()` | Generic number type | **Important differences:** - Roc: Fixed-size integers with explicit overflow behavior - Elixir: Arbitrary precision integers, no overflow - Roc: Compile-time type inference - Elixir: Runtime type checking via guards and pattern matching ### Collection Types | Roc | Elixir | Notes | |-----|--------|-------| | `List a` | `[a]` | Roc: indexed access O(1); Elixir: linked list O(n) | | `Set a` | `MapSet.t(a)` | Set implementations | | `Dict k v` | `%{k => v}` | Hash maps | | `(a, b)` | `{a, b}` | Tuples (2-element) | | `(a, b, c)` | `{a, b, c}` | Tuples (3-element) | | `Str` (bytes) | `binary()` | Byte sequences | **Key difference:** - Roc: Lists support efficient indexed access - Elixir: Lists are linked lists; use tuples or arrays for indexed access ### Composite Types | Roc | Elixir | Notes | |-----|--------|-------| | `{ name: Str, age: U32 }` | `%{name: String.t(), age: non_neg_integer()}` | Records → maps | | Type alias `User : { ... }` | `defstruct [:name, :age]` or `@type` | Structs for typed data | | `[Red, Yellow, Green]` | `:red \| :yellow \| :green` | Tags → atoms | | `[Ok a, Err e]` | `{:ok, a} \| {:error, e}` | Result type → tagged tuples | | `[Some a, None]` | `a \| nil` | Optional → nullable or tagged tuple | | `TagA(a, b)` | `{:tag_a, a, b}` | Tags with payloads → tuples | ### Function Types | Roc | Elixir | Notes | |-----|--------|-------| | `a -> b` | `@spec func(a) :: b` | Function signature → typespec | | `a, b -> c` | `@spec func(a, b) :: c` | Multi-argument function | | `(a -> b) -> c` | Higher-order function | Functions as values work similarly | | `where a implements Eq` | No direct equivalent | Use protocols or runtime checks | --- ## Paradigm Translation ### Mental Model Shift: Roc/Platform → Elixir/BEAM | Roc Concept | Elixir Approach | Key Insight | |-------------|-----------------|-------------| | Platform/Application separation | Mix application with OTP | Platform I/O → GenServer/Task processes | | Structural types (records) | Structs with @type specs | Named vs anonymous data | | Tag unions (exhaustive) | Atoms + pattern matching | Compiler checks → runtime patterns | | Result type | Tagged tuples {:ok/:error} | Explicit → idiomatic convention | | Abilities (traits) | Protocols | Polymorphism approaches differ | | Compile-time verification | Runtime guards + dialyzer | Static → gradual typing | | Tasks (platform effects) | Task/GenServer/Agent | Effects → actor model | | Immutable by default | Immutable by default | Both functional, different impl | ### Concurrency Mental Model | Roc Model | Elixir Model | Conceptual Translation | |-----------|--------------|------------------------| | Task (platform-managed) | GenServer/Agent | Effects → stateful processes | | Sequential Tasks with `!` | GenServer.call chaining | Synchronous execution | | Platform concurrency | spawn/Task.async | Platform handles → explicit processes | | No shared state | Process isolation | Both message-passing | | Platform supervision | OTP Supervisor | Restart policies explicit in Elixir | --- ## Idiom Translation ### Pattern: Tag Unions → Tagged Tuples Roc uses tag unions for discriminated values. Elixir uses tagged tuples with atoms. **Roc:** ```roc # Define union type Color : [Red, Yellow, Green, Custom(U8, U8, U8)] # Pattern matching colorName : Color -> Str colorName = \color -> when color is Red -> "red" Yellow -> "yellow" Green -> "green" Custom(r, g, b) -> "rgb(#{Num.toStr(r)}, #{Num.toStr(g)}, #{Num.toStr(b)})" ``` **Elixir:** ```elixir # Type specification @type color :: :red | :yellow | :green | {:custom, non_neg_integer(), non_neg_integer(), non_neg_integer()} # Pattern matching @spec color_name(color()) :: String.t() def color_name(color) do case color do :red -> "red" :yellow -> "yellow" :green -> "green" {:custom, r, g, b} -> "rgb(#{r}, #{g}, #{b})" end end ``` **Why this translation:** - Roc's exhaustive checking → Elixir relies on runtime pattern matching - Tags → atoms (lightweight constants) - Tags with payloads → tuples with atom tag as first element - Add @type specs for documentation and dialyzer support --- ### Pattern: Result Type → Tagged Tuples Roc's Result type maps directly to Elixir's {:ok, value} / {:error, reason} idiom. **Roc:** ```roc # Using Result type divide : I64, I64 -> Result I64 [DivByZero] divide = \a, b -> if b == 0 then Err(DivByZero) else Ok(a // b) # Using try (!) for propagation calculate : I64, I64, I64 -> Result I64 [DivByZero] calculate = \a, b, c -> x = divide!(a, b) y = divide!(x, c) Ok(y) ``` **Elixir:** ```elixir # Using tagged tuples @spec divide(integer(), integer()) :: {:ok, integer()} | {:error, :division_by_zero} def divide(a, b) when b != 0, do: {:ok, div(a, b)} def divide(_, 0), do: {:error, :division_by_zero} # Using with for error propagation @spec calculate(integer(), integer(), integer()) :: {:ok, integer()} | {:error, :division_by_zero} def calculate(a, b, c) do with {:ok, x} <- divide(a, b), {:ok, y} <- divide(x, c) do {:ok, y} end end ``` **Why this translation:** - Roc's `Result a err` → Elixir's `{:ok, a} | {:error, err}` convention - Roc's `!` try operator → Elixir's `with` statement for chaining - Both make error handling explicit in return types - Elixir pattern matching handles missing cases at runtime --- ### Pattern: Records → Structs Roc's structural records map to Elixir's structs for typed data. **Roc:** ```roc # Record type User : { name : Str, email : Str, age : U32, } # Creating records user : User user = { name: "Alice", email: "alice@example.com", age: 30 } # Updating records updatedUser = { user & age: 31 } # Pattern matching getName : User -> Str getName = \{ name } -> name ``` **Elixir:** ```elixir # Define struct defmodule User do @type t :: %__MODULE__{ name: String.t(), email: String.t(), age: non_neg_integer() } defstruct [:name, :email, :age] end # Creating structs user = %User{name: "Alice", email: "alice@example.com", age: 30} # Updating structs updated_user = %{user | age: 31} # Pattern matching @spec get_name(User.t()) :: String.t() def get_name(%User{name: name}), do: name ``` **Why this translation:** - Roc's structural records → Elixir's named structs - Record update syntax `{ r & field: value }` → `%{struct | field: value}` - Pattern matching syntax similar in both - Add @type for documentation and static analysis --- ### Pattern: Abilities → Protocols Roc's ability system (type classes) translates to Elixir's protocols. **Roc:** ```roc # Using Inspect ability debug : a -> Str where a implements Inspect debug = \value -> Inspect.toStr(value) # Using Eq ability areEqual : a, a -> Bool where a implements Eq areEqual = \x, y -> x == y ``` **Elixir:** ```elixir # Using String.Chars protocol (similar to Inspect) @spec debug(term()) :: String.t() def debug(value) do inspect(value) end # Equality is built-in for all terms @spec are_equal(term(), term()) :: boolean() def are_equal(x, y), do: x == y # Custom protocol defprotocol Serializable do @spec serialize(t) :: String.t() def serialize(data) end defimpl Serializable, for: Map do def serialize(map), do: Jason.encode!(map) end ``` **Why this translation:** - Roc's `implements` constraints → Elixir's protocol dispatch - Roc: compile-time ability resolution; Elixir: runtime protocol dispatch - Built-in abilities (Inspect, Eq) → built-in functions (inspect/1, ==) - Custom abilities → defprotocol + defimpl --- ### Pattern: Platform Tasks → OTP Processes Roc's platform-based Task model translates to Elixir's OTP processes. **Roc:** ```roc # Platform-provided Task main : Task {} [] main = content = File.readUtf8!("input.txt") processed = String.toUpper(content) File.writeUtf8!("output.txt", processed) Stdout.line!("Done!") ``` **Elixir:** ```elixir # Using Task for one-off operations def main do case File.read("input.txt") do {:ok, content} -> processed = String.upcase(content) File.write!("output.txt", processed) IO.puts("Done!") {:error, reason} -> IO.puts("Error: #{inspect(reason)}") end end # Or for stateful operations, use GenServer defmodule FileProcessor do use GenServer def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end def process_file(input, output) do GenServer.call(__MODULE__, {:process, input, output}) end @impl true def init(_opts), do: {:ok, %{}} @impl true def handle_call({:process, input, output}, _from, state) do with {:ok, content} <- File.read(input), processed = String.upcase(content), :ok <- File.write(output, processed) do {:reply, {:ok, "Done!"}, state} else {:error, reason} -> {:reply, {:error, reason}, state} end end end ``` **Why this translation:** - Roc's Task (sequential effects) → Elixir's procedural code or Task.async - Roc's platform manages execution → Elixir explicit process management - Stateful Tasks → GenServer with state - Platform supervision → OTP Supervisor for fault tolerance --- ## Error Handling Translation ### From Result Type to Tagged Tuples **Roc:** ```roc # Multiple error types with tag union parseAndDivide : Str, Str -> Result I64 [ParseError Str, DivByZero] parseAndDivide = \aStr, bStr -> a = Str.toI64!(aStr) |> Result.mapErr(\_ -> ParseError("Invalid a")) b = Str.toI64!(bStr) |> Result.mapErr(\_ -> ParseError("Invalid b")) divide!(a, b) # Handling all error cases (exhaustive) when parseAndDivide("10", "2") is Ok(result) -> "Result: #{Num.toStr(result)}" Err(ParseError(msg)) -> "Parse error: #{msg}" Err(DivByZero) -> "Division by zero" ``` **Elixir:** ```elixir # Multiple error types with tagged tuples @spec parse_and_divide(String.t(), String.t()) :: {:ok, integer()} | {:error, {:parse_error, String.t()} | :division_by_zero} def parse_and_divide(a_str, b_str) do with {:ok, a} <- parse_int(a_str, "Invalid a"), {:ok, b} <- parse_int(b_str, "Invalid b"), {:ok, result} <- divide(a, b) do {:ok, result} end end defp parse_int(str, error_msg) do case Integer.parse(str) do {num, ""} -> {:ok, num} _ -> {:error, {:parse_error, error_msg}} end end # Handling all error cases case parse_and_divide("10", "2") do {:ok, result} -> "Result: #{result}" {:error, {:parse_error, msg}} -> "Parse error: #{msg}" {:error, :division_by_zero} -> "Division by zero" end ``` **Key differences:** - Roc: Compiler enforces exhaustive pattern matching - Elixir: Runtime pattern matching, dialyzer can help detect missing cases - Both make error handling explicit in types/specs --- ## Concurrency Patterns ### Platform Tasks → GenServer State Management **Roc:** ```roc # Platform manages state via Task Counter : Task {} [] Counter = state = 0 loop(state) loop : I64 -> Task {} [] loop = \state -> when receive() is Increment -> loop(state + 1) Get(caller) -> send(caller, state) loop(state) ``` **Elixir:** ```elixir # Explicit GenServer for state management defmodule Counter do use GenServer # Client API def start_link(initial_value \\ 0) do GenServer.start_link(__MODULE__, initial_value, name: __MODULE__) end def increment do GenServer.cast(__MODULE__, :increment) end def get do GenServer.call(__MODULE__, :get) end # Server Callbacks @impl true def init(initial_value), do: {:ok, initial_value} @impl true def handle_cast(:increment, state) do {:noreply, state + 1} end @impl true def handle_call(:get, _from, state) do {:reply, state, state} end end ``` **Why this translation:** - Roc: Platform abstracts process lifecycle - Elixir: Explicit OTP behaviors for structure - Both: Message passing for state updates - Elixir adds supervision, hot code reloading, distribution --- ## Module System Translation ### Roc Modules → Elixir Modules **Roc:** ```roc # Interface declaration interface Math exposes [add, multiply, square] imports [] add : I64, I64 -> I64 add = \a, b -> a + b multiply : I64, I64 -> I64 multiply = \a, b -> a * b # Private function internal : I64 -> I64 internal = \x -> x * 2 square : I64 -> I64 square = \x -> multiply(x, x) ``` **Elixir:** ```elixir defmodule Math do @moduledoc """ Math operations module. """ @spec add(integer(), integer()) :: integer() def add(a, b), do: a + b @spec multiply(integer(), integer()) :: integer() def multiply(a, b), do: a * b # Private function @spec internal(integer()) :: integer() defp internal(x), do: x * 2 @spec square(integer()) :: integer() def square(x), do: multiply(x, x) end ``` **Why this translation:** - Roc's `interface` → Elixir's `defmodule` - Roc's `exposes` → Elixir's `def` (public) vs `defp` (private) - Both support documentation (Roc: doc comments; Elixir: @moduledoc/@doc) - Add @spec for type documentation --- ## Common Pitfalls ### 1. Losing Static Type Safety **Problem:** Roc's compile-time type checking → Elixir runtime errors ```roc # Roc: compile error if color not handled colorName = \color -> when color is Red -> "red" # Missing other cases - compiler error! ``` ```elixir # Elixir: runtime error if pattern not matched def color_name(color) do case color do :red -> "red" # Missing other cases - crash at runtime! end end ``` **Fix:** Add exhaustive patterns and dialyzer specs ```elixir @spec color_name(color()) :: String.t() def color_name(color) do case color do :red -> "red" :yellow -> "yellow" :green -> "green" {:custom, r, g, b} -> "rgb(#{r}, #{g}, #{b})" end end ``` ### 2. List Performance Assumptions **Problem:** Roc lists support O(1) indexed access; Elixir lists are linked (O(n)) ```roc # Roc: O(1) indexed access getItem = \list, index -> List.get(list, index) ``` ```elixir # Elixir: O(n) for lists - inefficient! def get_item(list, index) do Enum.at(list, index) end ``` **Fix:** Use tuples or arrays for indexed access ```elixir # Use tuple for fixed-size indexed access tuple = {1, 2, 3, 4} elem(tuple, 2) # O(1) # Or use :array module for dynamic arrays array = :array.from_list([1, 2, 3, 4]) :array.get(2, array) # Efficient indexed access ``` ### 3. Integer Overflow Behavior **Problem:** Roc has explicit overflow behavior; Elixir has arbitrary precision ```roc # Roc: Fixed-size integers can overflow x : U8 x = 255 y = x + 1 # Wraps to 0 or raises depending on context ``` ```elixir # Elixir: Arbitrary precision - no overflow x = 255 y = x + 1 # Just 256, promotes to bigint automatically ``` **Fix:** Add explicit bounds checking if needed ```elixir def safe_add_u8(a, b) when a >= 0 and a <= 255 and b >= 0 and b <= 255 do result = a + b if result > 255 do {:error, :overflow} else {:ok, result} end end ``` ### 4. Platform Abstractions **Problem:** Roc's platform model hides I/O details; Elixir makes them explicit ```roc # Roc: Platform handles concurrency main = content1 = File.readUtf8!("file1.txt") content2 = File.readUtf8!("file2.txt") # Platform may parallelize ``` ```elixir # Elixir: Explicit sequential execution def main do {:ok, content1} = File.read("file1.txt") {:ok, content2} = File.read("file2.txt") # Sequential by default end ``` **Fix:** Use Task.async for parallelism ```elixir def main do task1 = Task.async(fn -> File.read("file1.txt") end) task2 = Task.async(fn -> File.read("file2.txt") end) {:ok, content1} = Task.await(task1) {:ok, content2} = Task.await(task2) end ``` ### 5. Nil vs Tag Unions **Problem:** Roc has no nil; Elixir uses nil pervasively ```roc # Roc: Explicit optional type findUser : U64 -> [Some User, None] findUser = \id -> # Must return tag union ``` ```elixir # Elixir: Can return nil implicitly def find_user(id) do # nil is valid return value if id == 1 do %User{name: "Alice"} else nil end end ``` **Fix:** Be explicit with tagged tuples for consistency ```elixir @spec find_user(non_neg_integer()) :: {:ok, User.t()} | :error def find_user(id) do if id == 1 do {:ok, %User{name: "Alice"}} else :error end end ``` --- ## Testing Strategy ### Porting Roc Expects to ExUnit **Roc:** ```roc # Inline expect tests expect 1 + 1 == 2 expect List.map([1, 2, 3], \x -> x * 2) == [2, 4, 6] expect result = divide(10, 2) result == Ok(5) ``` **Elixir:** ```elixir defmodule MathTest do use ExUnit.Case test "addition works" do assert 1 + 1 == 2 end test "list map doubles values" do assert Enum.map([1, 2, 3], fn x -> x * 2 end) == [2, 4, 6] end test "divide returns ok tuple" do assert {:ok, 5} = Math.divide(10, 2) end end ``` ### Property-Based Testing for Static Invariants Use StreamData to test invariants that Roc guarantees statically: **Elixir:** ```elixir defmodule PropertiesTest do use ExUnit.Case use ExUnitProperties # Test invariant that Roc enforces: division never returns invalid results property "division always returns ok or error" do check all a <- integer(), b <- integer() do result = Math.divide(a, b) assert match?({:ok, _}, result) or match?({:error, _}, result) end end # Test exhaustiveness (Roc compiler enforces this) property "all color tags have names" do check all color <- one_of([ constant(:red), constant(:yellow), constant(:green), tuple({constant(:custom), integer(0..255), integer(0..255), integer(0..255)}) ]) do # Should not raise assert is_binary(ColorModule.color_name(color)) end end end ``` --- ## Tooling | Category | Roc | Elixir | Notes | |----------|-----|--------|-------| | Build Tool | `roc` CLI | Mix | Mix manages deps, compilation, tasks | | Package Manager | Platform URLs | Hex | Hex.pm for packages | | Test Framework | `expect`, `roc test` | ExUnit | Built-in testing | | Type Checking | Built-in | Dialyzer (optional) | Add @spec for static analysis | | REPL | Planned | IEx | Interactive shell | | Documentation | Doc comments | ExDoc | Generate HTML docs | | Formatter | `roc format` | `mix format` | Code formatting | | Linter | Built-in compiler | Credo (optional) | Code quality | --- ## Build System Migration ### Roc Application → Mix Project **Roc:** ```roc # app header app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/..." } import pf.Stdout import pf.Task exposing [Task] main : Task {} [] main = Stdout.line!("Hello, World!") ``` **Elixir:** ```elixir # mix.exs defmodule MyApp.MixProject do use Mix.Project def project do [ app: :my_app, version: "0.1.0", elixir: "~> 1.14", start_permanent: Mix.env() == :prod, deps: deps() ] end def application do [ extra_applications: [:logger] ] end defp deps do [ {:jason, "~> 1.4"} # Example dependency ] end end # lib/my_app.ex defmodule MyApp do def main do IO.puts("Hello, World!") end end ``` **Migration steps:** 1. Create Mix project: `mix new my_app` 2. Convert platform dependencies → Hex packages 3. Roc's `main : Task {} []` → Elixir's `def main` or OTP application 4. Platform I/O → Elixir stdlib or OTP 5. Add supervision tree if stateful --- ## Cross-Cutting Patterns For language-agnostic patterns and cross-language comparison, see: - `patterns-concurrency-dev` - Compare Roc's Task model with Elixir's processes/GenServers - `patterns-serialization-dev` - Encode/Decode abilities vs Jason/Protocols - `patterns-metaprogramming-dev` - Roc's minimalist approach vs Elixir's powerful macros --- ## Examples ### Example 1: Simple - Type Conversion **Before (Roc):** ```roc # Simple function with type signature double : I64 -> I64 double = \x -> x * 2 # Using it result = double(21) # 42 ``` **After (Elixir):** ```elixir # Function with typespec @spec double(integer()) :: integer() def double(x), do: x * 2 # Using it result = double(21) # 42 ``` ### Example 2: Medium - Result Type with Pattern Matching **Before (Roc):** ```roc # Function returning Result parseAge : Str -> Result U32 [InvalidAge Str] parseAge = \input -> when Str.toU32(input) is Ok(age) if age > 0 && age < 150 -> Ok(age) Ok(_) -> Err(InvalidAge("Age out of range")) Err(_) -> Err(InvalidAge("Not a number")) # Using with pattern matching displayAge : Str -> Str displayAge = \input -> when parseAge(input) is Ok(age) -> "Valid age: #{Num.toStr(age)}" Err(InvalidAge(msg)) -> "Error: #{msg}" ``` **After (Elixir):** ```elixir # Function returning tagged tuple @spec parse_age(String.t()) :: {:ok, non_neg_integer()} | {:error, {:invalid_age, String.t()}} def parse_age(input) do case Integer.parse(input) do {age, ""} when age > 0 and age < 150 -> {:ok, age} {_, ""} -> {:error, {:invalid_age, "Age out of range"}} _ -> {:error, {:invalid_age, "Not a number"}} end end # Using with pattern matching @spec display_age(String.t()) :: String.t() def display_age(input) do case parse_age(input) do {:ok, age} -> "Valid age: #{age}" {:error, {:invalid_age, msg}} -> "Error: #{msg}" end end ``` ### Example 3: Complex - GenServer State Machine **Before (Roc):** ```roc # State machine with platform task State : [Idle, Processing Str, Completed { result : Str, duration : U64 }] process : State, Event -> Task State [] process = \state, event -> when (state, event) is (Idle, Start(input)) -> Task.ok(Processing(input)) (Processing(input), Complete) -> result = String.toUpper(input) duration = 100 # ms Task.ok(Completed({ result, duration })) (Completed(_), Reset) -> Task.ok(Idle) _ -> # Invalid transition Task.ok(state) ``` **After (Elixir):** ```elixir defmodule StateMachine do use GenServer # Client API def start_link(opts \\ []) do GenServer.start_link(__MODULE__, :idle, opts) end def start_processing(pid, input) do GenServer.call(pid, {:start, input}) end def complete(pid) do GenServer.call(pid, :complete) end def reset(pid) do GenServer.call(pid, :reset) end def get_state(pid) do GenServer.call(pid, :get_state) end # Server Callbacks @impl true def init(_) do {:ok, :idle} end @impl true def handle_call({:start, input}, _from, :idle) do {:reply, :ok, {:processing, input}} end def handle_call(:complete, _from, {:processing, input}) do result = String.upcase(input) duration = 100 # ms state = {:completed, %{result: result, duration: duration}} {:reply, {:ok, state}, state} end def handle_call(:reset, _from, {:completed, _}) do {:reply: :ok, :idle} end def handle_call(:get_state, _from, state) do {:reply, state, state} end # Invalid transitions def handle_call(_, _from, state) do {:reply, {:error, :invalid_transition}, state} end end # Usage with supervision defmodule MyApp.Application do use Application def start(_type, _args) do children = [ {StateMachine, name: StateMachine} ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end end ``` --- ## See Also For more examples and patterns, see: - `meta-convert-dev` - Foundational patterns with cross-language examples - `convert-clojure-elixir` - Similar dynamic functional language pair - `convert-clojure-roc` - Reverse direction (dynamic → static) - `lang-roc-dev` - Roc development patterns and platform model - `lang-elixir-dev` - Elixir development patterns and OTP - `patterns-concurrency-dev` - Async, processes, actors across languages - `patterns-serialization-dev` - JSON, validation, encoding across languages --- ## References - [Roc Language](https://www.roc-lang.org/) - [Roc Tutorial](https://www.roc-lang.org/tutorial) - [Elixir Language](https://elixir-lang.org/) - [Phoenix Framework](https://www.phoenixframework.org/) - [Hex Package Manager](https://hex.pm/) - [Dialyzer](https://www.erlang.org/doc/man/dialyzer.html) - [StreamData](https://hexdocs.pm/stream_data/)