Claude-skill-registry convert-roc-elixir
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-roc-elixir" ~/.claude/skills/majiayu000-claude-skill-registry-convert-roc-elixir && rm -rf "$T"
skills/data/convert-roc-elixir/SKILL.mdConvert 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
- Foundational conversion patterns (APTV workflow, testing strategies)meta-convert-dev
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 |
|---|---|---|
| | UTF-8 strings (binary in Elixir) |
/ | | Arbitrary precision in Elixir |
| | 64-bit floating point |
| | true/false atoms |
| | Optional values |
| | Result pattern |
| | Lists (different impl: indexed vs linked) |
| or | Records → maps or structs |
| | Tag unions → atoms |
| | Tags with data → tuples |
| | Anonymous functions |
| | Type signatures → specs |
| | Pattern matching |
| or | Effects → processes/tasks |
When Converting Code
- Analyze source thoroughly - Understand Roc's platform model and static guarantees
- Map types first - Convert static type signatures to @specs and guards
- Preserve semantics - Functional purity mostly translates, add runtime validation
- Embrace BEAM - Platform Tasks → OTP processes for concurrency and fault tolerance
- Adopt Elixir idioms - Pattern matching, with statements, pipe operator, protocols
- Handle optionality - Tag unions → tagged tuples, add nil handling
- Test equivalence - Same inputs → same outputs, add property tests for static invariants
- Add supervision - Roc's platform restart → OTP supervision trees
Type System Mapping
Primitive Types
| Roc | Elixir | Notes |
|---|---|---|
| | Both UTF-8, Elixir on binaries |
/ / / / | | Elixir: arbitrary precision integers |
/ / / / | | Use guards for unsigned semantics |
/ | | 64-bit double precision |
| (library) | Use package for precision |
| | / atoms |
(inferred) | | 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 |
|---|---|---|
| | Roc: indexed access O(1); Elixir: linked list O(n) |
| | Set implementations |
| | Hash maps |
| | Tuples (2-element) |
| | Tuples (3-element) |
(bytes) | | 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 |
|---|---|---|
| | Records → maps |
Type alias | or | Structs for typed data |
| | Tags → atoms |
| | Result type → tagged tuples |
| | Optional → nullable or tagged tuple |
| | Tags with payloads → tuples |
Function Types
| Roc | Elixir | Notes |
|---|---|---|
| | Function signature → typespec |
| | Multi-argument function |
| Higher-order function | Functions as values work similarly |
| 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:
# 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:
# 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:
# 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:
# 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
→ Elixir'sResult a err
convention{:ok, a} | {:error, err} - Roc's
try operator → Elixir's!
statement for chainingwith - 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:
# 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:
# 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:
# 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:
# 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
constraints → Elixir's protocol dispatchimplements - 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:
# Platform-provided Task main : Task {} [] main = content = File.readUtf8!("input.txt") processed = String.toUpper(content) File.writeUtf8!("output.txt", processed) Stdout.line!("Done!")
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:
# 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:
# 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:
# 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:
# 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:
# 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:
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
→ Elixir'sinterfacedefmodule - Roc's
→ Elixir'sexposes
(public) vsdef
(private)defp - 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: compile error if color not handled colorName = \color -> when color is Red -> "red" # Missing other cases - compiler error!
# 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
@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: O(1) indexed access getItem = \list, index -> List.get(list, index)
# 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
# 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: Fixed-size integers can overflow x : U8 x = 255 y = x + 1 # Wraps to 0 or raises depending on context
# Elixir: Arbitrary precision - no overflow x = 255 y = x + 1 # Just 256, promotes to bigint automatically
Fix: Add explicit bounds checking if needed
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: Platform handles concurrency main = content1 = File.readUtf8!("file1.txt") content2 = File.readUtf8!("file2.txt") # Platform may parallelize
# 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
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: Explicit optional type findUser : U64 -> [Some User, None] findUser = \id -> # Must return tag union
# 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
@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:
# 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:
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:
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 | CLI | Mix | Mix manages deps, compilation, tasks |
| Package Manager | Platform URLs | Hex | Hex.pm for packages |
| Test Framework | , | 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 | | | Code formatting |
| Linter | Built-in compiler | Credo (optional) | Code quality |
Build System Migration
Roc Application → Mix Project
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:
# 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:
- Create Mix project:
mix new my_app - Convert platform dependencies → Hex packages
- Roc's
→ Elixir'smain : Task {} []
or OTP applicationdef main - Platform I/O → Elixir stdlib or OTP
- Add supervision tree if stateful
Cross-Cutting Patterns
For language-agnostic patterns and cross-language comparison, see:
- Compare Roc's Task model with Elixir's processes/GenServerspatterns-concurrency-dev
- Encode/Decode abilities vs Jason/Protocolspatterns-serialization-dev
- Roc's minimalist approach vs Elixir's powerful macrospatterns-metaprogramming-dev
Examples
Example 1: Simple - Type Conversion
Before (Roc):
# Simple function with type signature double : I64 -> I64 double = \x -> x * 2 # Using it result = double(21) # 42
After (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):
# 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):
# 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):
# 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):
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:
- Foundational patterns with cross-language examplesmeta-convert-dev
- Similar dynamic functional language pairconvert-clojure-elixir
- Reverse direction (dynamic → static)convert-clojure-roc
- Roc development patterns and platform modellang-roc-dev
- Elixir development patterns and OTPlang-elixir-dev
- Async, processes, actors across languagespatterns-concurrency-dev
- JSON, validation, encoding across languagespatterns-serialization-dev