| name | lang-roc-library-dev |
| description | Roc-specific library and platform development patterns. Use when creating reusable Roc packages, developing custom platforms, organizing package modules, exposing public APIs, managing platform interfaces, or publishing packages. Extends meta-library-dev with Roc's unique platform system and module patterns. |
| created | 2025-12-28T22:00 |
| updated | 2025-12-28T22:00 |
| tags | lang, roc, library, dev |
| source | arustydev/ai |
Roc Library Development
Roc-specific patterns for library package and platform development. This skill extends meta-library-dev with Roc's unique platform architecture, package system, and module organization.
This Skill Extends
meta-library-dev- Foundational library patterns (API design, versioning, testing strategies)
For general concepts like semantic versioning, module organization principles, and testing pyramids, see the meta-skill first.
This Skill Adds
- Roc platform system: Platform development, app/platform separation, I/O interfaces
- Package organization: Module structure, package declarations, visibility patterns
- Roc idioms: Pure functional patterns, platform interfaces, type inference
- Roc ecosystem: Standard library modules, builtin modules, platform conventions
This Skill Does NOT Cover
- General library patterns - see
meta-library-dev - Roc application development - focus is on reusable packages/platforms
- Roc-specific syntax basics - see language documentation
- CLI application development - see CLI-specific skills
Overview
Roc has a unique architecture that separates applications from platforms:
┌─────────────────────────────────────────────────────────────────┐
│ Roc Package Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ │
│ │ Application │ (your code) │
│ │ (.roc) │ │
│ └───────┬───────┘ │
│ │ │
│ │ imports │
│ ▼ │
│ ┌───────────────┐ │
│ │ Package │ (reusable modules) │
│ │ package {} │ │
│ └───────┬───────┘ │
│ │ │
│ │ depends on │
│ ▼ │
│ ┌───────────────┐ ┌──────────────┐ │
│ │ Platform │────────▶│ Runtime │ │
│ │ platform {} │ │ (Rust, C) │ │
│ └───────────────┘ └──────────────┘ │
│ │ │
│ └─ Provides I/O primitives (no I/O in stdlib) │
│ │
└─────────────────────────────────────────────────────────────────┘
Key Concepts:
| Concept | Purpose | I/O Allowed |
|---|---|---|
| Package | Reusable modules (pure functions/types) | No |
| Platform | Runtime + I/O primitives | Yes |
| App | Executable application | Yes (via platform) |
| Module | Single .roc file with exports | No (unless platform) |
This skill focuses on creating Packages and Platforms for reuse.
Quick Reference
File Types
| Type | Declaration | Purpose |
|---|---|---|
app |
app [main] { ... } |
Executable application |
package |
package [Module1, Module2] { ... } |
Reusable package |
platform |
platform "name" requires {} exposes {} packages {} ... |
Platform for apps |
module |
module [export1, export2] |
Single module file |
Common Commands
| Command | Description |
|---|---|
roc check |
Type-check without building |
roc build |
Build executable |
roc test |
Run tests |
roc format |
Format code |
roc docs |
Generate documentation |
Package Development
Package Structure
my-package/
├── package/
│ ├── main.roc # Package entry point
│ ├── Parser.roc # Module files
│ ├── Types.roc
│ └── Internal.roc
├── examples/
│ └── basic.roc # Example apps
├── platform/ # Optional: custom platform
│ └── main.roc
└── README.md
Package Declaration
package/main.roc:
package [
# Public modules (exposed to users)
Parser,
Types,
# Can also expose specific values
parseConfig,
defaultConfig,
]
Key Principles:
- Only expose what users need (minimal public API)
- Keep implementation modules private
- Re-export common types/functions at package level
- Use descriptive module names
Module Structure
package/Parser.roc:
module [
# Public API
parse,
parseWithConfig,
Config,
ParseError,
]
import Types exposing [Document, Metadata]
import Internal # Private import, not exposed
## Parse a document string.
##
## ```roc
## result = parse "# Title\nContent"
## expect result == Ok { title: "Title", content: "Content" }
## ```
parse : Str -> Result Document ParseError
parse = \input ->
parseWithConfig input defaultConfig
## Configuration for parsing.
Config : {
strict : Bool,
preserveWhitespace : Bool,
}
defaultConfig : Config
defaultConfig = {
strict: Bool.false,
preserveWhitespace: Bool.false,
}
## Parse with custom configuration.
parseWithConfig : Str, Config -> Result Document ParseError
parseWithConfig = \input, config ->
# Implementation
# ...
Visibility Patterns
Module-level visibility:
module [
# Public: Anyone importing this module can use these
publicFunction,
PublicType,
]
# Private: Only visible within this module
privateHelper : Str -> Str
privateHelper = \x -> x
# Public functions can call private helpers
publicFunction : Str -> Str
publicFunction = \input ->
privateHelper input
Package-level visibility:
# main.roc
package [
# These modules are visible to package users
Parser,
Types,
]
# Internal is NOT in package exports = private to package
import Internal
Platform Development
Platform Structure
my-platform/
├── platform/
│ └── main.roc # Platform declaration
├── host/
│ ├── Cargo.toml # Rust host implementation
│ └── src/
│ ├── lib.rs
│ └── main.rs
└── examples/
└── hello.roc
Platform Declaration
platform/main.roc:
platform "cli"
requires { Model } { main : Task Model [] }
exposes [
Task,
Stdout,
Stdin,
File,
Path,
Env,
]
packages {}
imports []
provides [mainForHost]
import Task
import Internal.Task
mainForHost : Task.Task Model [] as Fx
mainForHost = main
Key Components:
| Section | Purpose |
|---|---|
requires |
What apps must provide (e.g., main function) |
exposes |
Modules the platform provides to apps |
packages |
Dependencies |
imports |
Internal platform imports |
provides |
Entry point for host |
Platform Interface Design
Good platform design:
- Clear separation of concerns:
# Platform exposes pure types and Task-based I/O
module [
# Pure types
Request,
Response,
# Effectful operations
serveHttp : Config, (Request -> Task Response []) -> Task {} []
]
- Type-safe I/O:
# File module
read : Path -> Task Str [FileNotFound, PermissionDenied]
write : Path, Str -> Task {} [PermissionDenied, DiskFull]
# Stdin module
line : Task Str [EndOfInput]
- Composable effects:
# Tasks can be chained
program : Task {} []
program =
path <- Task.await (Env.var "CONFIG_PATH")
contents <- Task.await (File.read path)
config <- Task.await (parseConfig contents)
runServer config
Module Organization Patterns
Single Responsibility Modules
# Types.roc - Just type definitions
module [Config, Document, Metadata]
Config : {
timeout : U64,
retries : U8,
}
Document : {
title : Str,
content : Str,
metadata : Metadata,
}
Metadata : {
author : Str,
created : U64,
}
Extension Modules
# ConfigExt.roc - Operations on Config
module [default, withTimeout, validate]
import Types exposing [Config]
default : Config
default = {
timeout: 30_000,
retries: 3,
}
withTimeout : Config, U64 -> Config
withTimeout = \config, ms -> { config & timeout: ms }
validate : Config -> Result Config [InvalidTimeout, InvalidRetries]
validate = \config ->
if config.timeout == 0 then
Err InvalidTimeout
else if config.retries > 10 then
Err InvalidRetries
else
Ok config
Re-export Pattern
package/main.roc:
package [
# Re-export commonly used types from multiple modules
Config,
Document,
# Re-export main functions
parse,
parseWithConfig,
# Expose modules for advanced usage
Parser,
Types,
]
# Import everything needed for re-exports
import Parser exposing [parse, parseWithConfig]
import Types exposing [Config, Document]
Type Design Patterns
Opaque Types
module [UserId, fromU64, toU64]
# Opaque: internals hidden from users
UserId := U64
## Create a UserId from a U64.
fromU64 : U64 -> UserId
fromU64 = @UserId
## Extract the U64 from a UserId.
toU64 : UserId -> U64
toU64 = \@UserId id -> id
Tagged Unions for Errors
module [ParseError, toStr]
ParseError : [
InvalidSyntax Str,
UnexpectedEnd,
UnknownTag Str,
]
toStr : ParseError -> Str
toStr = \err ->
when err is
InvalidSyntax msg -> "Invalid syntax: $(msg)"
UnexpectedEnd -> "Unexpected end of input"
UnknownTag tag -> "Unknown tag: $(tag)"
Builder Pattern (Records with Defaults)
module [Config, default, build, withTimeout, withRetries]
Config : {
timeout : U64,
retries : U8,
strict : Bool,
}
default : Config
default = {
timeout: 30_000,
retries: 3,
strict: Bool.false,
}
build : {} -> Config
build = \{} -> default
withTimeout : Config, U64 -> Config
withTimeout = \config, ms -> { config & timeout: ms }
withRetries : Config, U8 -> Config
withRetries = \config, n -> { config & retries: n }
# Usage:
# config = build {} |> withTimeout 60_000 |> withRetries 5
Testing Patterns
Expect Blocks
## Parse a valid document.
parse : Str -> Result Document ParseError
parse = \input ->
# Implementation
# ...
# Test cases using expect
expect
result = parse "# Title\nContent"
result == Ok { title: "Title", content: "Content" }
expect
result = parse ""
result == Err UnexpectedEnd
expect
result = parse "# Title\nContent"
Result.isOk result
Test Modules
tests/ParserTests.roc:
module []
import Parser exposing [parse, ParseError]
expect
# Happy path
input = "# Hello\nWorld"
result = parse input
result == Ok { title: "Hello", content: "World" }
expect
# Error case
result = parse ""
result == Err UnexpectedEnd
expect
# Edge case
input = Str.repeat "a" 10_000
result = parse input
Result.isOk result
Property-Based Testing Pattern
# Generate test cases
expectAll = \cases ->
List.all cases \case ->
expected = case.expected
actual = parse case.input
actual == expected
expect
cases = [
{ input: "# A\nB", expected: Ok { title: "A", content: "B" } },
{ input: "", expected: Err UnexpectedEnd },
{ input: "###", expected: Err (InvalidSyntax "Too many #") },
]
expectAll cases
Builtin Modules
Roc provides several builtin modules auto-imported into every file:
| Module | Purpose | Common Functions |
|---|---|---|
Str |
String operations | concat, split, trim, to_utf8 |
Num |
Numeric operations | add, sub, is_even, abs |
List |
List operations | map, filter, fold_left, len |
Dict |
Dictionary/Map | insert, get, keys, values |
Set |
Set operations | insert, contains, union |
Bool |
Boolean operations | true, false, not |
Result |
Result type | Ok, Err, map, try |
Note: The standard library provides NO I/O operations. All I/O comes from platforms.
Breaking Changes (2025)
Snake Case Migration
Builtins are migrating from camelCase to snake_case:
# Old (deprecated)
Str.joinWith
List.keepIf
# New (current)
Str.join_with
List.keep_if
Action: Use new snake_case names in all new packages.
Task Deprecation (Purity Inference)
Task is deprecated in favor of purity inference:
# Old pattern
readConfig : Task Config [FileNotFound]
readConfig =
path <- Task.await (Env.var "CONFIG")
contents <- Task.await (File.read path)
Task.ok (parse contents)
# New pattern (purity inferred)
readConfig : Config ![FileNotFound, EnvVarNotSet]
readConfig =
path = Env.var! "CONFIG"
contents = File.read! path
parse contents
Action: For new platforms, use purity inference. For compatibility, check platform docs.
Publishing Packages
Pre-publish Checklist
-
roc checkpasses on all modules -
roc testpasses -
roc formatapplied to all files - Documentation comments on all public functions
- Examples in
examples/directory - README.md with usage examples
- Version follows semantic versioning
- No dependencies on unreleased packages
- License file included
Package Metadata
Include in README.md:
# My Package
Brief description of what this package does.
## Installation
How to include this package in a Roc project.
## Quick Start
\```roc
import MyPackage exposing [parse]
main =
result = parse "input"
# ...
\```
## Modules
- `Parser` - Main parsing logic
- `Types` - Type definitions
- `Config` - Configuration utilities
## Examples
See `examples/` directory.
## License
MIT
Common Patterns
Error Handling
# Return Result for operations that can fail
parse : Str -> Result Document [InvalidSyntax Str, UnexpectedEnd]
parse = \input ->
if Str.is_empty input then
Err UnexpectedEnd
else
# Parse logic
Ok document
# Chain Results
processDocument : Str -> Result ProcessedDoc [ParseError, ValidationError]
processDocument = \input ->
parsed <- Result.try (parse input |> Result.map_err ParseError)
validated <- Result.try (validate parsed |> Result.map_err ValidationError)
Ok (process validated)
Polymorphic Functions
# Generic over any type
identity : a -> a
identity = \x -> x
# Constrained generics (via abilities - future feature)
compare : a, a -> [LT, EQ, GT] where a implements Ord
Pipeline Pattern
# Use |> for readable data transformations
processData : List Str -> List Str
processData = \items ->
items
|> List.map Str.trim
|> List.keep_if (\s -> !(Str.is_empty s))
|> List.map Str.to_lowercase
|> List.sort_with Str.compare
Anti-Patterns
1. Leaking Implementation Details
# Bad: Exposing internal structure
module [Config, InternalState]
# Good: Only expose what's needed
module [Config, fromStr, toStr]
2. Overly Large Modules
# Bad: Single module with 1000+ lines
# Good: Split into focused modules
# - Types.roc (types)
# - Parser.roc (parsing)
# - Validator.roc (validation)
# - main.roc (re-exports)
3. Missing Documentation
# Bad: No docs
parse : Str -> Result Document ParseError
# Good: Documented
## Parse a document from a string.
##
## Returns a Document on success, or ParseError if the input is invalid.
##
## ```roc
## result = parse "# Title"
## expect result == Ok { title: "Title", ... }
## ```
parse : Str -> Result Document ParseError
4. Platform Doing Too Much
# Bad: Platform provides complex business logic
module [
readUserFromDb,
validateUser,
sendEmail,
]
# Good: Platform provides primitives
module [
dbQuery,
httpRequest,
# Let apps compose these into business logic
]
Troubleshooting
Module Not Found
Symptom: Module 'MyModule' not found
Causes & Fixes:
| Cause | Fix |
|---|---|
| Module not in package exports | Add to package [...] list |
| File name doesn't match module | Rename file to match (e.g., MyModule.roc) |
| Import path incorrect | Use correct relative/absolute path |
Type Inference Failures
Symptom: Cannot infer type for this expression
Fixes:
- Add explicit type annotation
- Provide more context (often happens in empty lists)
- Check for ambiguous number types (add
U64,I32, etc.)
# Problem: Ambiguous type
nums = []
# Fix: Provide type context
nums : List U64
nums = []
# Or use in context
nums = List.range { start: At 1, end: At 10 }
Circular Dependencies
Symptom: Circular dependency detected
Fix: Restructure modules
# Before (circular):
# A.roc imports B.roc
# B.roc imports A.roc
# After: Extract shared types
# Types.roc (shared types)
# A.roc imports Types
# B.roc imports Types
Platform Examples
Basic CLI Platform
platform "basic-cli"
requires { Model } { main : Task Model [] }
exposes [
Task,
Stdout,
Stdin,
File,
Path,
Env,
Arg,
]
packages {}
imports []
provides [mainForHost]
mainForHost : Task Model [] as Fx
mainForHost = main
Web Platform
platform "basic-webserver"
requires {} { main : Request -> Task Response [] }
exposes [
Request,
Response,
Task,
Stdout,
]
packages {}
imports []
provides [mainForHost]
Request : {
method : [GET, POST, PUT, DELETE],
url : Str,
headers : Dict Str Str,
body : List U8,
}
Response : {
status : U16,
headers : Dict Str Str,
body : List U8,
}
References
meta-library-dev- Foundational library patterns- Roc Tutorial
- Roc Platforms
- GitHub - roc-lang/roc
- basic-cli Platform
- basic-webserver Platform