返回 Skill 列表
extension
分类: 开发与工程无需 API Key

lang-roc-dev

基础的Roc模式涵盖了平台/应用程序架构、记录、标签、模式匹配、功能和函数式惯用法。在编写Roc代码、理解平台模型或需要指导使用哪种专门的Roc技能时,请使用这些模式。这是Roc开发的入门点。

person作者: jakexiaohubgithub

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:

  • app header 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))

Zero and Default Values

Roc takes a distinctive approach to null/nil/undefined: it doesn't have them. Instead, Roc uses explicit tag unions to represent the presence or absence of values.

No Null or Nil

# Roc does NOT have:
# - null (like Java, JavaScript)
# - nil (like Ruby, Go)
# - None (like Python's None)
# - undefined (like JavaScript)

# Instead, absence is always explicit via tag unions

Optional Values with Tag Unions

# The idiomatic way to represent optional values
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"

Semantic Tag Names

Unlike a generic Maybe type, Roc encourages descriptive tag names:

# More descriptive than Maybe:
artist : [Loading, Loaded Artist]           # Still loading vs loaded
email : [Unspecified, Specified Str]        # Never provided vs provided
result : [Pending, Completed Data, Failed]  # State machine

# Each tells you WHY the value might be absent

Zero Values for Built-in Types

# Empty/zero values for collections
emptyList : List a
emptyList = []

emptyDict : Dict k v
emptyDict = Dict.empty({})

emptySet : Set a
emptySet = Set.empty({})

emptyStr : Str
emptyStr = ""

# Numbers default to 0 when type is known
zero : I64
zero = 0

Default Values in Records

Roc supports default values for record fields using the ?? syntax:

# Function with default field values
table : { height : U64, width : U64, title ?? Str, description ?? Str } -> Table
table = \{ height, width, title ?? "oak", description ?? "a wooden table" } ->
    # title defaults to "oak" if not provided
    # description defaults to "a wooden table" if not provided
    buildTable(height, width, title, description)

# Calling with defaults
table({ height: 100, width: 50 })  # uses default title and description
table({ height: 100, width: 50, title: "maple" })  # overrides title only

Initialization Patterns

# Builder pattern for complex initialization
Config : {
    host : Str,
    port : U16,
    timeout : U64,
    retries : U8,
}

defaultConfig : Config
defaultConfig = {
    host: "localhost",
    port: 8080,
    timeout: 30000,
    retries: 3,
}

# Create config with overrides
myConfig = { defaultConfig & host: "api.example.com", port: 443 }

Safe Access Patterns

# List access returns Result (never crashes)
when List.get(myList, 0) is
    Ok(first) -> "First element: \(first)"
    Err(OutOfBounds) -> "List was empty"

# Dict access returns Result
when Dict.get(myDict, "key") is
    Ok(value) -> value
    Err(KeyNotFound) -> defaultValue

# With default fallback
value = Dict.get(myDict, "key") |> Result.withDefault("default")

Serialization

Roc provides serialization through the Encoding and Decoding abilities, which work with format-specific encoders/decoders like JSON.

Encoding (Serialization)

# Basic JSON encoding
import json.Json

fruitBasket : List (Str, U32)
fruitBasket = [
    ("Apples", 10),
    ("Bananas", 12),
    ("Oranges", 5),
]

# Encode to bytes
bytes : List U8
bytes = Encode.toBytes(fruitBasket, Json.utf8)
# Result: [["Apples",10],["Bananas",12],["Oranges",5]]

Decoding (Deserialization)

import json.Json

bytes : List U8
bytes = "[10, 20, 30]" |> Str.toUtf8

# Decode from bytes
result : Result (List U32) _
result = Decode.fromBytes(bytes, Json.utf8)

when result is
    Ok(numbers) -> "Got: \(Inspect.toStr(numbers))"
    Err(_) -> "Failed to decode"

Records and Tags

# Records automatically derive Encoding/Decoding
User : {
    name : Str,
    age : U32,
    role : [Admin, User, Guest],
}

user : User
user = { name: "Alice", age: 30, role: Admin }

# Encode record to JSON
jsonBytes = Encode.toBytes(user, Json.utf8)
# {"name":"Alice","age":30,"role":"Admin"}

# Decode back to record
decoded : Result User _
decoded = Decode.fromBytes(jsonBytes, Json.utf8)

Partial Decoding

