xiaohongshu
xiaohongshu
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 ...
Full skill instructions
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.
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.
meta-convert-devlang-roc-devlang-elixir-devconvert-elixir-roc| 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 |
| 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 | 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 | 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 |
| 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 |
| 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 |
| 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 |
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 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:
Result a err → Elixir's {:ok, a} | {:error, err} convention! try operator → Elixir's with statement for chainingRoc'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: "[email protected]", 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: "[email protected]", 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:
{ r & field: value } → %{struct | field: value}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:
implements constraints → Elixir's protocol dispatchRoc'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:
# 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:
# 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:
# 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:
interface → Elixir's defmoduleexposes → Elixir's def (public) vs defp (private)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
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
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
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
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
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
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
| 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 |
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:
mix new my_appmain : Task {} [] → Elixir's def main or OTP applicationFor language-agnostic patterns and cross-language comparison, see:
patterns-concurrency-dev - Compare Roc's Task model with Elixir's processes/GenServerspatterns-serialization-dev - Encode/Decode abilities vs Jason/Protocolspatterns-metaprogramming-dev - Roc's minimalist approach vs Elixir's powerful macrosBefore (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
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
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
For more examples and patterns, see:
meta-convert-dev - Foundational patterns with cross-language examplesconvert-clojure-elixir - Similar dynamic functional language pairconvert-clojure-roc - Reverse direction (dynamic → static)lang-roc-dev - Roc development patterns and platform modellang-elixir-dev - Elixir development patterns and OTPpatterns-concurrency-dev - Async, processes, actors across languagespatterns-serialization-dev - JSON, validation, encoding across languagesxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience