Roc — Reference
Source: https://www.roc-lang.org/
Roc
- Created: 2018 by Richard Feldman; first public alpha 2024
- Latest stable: alpha4-rolling (initial alpha4 cut 2024-08-26; rolling tag updated continuously). No 1.0, no numbered semver release yet
- Status: Alpha. Quote from the homepage: “It doesn’t even have a numbered release yet, just alpha builds!” Breaking changes between alphas are expected. Compiler is undergoing a Rust → Zig rewrite (the “zig compiler” — current alpha4 docs target the Rust compiler; the Zig one has a different syntax for some constructs)
- Paradigms: Pure functional, ML-family
- Typing: Static, strong, 100% type inference (Hindley-Milner with extensions for tags, records, abilities). Annotations always optional
- Memory: Automatic reference counting with compiler-driven uniqueness/borrow optimization (in-place mutation when refcount=1). No GC, no manual
free - Compilation: AOT to native machine code (LLVM via Rust compiler; direct codegen in the Zig rewrite) and to WebAssembly. Designed for “build fast, run fast”
- Primary domains: CLI tools, web servers, scripts, embedded as a scripting language in larger apps via the platform/host model
- Official docs: https://www.roc-lang.org/ · Tutorial: https://www.roc-lang.org/tutorial · Builtins: https://www.roc-lang.org/builtins
At a glance
Roc is Richard Feldman’s “what if Elm targeted servers and CLIs” — a small, friendly ML descendant with no runtime exceptions, total type inference, and a unique platform/application split that pushes effects entirely outside the application code. Apps are pure; platforms (written in Rust/Zig/C) provide the effects. The language is in active alpha; the compiler itself is being rewritten in Zig (replacing the original Rust implementation) for speed and contributor accessibility. Don’t ship Roc to production yet, but it’s mature enough for small CLIs and personal projects.
Getting started
Install: No prebuilt for every platform yet. Two compilers exist:
- Rust compiler (stable for alpha4): download a build from https://github.com/roc-lang/roc/releases tagged
alpha4-rolling. Linux x86_64 and macOS arm64/x86_64 are the best supported. Extract and putrocon PATH. - Zig compiler (in development):
git clone https://github.com/roc-lang/roc && cd roc && zig build. Syntax is in flux — follow the in-tree docs not the web tutorial.
roc version # confirm install
roc repl # interactiveHello world (hello.roc):
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/.../basic-cli.tar.br" }
import pf.Stdout
main! = |_args|
Stdout.line!("Hello, World!")The first line picks the platform, which determines what effects you can do. Run: roc dev hello.roc (dev mode, fast compile) or roc build hello.roc (release).
Project layout: Single-file scripts are common. For multi-file:
myapp/
├── main.roc # app header at top
└── Util.roc # module Util exposing [...]
The app header lists the platform URL (content-addressed, downloaded once into ~/.cache/roc).
Build/package tool: roc is the only tool. Sub-commands: roc dev (build+run debug), roc build (optimised), roc test (run expects), roc check (type-check only), roc format (canonical formatter), roc repl, roc docs (gen docs), roc glue (codegen for platform authors).
No central registry. Packages and platforms are content-addressed URLs to .tar.br archives (Brotli-compressed). Hashes pinned in the app/module header guarantee reproducibility.
REPL: roc repl — accepts expressions, prints type + value ("hello" : Str).
Basics
Types & literals:
Bool(Bool.true,Bool.false)- Numbers: polymorphic by default. Concrete types
U8 U16 U32 U64 U128,I8…I128,F32 F64,Dec(fixed-point decimal — money-safe) Str(UTF-8, immutable)List a(homogeneous),Dict k v,Set a- Records
{ name : Str, age : U64 } - Tags
Red,Green,Some 1,Err NotFound— open or closed unions inferred by use
Variables (definitions): = defines an immutable value. No reassignment ever. Names are snake_case:
greeting = "Hello"
counts = { birds: 5, iguanas: 7 }|x| x + 1 is a lambda. Type annotations are optional but document intent:
add : I64, I64 -> I64
add = |a, b| a + bControl flow: Everything is an expression.
-
if cond then a else b(must haveelse) -
when value isfor pattern matching:when stoplight is Red -> "stop" Yellow -> "slow" Green -> "go" -
Pattern matching is exhaustive — compiler errors on missing cases. Tags +
whengive you Roc’s sum types.
Functions: Pipe-bar lambdas: |a, b| a + b. First-class. Currying: not automatic — Roc functions take all args at once. Compose with |>:
result = numbers |> List.map(|n| n * 2) |> List.sumStrings: Str is UTF-8. Interpolation: "Hello, $(name)!". Multiline """...""". No char type — single-character strings.
Collections: List, Dict, Set in the standard Builtin. Lists are persistent immutable values, but the compiler rewrites in place when the refcount allows it. Pattern match with [head, .. as tail], [], [a, b, c].
Intermediate
Type system depth:
- Type inference is total — annotations are documentation, not requirement
- Open vs closed records:
{ name : Str }*accepts records with extra fields;{ name : Str }is closed - Tags as anonymous unions:
[Red, Green, Blue]is a closed tag union;[Red, Green]*is open. Tags can carry payloads:[Ok a, Err e]is the standardResult - Type aliases:
Color : [Red, Green, Blue] - Opaque types:
Username := Str— only the defining module can construct/deconstruct. Use@Username "alice"inside, opaque outside - Abilities (Roc’s typeclass-equivalent):
Encoding,Decoding,Hash,Eq,Inspect— derive automatically withimplementsclauses on opaque types:Username := Str implements [Eq, Hash] - Type variables: lowercase identifiers (
a,b) — no'aML-style ticks
Modules: module [exposed1, exposed2] header lists exports. Imports: import Util then call Util.foo. Built-ins (Str, List, Dict, Result, Num, Bool, Set, Decode, Encode, Inspect) are auto-imported.
Error handling — no runtime exceptions, ever:
Result a ewithOk a/Err etags?postfix unwraps aResult, propagating theErr(early-return). Massive build-speed improvement landed in alpha4??provides a default:parse(input) ?? 0dbg exprprints to stderr (file:line included), then returns the value — for debugging onlycrash "msg"is the explicit panic; only used for “this is logically impossible”
Concurrency: Not built into the language. Concurrency is the platform’s job. The basic-webserver platform uses Tokio under the hood and exposes a request-handler model; the basic-cli platform is single-threaded async via Task! (the ! suffix marks effectful functions). The application code stays pure; the platform supplies the runtime.
I/O — the platform model:
- Pure Roc code cannot do I/O directly. Effectful functions are marked with
!suffix (Stdout.line!,File.read_utf8!) - The
appheader binds a platform that exports an effects API (e.g.pf.Stdout,pf.File,pf.Env,pf.Http) - The platform itself is C/Rust/Zig code that implements the effect handlers — the equivalent of an algebraic effect handler
Stdlib highlights: Str (UTF-8 ops + parsing), List (~100 functions), Dict, Set, Result, Num (numeric conversions, math), Decode/Encode (ability-based serialization, used by JSON/CBOR/MsgPack libs), Inspect (debug formatting, like Rust’s Debug), Hash, Bool. Standard library is intentionally tiny — domain functionality lives in packages or the platform.
Advanced
Memory model — reference counting + uniqueness:
- Every heap value carries a refcount; decremented at scope exit
- Compiler does opportunistic in-place mutation: when it can prove a refcount is 1 (uniqueness), mutating ops on
List/Dicthappen in place. SoList.set list 0 99is O(1) whenlistis unique, O(n) copy otherwise - No GC pauses, no global heap walk
- For numeric/scalar arrays this is competitive with mutable-by-default languages while keeping immutable semantics
- “Persistent without the cost when you don’t need persistence”
Concurrency deep dive: Pure Roc has no concurrency primitives. Platforms expose what they expose:
basic-cli:Task!for sequential async I/O (single-threaded)basic-webserver: requests are independent Roc evaluations (effectively share-nothing parallelism via Tokio worker threads)- Custom platforms can expose channels, actors, anything — but it’s a platform-author decision
FFI: Roc apps cannot directly call C. Platforms can — they’re written in Rust/Zig/C and link C libs natively, then expose a Roc API. This is intentional: Roc apps stay pure-functional and portable; platform authors handle the unsafe edge.
Reflection: No runtime reflection. The Inspect, Encoding, Decoding abilities give you derive-based introspection at compile time only. dbg uses Inspect to print values.
Performance tools: roc build --optimize enables Rust/LLVM full opts. roc build --time prints phase timings. Profiling = use the platform’s profiler (e.g. for Rust-host platforms, perf/samply/Instruments works on the final binary). The Zig-rewrite compiler emits direct codegen and is dramatically faster than the Rust LLVM path for dev builds.
God mode
Opaque types + abilities: Roc’s typeclass story. Define:
Username := Str implements [Eq, Hash, Inspect]Outside the defining module, Username is a black box; inside, @Username s constructs and \@Username s -> s destructures. Abilities (Eq, Hash, Encoding, Decoding, Inspect) are derived automatically. Custom abilities: Encoding implements toEncoder : a -> Encoder where a implements Encoding.
Platform/application split — the BIG idea:
- An application is pure Roc that targets a chosen platform interface
- A platform is a project (in Rust/Zig/C) that implements the effect handlers AND defines the API surface the app sees (the types of effects available)
- This is algebraic effect handlers in production: an app is portable across platforms that implement the same interface; a platform can sandbox effects (e.g. a “just file I/O, no network” platform)
- Existing platforms:
basic-cli,basic-webserver,roc-ray(game),false-interpreter(a sample), more in https://github.com/roc-lang/
Open vs closed unions: [Red, Green] is closed; [Red, Green]a is open and accepts more tags (the a is a row variable). Functions can constrain inputs to a closed set while staying generic over additions to outputs. This is structural ML row-polymorphism done well.
Tags carry payloads, are anonymous: No data Color = Red | Green declaration needed. Red is just a value; Some 5 is a value; Err NotFound is a value. Type inference figures out the union from usage.
Reference-count-based mutation: Profile a Roc program and you’ll find idiomatic immutable code generates mutating loops. The compiler emits if refcount == 1 { in_place_set(...) } else { copy_then_set(...) } automatically.
Roc compiler in Zig (post-Rust rewrite): Original compiler in Rust + LLVM. Late 2024–2026 the team is rewriting in Zig for: dramatically faster dev builds (no LLVM frontend overhead in dev mode), simpler contributor onboarding, smaller compiler binary. Some syntax differs (the Zig-compiler tutorial is in-tree); web tutorial currently targets the Rust compiler.
No-runtime-exception philosophy: The only way a Roc program crashes is crash "msg" (explicit). All errors are values. There is no division-by-zero exception (a / b returns Result), no array-out-of-bounds exception (List.get returns Result), no null. This is enforced by the type system, not by convention.
Format on save, lint on commit: roc format is canonical and not configurable. The Zig rewrite includes a more capable static linter alongside the formatter.
Idioms & style
- Naming:
snake_casefor values/functions,PascalCasefor types and tags,snake_case!for effectful functions (the!is part of the name, signaling effects) - Formatter:
roc format— non-configurable, canonical; run on save - Linter: Built into the Zig compiler; in alpha4 (Rust compiler) the formatter handles light lint-style suggestions
- Idiomatic patterns: Use
?to threadResults; use??for fallback; prefer pipelines (|>) over nested calls; keep apps pure and push effects through the platform; use opaque types liberally to enforce module invariants - Avoid:
crash, except for “logically impossible” branches (e.g. just-checked invariant); deeply nestedwhen; non-exhaustive match patterns (the compiler will reject them anyway) - Expert review focus: correct ability constraints on generic functions, opaque type boundaries (don’t leak the
@Fooconstructor), platform choice (does the app need network? thenbasic-cliis wrong), effect propagation (every!function call must be in an!context)
Ecosystem
Mostly nascent — alpha-stage language.
| Domain | Project |
|---|---|
| CLI platform | basic-cli (default for scripts) |
| Web server | basic-webserver (Tokio + Hyper underneath) |
| Game | roc-ray (raylib bindings as a platform) |
| JSON | roc-json (third-party, ability-based) |
| HTTP client | exposed by basic-webserver and basic-cli platforms |
| Test | Built-in expect blocks; roc test runs them |
| Docs | roc docs generates HTML; hosted at https://www.roc-lang.org/packages/ |
Notable users: Vendr (Richard Feldman’s company, has used Roc in production tooling), various indie/hobbyist CLIs. No mainstream commercial deployment yet — language status precludes it.
Gotchas
- Alpha = breaking changes — pin a specific
alpha4-rollingbuild, expect to update code between alphas - Two compilers exist (Rust and Zig); their syntaxes diverge for some constructs. Web tutorial → Rust compiler; in-tree docs → Zig compiler
!is part of the function name —Stdout.lineandStdout.line!are different; the!signals “this performs effects, can only be called from another!context”- Platform URL is content-addressed — once downloaded the hash is locked. Updating a platform = update the URL and hash in the
appheader - Pure Roc cannot call C — no FFI from app code. If you need it, write a platform
- Number type defaults can surprise:
1 + 2is polymorphic; assigning it forces a concrete type. Annotate when LSP-driven inference is unhelpful - Refcounted in-place mutation only kicks in when refcount=1 — alias the value and mutating ops silently become O(n) copies. Profile if hot
Decis NOTF64—Decis a fixed-point decimal (money-safe but slower). Use the right type for the domain- No null, no exception, no goto, no
let mut, no inheritance — by design. Don’t try to port idiomatic Java/Python directly - Editor support is limited —
roc_ls(LSP) exists but is alpha quality. VS Code extension is the most-tested - Documentation can lag the code — alpha-stage. Source is the truth: https://github.com/roc-lang/roc
Citations
- Roc official site: https://www.roc-lang.org/
- Tutorial: https://www.roc-lang.org/tutorial
- Builtins reference: https://www.roc-lang.org/builtins
- Source: https://github.com/roc-lang/roc
- Latest release alpha4-rolling: https://github.com/roc-lang/roc/releases
- Platforms list: https://www.roc-lang.org/platforms
- basic-cli platform: https://github.com/roc-lang/basic-cli
- basic-webserver platform: https://github.com/roc-lang/basic-webserver
- Richard Feldman talks (“Outperforming Imperative with Pure Functional Languages”, “Effect Interpreters”) on YouTube