# Decode with validation
parseUser : List U8 -> Result User [InvalidJson, MissingField Str]
parseUser = \bytes ->
    when Decode.fromBytes(bytes, Json.utf8) is
        Ok(user) -> Ok(user)
        Err(_) -> Err(InvalidJson)

Custom Encoding for Opaque Types

# Opaque types need explicit ability derivation
Email := Str implements [Encoding, Decoding]

# Or custom implementation
UserId := U64 implements [
    Encoding { toEncoder: userIdEncoder },
    Decoding { decoder: userIdDecoder },
]

userIdEncoder : UserId -> Encoder fmt where fmt implements EncoderFormatting
userIdEncoder = \@UserId(id) ->
    Encode.u64(id)

userIdDecoder : Decoder UserId fmt where fmt implements DecoderFormatting
userIdDecoder =
    Decode.u64 |> Decode.map(@UserId)

Encoding Ability Definition

# From Encode.roc
Encoding implements
    toEncoder : val -> Encoder fmt
        where val implements Encoding, fmt implements EncoderFormatting

# From Decode.roc
Decoding implements
    decoder : Decoder val fmt
        where val implements Decoding, fmt implements DecoderFormatting

Limitations

# Note: Dict encoding/decoding has limited support
# See https://github.com/roc-lang/roc/issues/5294

# Functions cannot be encoded (they don't implement Encoding)
# This won't work:
# myFunc = \x -> x + 1
# Encode.toBytes(myFunc, Json.utf8)  # Error!

Build System

Roc's build system uses the roc CLI and a platform-based architecture. Understanding this is essential for project setup and development workflow.

CLI Commands

# Run application directly (compiles and runs)
roc main.roc

# Build optimized executable
roc build main.roc
roc build main.roc --optimize  # maximum optimization

# Development mode (faster compilation, includes dbg output)
roc dev main.roc

# Run tests (executes all top-level expect expressions)
roc test main.roc

# Start REPL
roc repl

# Format code
roc format main.roc
roc format .  # format all .roc files

# Check types without building
roc check main.roc

# Generate documentation
roc docs package/*.roc

Application Structure

# main.roc - Application header
app [main!] {
    pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br"
}

import pf.Stdout

main! = |_args|
    Stdout.line!("Hello, World!")

Platform URLs

# Platform URL format
# https://host/path/HASH.tar.br
#                  ^^^^
#                  BLAKE3 hash for integrity verification

# Common platforms
pf: platform "https://github.com/roc-lang/basic-cli/..."      # CLI apps
pf: platform "https://github.com/roc-lang/basic-webserver/..." # Web servers

# Local platform (for development)
pf: platform "../my-platform/main.roc"

Package Structure

# package/main.roc - Package header
package [
    MyModule,
    AnotherModule,
] {
    json: "https://github.com/lukewilliamboswell/roc-json/..."
}

Multi-Module Projects

my-app/
├── main.roc           # Application entry point
├── User.roc           # User module
├── Database.roc       # Database module
└── Utils/
    ├── Strings.roc    # String utilities
    └── Math.roc       # Math utilities
# main.roc
app [main!] { pf: platform "..." }

import pf.Stdout
import User
import Database
import Utils.Strings

main! = |_args|
    user = User.create("Alice", 30)
    Stdout.line!(User.getName(user))

Building for Different Targets

# Default: native executable
roc build main.roc

# WebAssembly (when platform supports it)
roc build --target wasm32 main.roc

# Specific output name
roc build main.roc --output myapp

Package Management

# Packages are specified by URL with hash
app [main!] {
    pf: platform "...",
    json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.10.0/KbIfTNbxShRX1A1FgXei1SpO5Jn8sgP6HP6PXbi-xyA.tar.br",
}

# Packages are cached locally after first download
# Cache location: ~/.cache/roc (Unix) or %APPDATA%\Roc (Windows)

Build Distribution

# Create distributable package
roc build --bundle .tar.br package/main.roc

# The output includes the BLAKE3 hash for the URL

Common Build Issues

# Clear cache if experiencing issues
rm -rf ~/.cache/roc

# Verify platform/package integrity
# (automatic - hash mismatch will fail with error)

# Check for type errors without full build
roc check main.roc

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 actors
  • patterns-metaprogramming-dev - Compare Roc's minimalist approach with macros, decorators, and codegen
  • patterns-serialization-dev - JSON, YAML encoding/decoding with Roc's Encode/Decode abilities

References