| name | lang-roc-dev |
| description | Foundational Roc patterns covering platform/application architecture, records, tags, pattern matching, abilities, and functional idioms. Use when writing Roc code, understanding the platform model, or needing guidance on which specialized Roc skill to use. This is the entry point for Roc development. |
Roc Fundamentals
Foundational Roc patterns and core language features. This skill serves as both a reference for common patterns and an index to specialized Roc skills.
Overview
┌─────────────────────────────────────────────────────────────────┐
│ Roc Skill Hierarchy │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────┐ │
│ │ lang-roc-dev │ ◄── You are here │
│ │ (foundation) │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ patterns │ │ platform │ │
│ │ -dev │ │ -dev │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
This skill covers:
- Platform vs application architecture
- Records and their operations
- Tags and tag unions
- Pattern matching with when expressions
- Abilities (Roc's trait system)
- Result type and error handling
- Functional patterns and idioms
- Type inference fundamentals
This skill does NOT cover (see specialized skills):
- Platform development →
lang-roc-platform-dev - Best practices and advanced patterns →
lang-roc-patterns-dev - Testing strategies →
lang-roc-patterns-dev
Quick Reference
| Task | Syntax |
|---|---|
| Define record | { name: "Alice", age: 30 } |
| Record type | { name : Str, age : U32 } |
| Update record | { user & age: 31 } |
| Define tag | Red or Custom(40, 60, 80) |
| Tag union type | [Red, Yellow, Green] |
| Pattern match | when x is ... -> ... |
| Function type | add : I64, I64 -> I64 |
| Error handling | Result a err |
| Ability constraint | a -> Str where a implements Inspect |
| Inline test | expect 1 + 1 == 2 |
Skill Routing
Use this table to find the right specialized skill:
| When you need to... | Use this skill |
|---|---|
| Build custom platforms | lang-roc-platform-dev |
| Advanced testing strategies | lang-roc-patterns-dev |
| Performance optimization | lang-roc-patterns-dev |
| Library design patterns | lang-roc-patterns-dev |
Platform vs Application Model
Architecture Overview
Roc uses a unique separation between platforms and applications:
┌─────────────────────────────────────────────────┐
│ Application │
│ (Pure functional Roc code) │
│ │
│ • Business logic │
│ • Data transformations │
│ • No direct I/O │
└────────────┬────────────────────────────────────┘
│ Pure interface
▼
┌─────────────────────────────────────────────────┐
│ Platform │
│ (Roc API + Host implementation) │
│ │
│ Roc API: Pure functions applications use │
│ Host: Actual I/O implementation │
│ (written in Rust, C, Zig, etc.) │
└─────────────────────────────────────────────────┘
Application Structure
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}
import pf.Stdout
import pf.Task exposing [Task]
main : Task {} []
main =
Stdout.line! "Hello, World!"
Key concepts:
appheader declares entry point and platform- Platform provides I/O capabilities (Stdout, File, Http, etc.)
- Application code remains pure
- Platform handles all effects
Platform Responsibilities
Platforms provide:
- I/O primitives: File system, network, console
- Memory management: Allocation and deallocation
- Program lifecycle: Startup, shutdown, event loops
- Host integration: FFI to system libraries
Common Platforms
| Platform | Purpose | Use Case |
|---|---|---|
basic-cli |
CLI applications | Scripts, command-line tools |
basic-webserver |
Web servers | HTTP services, APIs |
static-site-gen |
Static sites | Documentation, blogs |
Records
Record Basics
# Record literal
user = { name: "Alice", age: 30, active: Bool.true }
# Record type annotation
user : { name : Str, age : U32, active : Bool }
# Accessing fields
userName = user.name
userAge = user.age
# Field access with destructuring
{ name, age } = user
Record Updates
# Update single field
olderUser = { user & age: 31 }
# Update multiple fields
updatedUser = { user &
age: 31,
active: Bool.false
}
# Nested updates
person = {
name: "Alice",
address: { city: "NYC", zip: "10001" }
}
movedPerson = { person &
address: { person.address & city: "SF" }
}
Optional Fields
Roc uses tag unions for optional values, not special record syntax:
# Type with optional email
User : {
name : Str,
age : U32,
email : [Some Str, None],
}
# Creating users
userWithEmail = {
name: "Alice",
age: 30,
email: Some("alice@example.com")
}
userWithoutEmail = {
name: "Bob",
age: 25,
email: None
}
# Handling optional values
emailText = when user.email is
Some(addr) -> addr
None -> "no email"
Record Functions
# Function taking a record
greet : { name : Str, age : U32 } -> Str
greet = \user ->
"Hello, \(user.name)! You are \(Num.toStr(user.age))"
# Destructuring in parameters
greetShort : { name : Str, age : U32 } -> Str
greetShort = \{ name, age } ->
"Hello, \(name)! You are \(Num.toStr(age))"
# Partial destructuring
getName : { name : Str, age : U32 } -> Str
getName = \{ name } -> # age is ignored
name
Tags and Tag Unions
Tag Basics
# Simple tags (like enums)
color = Red
direction = North
# Tags with payloads
color = Custom(40, 60, 80)
result = Ok(42)
error = Err("Something went wrong")
# Tags with named payloads (records)
point = Point({ x: 10, y: 20 })
# Or more commonly
point = Point({ x: 10, y: 20 })
Tag Union Types
# Type definition
Color : [Red, Yellow, Green, Custom(U8, U8, U8)]
# Union of different tag shapes
Result a e : [Ok a, Err e]
# Nested unions
Expression : [
Num(I64),
Add(Expression, Expression),
Multiply(Expression, Expression),
]
Pattern Matching on Tags
# Basic matching
colorName = when color is
Red -> "red"
Yellow -> "yellow"
Green -> "green"
Custom(r, g, b) -> "rgb(\(Num.toStr(r)), \(Num.toStr(g)), \(Num.toStr(b)))"
# Matching with guards
describe = when age is
n if n < 13 -> "child"
n if n < 20 -> "teenager"
_ -> "adult"
# Nested pattern matching
eval : Expression -> I64
eval = \expr ->
when expr is
Num(n) -> n
Add(left, right) -> eval(left) + eval(right)
Multiply(left, right) -> eval(left) * eval(right)
Structural Tag Unions
Roc uses structural (not nominal) types:
# No need to declare the type separately
handleStatus : [Pending, Approved, Rejected] -> Str
handleStatus = \status ->
when status is
Pending -> "Waiting..."
Approved -> "Done!"
Rejected -> "Failed"
# Type is inferred from patterns
# This is equivalent to:
# Status : [Pending, Approved, Rejected]
# handleStatus : Status -> Str
Open vs Closed Tag Unions
# Closed tag union (exact set)
Color : [Red, Green, Blue]
# Open tag union (can accept more)
# Used for extensibility
handleColor : [Red, Green, Blue]* -> Str
handleColor = \color ->
when color is
Red -> "red"
Green -> "green"
Blue -> "blue"
_ -> "unknown color"
# The * makes it open to additional tags
Pattern Matching
When Expressions
# Basic when
result = when value is
1 -> "one"
2 -> "two"
_ -> "other"
# With destructuring
when point is
{ x: 0, y: 0 } -> "origin"
{ x, y: 0 } -> "on x-axis at \(Num.toStr(x))"
{ x: 0, y } -> "on y-axis at \(Num.toStr(y))"
{ x, y } -> "at (\(Num.toStr(x)), \(Num.toStr(y)))"
# Multiple values
when (x, y) is
(0, 0) -> "origin"
(0, _) -> "on y-axis"
(_, 0) -> "on x-axis"
_ -> "elsewhere"
Pattern Types
# Literal patterns
when n is
0 -> "zero"
1 -> "one"
_ -> "other"
# Variable binding
when result is
Ok(value) -> value
Err(msg) -> "Error: \(msg)"
# List patterns
when list is
[] -> "empty"
[first] -> "single: \(first)"
[first, second] -> "pair: \(first), \(second)"
[first, ..] -> "starts with: \(first)"
# Record patterns
when user is
{ name: "Alice", .. } -> "Hello Alice!"
{ name, .. } -> "Hello \(name)!"
# Guard clauses
when n is
x if x < 0 -> "negative"
x if x > 0 -> "positive"
_ -> "zero"
Exhaustiveness Checking
# Compiler enforces exhaustive matching
handleColor : [Red, Green, Blue] -> Str
handleColor = \color ->
when color is
Red -> "red"
Green -> "green"
# Missing Blue - compiler error!
# Must handle all cases or use wildcard
handleColorComplete : [Red, Green, Blue] -> Str
handleColorComplete = \color ->
when color is
Red -> "red"
Green -> "green"
Blue -> "blue" # Now complete
# Or use wildcard for remaining cases
handleColorDefault : [Red, Green, Blue] -> Str
handleColorDefault = \color ->
when color is
Red -> "red"
_ -> "not red"
Abilities
What are Abilities?
Abilities are Roc's version of traits/typeclasses:
# Using the Eq ability
areEqual : a, a -> Bool where a implements Eq
areEqual = \x, y ->
x == y
# Using the Inspect ability (like Debug in Rust)
debug : a -> Str where a implements Inspect
debug = \value ->
Inspect.toStr(value)
Built-in Abilities
| Ability | Purpose | Example |
|---|---|---|
Eq |
Structural equality | x == y |
Hash |
Hashing for dictionaries | Dict.insert(dict, key, value) |
Inspect |
Debug representation | Inspect.toStr(value) |
Decode |
Deserialize from bytes | Decode.fromBytes(bytes) |
Encode |
Serialize to bytes | Encode.toBytes(value) |
Automatic Derivation
Records and tags automatically implement abilities:
# This type automatically has Eq, Hash, Inspect
User : {
name : Str,
age : U32,
role : [Admin, User, Guest],
}
user1 = { name: "Alice", age: 30, role: Admin }
user2 = { name: "Alice", age: 30, role: Admin }
# Eq works automatically
expect user1 == user2 # true
# Inspect works automatically
dbg(user1) # Shows: { name: "Alice", age: 30, role: Admin }
Custom Ability Implementations
# Define a custom ability
Hash implements
hash : a -> U64 where a implements Hash
# Implement for custom type
CustomType implements [Hash]
hash = \val ->
# Custom hashing logic
computeHash(val)
Ability Constraints in Functions
# Single constraint
toString : a -> Str where a implements Inspect
toString = \value ->
Inspect.toStr(value)
# Multiple constraints
compare : a, a -> [Less, Equal, Greater]
where a implements Eq & Ord
compare = \x, y ->
if x < y then
Less
else if x > y then
Greater
else
Equal
# Constraints on type parameters
Map k v : ... where k implements Hash & Eq
Result Type and Error Handling
Result Basics
# Result type definition
Result a e : [Ok a, Err e]
# Returning Results
divide : I64, I64 -> Result I64 [DivByZero]
divide = \a, b ->
if b == 0 then
Err(DivByZero)
else
Ok(a // b)
# Using Results
when divide(10, 2) is
Ok(result) -> Num.toStr(result)
Err(DivByZero) -> "Cannot divide by zero"
Error Propagation with Try
# Using try (!) for error propagation
calculate : I64, I64, I64 -> Result I64 [DivByZero]
calculate = \a, b, c ->
x = divide!(a, b) # Returns early on Err
y = divide!(x, c) # Returns early on Err
Ok(y)
# Equivalent to:
calculateVerbose : I64, I64, I64 -> Result I64 [DivByZero]
calculateVerbose = \a, b, c ->
when divide(a, b) is
Err(e) -> Err(e)
Ok(x) ->
when divide(x, c) is
Err(e) -> Err(e)
Ok(y) -> Ok(y)
Multiple Error Types
# Tag union for different errors
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
when parseAndDivide("10", "2") is
Ok(result) -> "Result: \(Num.toStr(result))"
Err(ParseError(msg)) -> "Parse error: \(msg)"
Err(DivByZero) -> "Division by zero"
Result Helpers
# Map over Ok value
result = divide(10, 2)
|> Result.map(\x -> x * 2) # Ok(10)
# Map over Err value
result = divide(10, 0)
|> Result.mapErr(\DivByZero -> "Error: division by zero")
# Provide default on error
value = divide(10, 0)
|> Result.withDefault(0) # Returns 0
# Chain Results
chain : Result a e, (a -> Result b e) -> Result b e
result = divide(10, 2)
|> Result.try(\x -> divide(x, 2))
Type System Fundamentals
Type Inference
# Types are inferred
double = \x -> x * 2
# Inferred type: Num a -> Num a
# Can add explicit annotations
double : I64 -> I64
double = \x -> x * 2
# Type parameters
identity : a -> a
identity = \x -> x
# Type parameters with constraints
show : a -> Str where a implements Inspect
show = \x -> Inspect.toStr(x)
Number Types
# Integer types
i8, i16, i32, i64, i128 # Signed integers
u8, u16, u32, u64, u128 # Unsigned integers
# Flexible numbers (type inferred from usage)
x = 42 # Num *
y = x + 1 # Still Num *, becomes concrete when needed
# Explicit typing
x : I64
x = 42
# Floating point
f32, f64 # 32-bit and 64-bit floats
pi : F64
pi = 3.14159
Function Types
# Function with single parameter
increment : I64 -> I64
# Multiple parameters (curried)
add : I64, I64 -> I64
# Function taking a function
map : List a, (a -> b) -> List b
# Function with abilities constraint
sort : List a -> List a where a implements Ord
Type Aliases
# Create type aliases for clarity
UserId : U64
UserName : Str
User : {
id : UserId,
name : UserName,
email : Str,
}
# Opaque types (hide implementation)
Age := U32
createAge : U32 -> Age
createAge = \n -> @Age(n)
getAge : Age -> U32
getAge = \@Age(n) -> n
Functional Patterns
List Operations
# Map
numbers = [1, 2, 3, 4, 5]
doubled = List.map(numbers, \n -> n * 2)
# [2, 4, 6, 8, 10]
# Filter
evens = List.keepIf(numbers, \n -> n % 2 == 0)
# [2, 4]
# Fold (reduce)
sum = List.walk(numbers, 0, \acc, n -> acc + n)
# 15
# Find
maybeFirst = List.findFirst(numbers, \n -> n > 3)
# Ok(4)
# Chain operations with pipeline
result = numbers
|> List.map(\n -> n * 2)
|> List.keepIf(\n -> n > 5)
|> List.walk(0, Num.add)
Pipeline Operator
# Without pipeline
result = add(multiply(2, 3), 4)
# With pipeline (left to right)
result = 2
|> multiply(3)
|> add(4)
# Common with list operations
users
|> List.map(\u -> u.name)
|> List.sortAsc
|> Str.joinWith(", ")
Dict (Dictionary/Map)
# Create dictionary
scores = Dict.empty({})
|> Dict.insert("Alice", 100)
|> Dict.insert("Bob", 85)
|> Dict.insert("Charlie", 92)
# Get value
aliceScore = Dict.get(scores, "Alice")
# Ok(100)
# Update value
newScores = Dict.update(scores, "Alice", \maybeScore ->
when maybeScore is
Some(score) -> Some(score + 10)
None -> None
)
# Keys and values
allNames = Dict.keys(scores)
allScores = Dict.values(scores)
Set Operations
# Create sets
set1 = Set.fromList([1, 2, 3, 4])
set2 = Set.fromList([3, 4, 5, 6])
# Union
union = Set.union(set1, set2)
# {1, 2, 3, 4, 5, 6}
# Intersection
intersection = Set.intersection(set1, set2)
# {3, 4}
# Difference
diff = Set.difference(set1, set2)
# {1, 2}
# Contains
hasThree = Set.contains(set1, 3)
# Bool.true
Testing with Expect
Basic Expects
# Simple test
expect 1 + 1 == 2
# Test with variables
add = \a, b -> a + b
expect add(2, 3) == 5
# Multiple expects
expect
result = divide(10, 2)
result == Ok(5)
expect
result = divide(10, 0)
result == Err(DivByZero)
Inline Expects
# Verify assumptions in functions
factorial : U64 -> U64
factorial = \n ->
# Verify our assumption
expect n <= 20 # Factorial grows quickly!
when n is
0 -> 1
_ -> n * factorial(n - 1)
Testing with roc test
# Top-level expects run with `roc test`
expect List.map([1, 2, 3], \x -> x * 2) == [2, 4, 6]
expect
users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
]
oldest = List.sortWith(users, \a, b ->
Num.compare(b.age, a.age)
)
List.first(oldest) == Ok({ name: "Alice", age: 30 })
Common Idioms
Option Pattern (Maybe)
# Roc doesn't have a built-in Maybe/Option
# Use tag unions instead
MaybeUser : [Some User, None]
findUser : U64 -> [Some User, None]
findUser = \id ->
# ... search logic
if found then
Some(user)
else
None
# Using the result
when findUser(1) is
Some(user) -> "Found: \(user.name)"
None -> "Not found"
Parsing Pattern
# Parser combinator style
Parser a : Str -> Result { value : a, rest : Str } [ParseError Str]
parseInt : Parser I64
parseInt = \input ->
when Str.toI64(input) is
Ok(n) -> Ok({ value: n, rest: "" })
Err(_) -> Err(ParseError("Not a number"))
# Chain parsers
parsePoint : Parser { x : I64, y : I64 }
parsePoint = \input ->
{ value: x, rest: afterX } = parseInt!(input)
{ value: y, rest: afterY } = parseInt!(afterX)
Ok({ value: { x, y }, rest: afterY })
Task Pattern (Effects)
# Platform-provided Task type for effects
Task a err : [Task a err]
# Chaining tasks
main : Task {} []
main =
# Read file
content = File.readUtf8!("input.txt")
# Process content
processed = String.toUpper(content)
# Write result
File.writeUtf8!("output.txt", processed)
# Log completion
Stdout.line!("Done!")
Builder Pattern
# Use records with update syntax
RequestBuilder : {
url : Str,
method : [Get, Post, Put, Delete],
headers : Dict Str Str,
body : [Some Str, None],
}
defaultRequest : Str -> RequestBuilder
defaultRequest = \url -> {
url,
method: Get,
headers: Dict.empty({}),
body: None,
}
# Build requests
request = defaultRequest("https://api.example.com")
|> \r -> { r & method: Post }
|> \r -> { r & headers: Dict.insert(r.headers, "Content-Type", "application/json") }
|> \r -> { r & body: Some("{\"key\": \"value\"}") }
Troubleshooting
Type Mismatch Errors
Problem: "Type mismatch in when branch"
# Wrong - branches return different types
value = when condition is
Bool.true -> 42
Bool.false -> "false" # Error!
Fix: Ensure all branches return the same type:
value = when condition is
Bool.true -> "true"
Bool.false -> "false"
Non-Exhaustive Pattern Match
Problem: "Pattern match is not exhaustive"
# Wrong - missing Blue case
colorName = when color is
Red -> "red"
Green -> "green"
# Missing Blue!
Fix: Add all cases or use wildcard:
# Option 1: Add missing case
colorName = when color is
Red -> "red"
Green -> "green"
Blue -> "blue"
# Option 2: Use wildcard
colorName = when color is
Red -> "red"
_ -> "other"
Circular Type Definitions
Problem: Type depends on itself incorrectly
# Correct recursive type
List a : [Nil, Cons a (List a)]
# Wrong - missing tag wrapper
BadList a : [a, BadList a] # Error!
Fix: Wrap recursive references in tags:
List a : [Nil, Cons a (List a)]
Ability Constraint Errors
Problem: "Type doesn't implement required ability"
# Wrong - function has no Eq
compare : a, a -> Bool where a implements Eq
compare = \x, y -> x == y
# Trying to use with function
fn = \x -> x + 1
result = compare(fn, fn) # Error: function doesn't implement Eq
Fix: Only use types that implement the required ability:
result = compare(42, 42) # OK: I64 implements Eq
Task Error Propagation
Problem: Not handling task errors properly
# Wrong - ignoring potential errors
main =
content = File.readUtf8!("missing.txt") # Might fail!
Stdout.line!(content)
Fix: Handle errors explicitly:
main =
when File.readUtf8("missing.txt") is
Ok(content) -> Stdout.line!(content)
Err(err) -> Stderr.line!("Error: \(Inspect.toStr(err))")
Module System
Package vs Application vs Platform
Roc has three distinct module types:
# Application - Entry point with platform dependency
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}
# Package - Reusable library
package [List, Dict, Set] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}
# Platform - Provides I/O capabilities (advanced)
platform "my-platform"
requires {} { main : Task {} [] }
exposes [Stdout, File, Task]
packages {}
imports []
provides [mainForHost]
Exposing and Importing
# Module declaration with exports
interface MyModule
exposes [
User, # Type
createUser, # Function
updateUser, # Function
]
imports [
pf.Stdout,
pf.Task.{ Task },
]
# Define exports
User : {
name : Str,
age : U32,
}
createUser : Str, U32 -> User
createUser = \name, age -> { name, age }
updateUser : User, U32 -> User
updateUser = \user, newAge -> { user & age: newAge }
Import Patterns
# Import from platform
import pf.Stdout
import pf.Task exposing [Task]
# Import multiple from same module
import pf.File exposing [readUtf8, writeUtf8]
# Import with alias (not yet implemented, but planned)
# import pf.Stdout as Out
# Import from package
import List
import Dict exposing [Dict]
import Set
# Use imported items
main : Task {} []
main =
# Use qualified
List.map([1, 2, 3], \n -> n * 2)
# Use exposed directly
task = Task.ok({})
Module Organization
my-app/
├── main.roc # Application entry point
├── User.roc # User module
├── Post.roc # Post module
└── Utils/
├── Strings.roc # String utilities
└── Math.roc # Math utilities
# main.roc
app [main] {
pf: platform "..."
}
import pf.Stdout
import pf.Task exposing [Task]
import User
import Post
main : Task {} []
main =
user = User.create("Alice", 30)
post = Post.create(user, "Hello, World!")
Stdout.line!("Created post by \(User.getName(user))")
# User.roc
interface User
exposes [User, create, getName, getAge]
imports []
User : {
name : Str,
age : U32,
}
create : Str, U32 -> User
create = \name, age -> { name, age }
getName : User -> Str
getName = \user -> user.name
getAge : User -> U32
getAge = \user -> user.age
Visibility and Encapsulation
# Only exposed items are public
interface Counter
exposes [Counter, new, increment, getValue]
imports []
# Opaque type - internal structure hidden
Counter := { count : U32 }
# Public API
new : Counter
new = @Counter({ count: 0 })
increment : Counter -> Counter
increment = \@Counter({ count }) ->
@Counter({ count: count + 1 })
getValue : Counter -> U32
getValue = \@Counter({ count }) -> count
# Private helper (not exposed)
internalHelper : U32 -> U32
internalHelper = \n -> n * 2
Package Structure
# Package with multiple modules
package [
# Export types
User,
Post,
Comment,
# Export functions
createUser,
createPost,
addComment,
] {
pf: platform "..."
}
import User
import Post
import Comment
# Re-export from submodules
User : User.User
createUser : User.create
Post : Post.Post
createPost : Post.create
Comment : Comment.Comment
addComment : Comment.add
Platform Interface
Platforms define the interface between pure Roc code and host capabilities:
# Platform exposes capabilities
platform "basic-cli"
requires {} { main : Task {} [] }
exposes [
Stdout,
Stderr,
File,
Path,
Env,
Arg,
Task,
]
packages {}
imports []
provides [mainForHost]
# Application imports from platform
app [main] { pf: platform "..." }
import pf.Stdout
import pf.File
import pf.Task exposing [Task]
# Application provides the required main function
main : Task {} []
main =
content = File.readUtf8!("input.txt")
Stdout.line!(content)
Dependency Management
# Applications depend on platforms
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br"
}
# Packages depend on platforms (for types)
package [myLib] {
pf: platform "..."
}
# Platform URLs point to packages
# Format: https://host/path/to/archive.tar.br
# Hash in URL ensures integrity
Module Best Practices
# Interface - public API design
interface Database
exposes [
# Types
Connection,
QueryResult,
# Core functions
connect,
disconnect,
query,
# Helpers
mapRows,
]
imports [
pf.Task.{ Task },
]
# Clear separation of concerns
Connection := { host : Str, port : U16 } # Opaque
QueryResult : { rows : List (Dict Str Str) } # Transparent
# Type-driven design
connect : Str, U16 -> Task Connection [ConnectionFailed Str]
disconnect : Connection -> Task {} []
query : Connection, Str -> Task QueryResult [QueryFailed Str]
Common Module Patterns
# 1. Builder pattern with module
interface RequestBuilder
exposes [Request, new, withHeader, withBody, build]
imports []
Request := {
url : Str,
headers : Dict Str Str,
body : [Some Str, None],
}
new : Str -> Request
new = \url -> @Request({
url,
headers: Dict.empty({}),
body: None,
})
withHeader : Request, Str, Str -> Request
withHeader = \@Request(req), key, value ->
@Request({ req & headers: Dict.insert(req.headers, key, value) })
withBody : Request, Str -> Request
withBody = \@Request(req), body ->
@Request({ req & body: Some(body) })
# 2. Smart constructors
interface Email
exposes [Email, fromStr, toString]
imports []
Email := Str
fromStr : Str -> Result Email [InvalidEmail]
fromStr = \str ->
if Str.contains(str, "@") then
Ok(@Email(str))
else
Err(InvalidEmail)
toString : Email -> Str
toString = \@Email(str) -> str
# 3. Namespace-style modules
interface StringUtils
exposes [capitalize, reverse, isPalindrome]
imports []
capitalize : Str -> Str
capitalize = \str ->
when Str.toUtf8(str) is
[] -> ""
[first, .. as rest] ->
Str.fromUtf8([Str.toUpper(first)] |> List.concat(rest))
reverse : Str -> Str
reverse = \str ->
str
|> Str.toUtf8
|> List.reverse
|> Str.fromUtf8
isPalindrome : Str -> Bool
isPalindrome = \str ->
str == reverse(str)
Concurrency
Roc's concurrency model is Task-based and platform-provided. Unlike languages with built-in threading, Roc delegates all concurrent execution to the platform layer. For cross-language comparison, see patterns-concurrency-dev.
Task-Based Concurrency
# Task represents an effect that may run concurrently
# Type: Task ok err
# ok - Success type
# err - Error type
import pf.Task exposing [Task]
import pf.Stdout
import pf.File
# Sequential execution
main : Task {} []
main =
# Each step waits for previous
content1 = File.readUtf8!("file1.txt")
content2 = File.readUtf8!("file2.txt")
Stdout.line!("Read both files sequentially")
Platform-Provided Concurrency
Platforms may provide concurrent execution primitives:
# Platform might expose concurrent operations
import pf.Task exposing [Task, parallel]
import pf.File
# Hypothetical concurrent file reading
readBothFiles : Task (Str, Str) [FileErr]
readBothFiles =
# Platform handles concurrent execution
Task.parallel2(
File.readUtf8("file1.txt"),
File.readUtf8("file2.txt")
)
# Pattern: Platform provides concurrency, app stays pure
main : Task {} []
main =
(content1, content2) = readBothFiles!
Stdout.line!("Read files concurrently via platform")
No Built-In Threading
# Roc applications don't directly manage threads
# All concurrency is platform capability
# This is NOT possible in Roc:
# - spawn_thread()
# - async/await (no built-in)
# - goroutines
# - manual thread pools
# Instead: Platform provides concurrent primitives
# Application code remains pure and sequential
Task Composition
# Tasks compose like other values
import pf.Task exposing [Task]
import pf.Http
import pf.Stdout
# Chain tasks
fetchAndPrint : Str -> Task {} [HttpErr]
fetchAndPrint = \url ->
response = Http.get!(url)
Stdout.line!(response.body)
# Multiple independent tasks
fetchMultiple : List Str -> Task (List Str) [HttpErr]
fetchMultiple = \urls ->
# Platform may execute these concurrently
List.map(urls, \url ->
Http.get(url)
|> Task.map(\resp -> resp.body)
)
|> Task.sequence # Platform-provided
Error Handling in Concurrent Tasks
# Each Task carries its error type
import pf.Task exposing [Task]
# Task that may fail
riskyOperation : Task Str [NetworkErr, ParseErr]
riskyOperation =
data = fetchData! # May fail with NetworkErr
parsed = parseData!(data) # May fail with ParseErr
Task.ok(parsed)
# Handle errors
safeOperation : Task Str []
safeOperation =
when riskyOperation is
Ok(result) -> Task.ok(result)
Err(NetworkErr) -> Task.ok("Network error occurred")
Err(ParseErr) -> Task.ok("Parse error occurred")
Concurrency Patterns
# Pattern 1: Batch processing
processBatch : List Item -> Task (List Result) [ProcessErr]
processBatch = \items ->
# Platform may parallelize map
items
|> List.map(processItem)
|> Task.sequence
# Pattern 2: Timeout
withTimeout : Task a err, U64 -> Task a [Timeout, TaskErr err]
withTimeout = \task, ms ->
# Platform provides timeout mechanism
Task.timeout(task, ms)
# Pattern 3: Retry
retry : Task a err, U32 -> Task a err
retry = \task, attempts ->
when task is
Ok(result) -> Task.ok(result)
Err(e) if attempts > 0 -> retry(task, attempts - 1)
Err(e) -> Task.err(e)
Platform Responsibilities
Platforms handle:
- Thread pool management
- Work scheduling
- Concurrent I/O
- Synchronization primitives
- Event loops
Applications handle:
- Pure data transformations
- Task composition
- Error handling logic
- Business logic
# Clear separation
┌─────────────────────────────┐
│ Application (Pure Roc) │
│ │
│ • Task composition │
│ • Business logic │
│ • Data transformation │
└──────────────┬───────────────┘
│ Task interface
┌──────────────▼───────────────┐
│ Platform (Host + Roc API) │
│ │
│ • Thread management │
│ • Concurrent execution │
│ • I/O operations │
│ • Synchronization │
└──────────────────────────────┘
Current State and Future
# As of 2025, Roc's concurrency is evolving
# Current: Task-based, platform-specific
# Expected: Standardized concurrent primitives in platform APIs
# Watch for:
# - Task.parallel, Task.race
# - Structured concurrency helpers
# - Standard async patterns
# Note: This section reflects current design
# May evolve as language matures
Metaprogramming
Roc takes a minimalist approach to metaprogramming. Unlike languages with macro systems, Roc focuses on simplicity and relies on code generation tools outside the language. For cross-language comparison, see patterns-metaprogramming-dev.
No Built-In Macros
# Roc does NOT have:
# - Macro systems (like Rust, Elixir)
# - Decorators (like Python, TypeScript)
# - Annotations (like Java)
# - Template metaprogramming (like C++)
# - Code generation directives
# Philosophy: Keep the language simple
# Metaprogramming happens outside the language
Abilities as Alternative
Abilities provide some metaprogramming-like features through automatic derivation:
# Automatic implementations
User : {
name : Str,
age : U32,
role : [Admin, User, Guest],
}
# These are automatically derived:
# - Eq (structural equality)
# - Hash (for Dict keys)
# - Inspect (debug representation)
# - Encode/Decode (serialization)
user1 = { name: "Alice", age: 30, role: Admin }
user2 = { name: "Alice", age: 30, role: Admin }
# Eq works automatically
expect user1 == user2 # true
# Inspect works automatically
dbg(user1) # Shows structure
Code Generation Outside Roc
# External code generation tools
# Example: Generate Roc from schema
# schema.json → generate_roc.py → types.roc
# types.roc (generated)
# DO NOT EDIT - Generated from schema.json
User : {
name : Str,
email : Str,
age : U32,
}
Post : {
title : Str,
content : Str,
author : User,
}
Opaque Types for Encapsulation
# Opaque types provide controlled abstraction
interface UserId
exposes [UserId, fromU64, toU64, new]
imports []
# Internal representation hidden
UserId := U64
# Smart constructors control creation
new : -> UserId
new = @UserId(generateId())
fromU64 : U64 -> UserId
fromU64 = \id -> @UserId(id)
toU64 : UserId -> U64
toU64 = \@UserId(id) -> id
# Pattern unwrapping only in this module
# External code can't access internal U64
Type-Driven Development
Instead of metaprogramming, Roc encourages type-driven design:
# Phantom types for state machines
Request state : { url : Str, headers : Dict Str Str }
# States tracked in type system
[Unsent, Prepared, Sent]
buildRequest : Str -> Request [Unsent]
buildRequest = \url -> { url, headers: Dict.empty({}) }
prepare : Request [Unsent] -> Request [Prepared]
prepare = \req -> req # Type changes, value stays same
send : Request [Prepared] -> Task Response [HttpErr]
send = \req -> Http.send(req)
# Type system prevents:
# send(buildRequest("...")) # Error: can't send Unsent request
Workarounds for Common Metaprogramming Needs
# 1. Instead of derive macros → Use abilities
# Automatic: Eq, Hash, Inspect, Encode, Decode
# 2. Instead of decorators → Use higher-order functions
logged : (a -> b), Str -> (a -> b)
logged = \fn, name ->
\arg ->
dbg("Calling \(name)")
result = fn(arg)
dbg("Result: \(Inspect.toStr(result))")
result
myFunction : U32 -> U32
myFunction = \x -> x + 1
loggedFunction = logged(myFunction, "myFunction")
# 3. Instead of code generation → External tools
# Use build scripts, code generators, or platform-specific tools
# 4. Instead of reflection → Static typing
# Design APIs that don't need runtime inspection
Build-Time Tools
# Roc supports external build-time generation
# 1. Shell scripts
#!/bin/bash
# generate_types.sh
python3 codegen.py schema.json > generated/types.roc
# 2. Makefile
generated/types.roc: schema.json codegen.py
python3 codegen.py schema.json > generated/types.roc
# 3. Just tasks (recommended)
generate:
python3 codegen.py schema.json > generated/types.roc
# 4. Custom tools
roc-codegen --input schema.json --output generated/types.roc
Design Philosophy
Why no metaprogramming in Roc?
Pros of current approach:
✓ Simpler language to learn
✓ Easier to understand code
✓ No hidden complexity
✓ Better tooling support
✓ Faster compilation
✓ Clear separation of concerns
Cons:
✗ More boilerplate for repetitive code
✗ External tools needed for generation
✗ Less flexibility than macro systems
Trade-off: Simplicity over flexibility
Future Considerations
# As of 2025, Roc may evolve to include:
# - More powerful ability derivation
# - Build hooks in the package system
# - Standard code generation conventions
# Watch for:
# - Expanded automatic derivations
# - Platform-provided build tooling
# - Community code generation tools
# Note: This reflects current design philosophy
# May change as language matures and use cases emerge
Cross-Cutting Patterns
For language-agnostic patterns and cross-language translation guides, see:
patterns-concurrency-dev- Compare Roc's Task model with async/await, goroutines, and actorspatterns-metaprogramming-dev- Compare Roc's minimalist approach with macros, decorators, and codegenpatterns-serialization-dev- JSON, YAML encoding/decoding with Roc's Encode/Decode abilities
References
- Roc Tutorial
- Roc Examples
- Roc Abilities Documentation
- Platforms and Apps
- Roc FAQ
- Specialized skills:
lang-roc-patterns-dev,lang-roc-platform-dev