llms.md

Roc Language Reference

REPL

Run with roc repl. Online: roc-lang.org/repl

greeting = "Hi"
audience = "World"

Arithmetic: 1 + 2 * (3 - 4) follows order of operations.

Calling Functions

Str.concat("Hi ", "there.")  # "Hi there."

Str is a module, concat is a function in that module.

String Interpolation

"${greeting} there, ${audience}."
"Two plus three is: ${Num.to_str(2 + 3)}"

Building an Application

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!("Hi there, from inside a Roc app. 🎉")

Run: roc main.roc

Defs

birds = 3
iguanas = 2
total = Num.to_str(birds + iguanas)

main! = |_args|
    Stdout.line!("There are ${total} animals.")

Defs are constant—cannot be reassigned.

Defining Functions

add_and_stringify = |num1, num2|
    Num.to_str(num1 + num2)

if-then-else

add_and_stringify = |num1, num2|
    sum = num1 + num2
    if sum == 0 then
        ""
    else if sum < 0 then
        "negative"
    else
        Num.to_str(sum)

Every if must have both then and else.

Comments

# Single line comment
## Doc comment (included in generated docs)
##     x = 2  # Code block in doc (5 spaces after ##)

Records

add_and_stringify = |counts|
    Num.to_str(counts.birds + counts.iguanas)

total = add_and_stringify({ birds: 5, iguanas: 7 })

Functions accept records with extra fields:

total_with_note = add_and_stringify({ birds: 4, iguanas: 3, note: "Whee!" })

Record Shorthands

return_foo = .foo  # Same as |record| record.foo
{ x: x, y: y }     # Same as { x, y }

Record Destructuring

add_and_stringify = |{ birds, iguanas }|
    Num.to_str(birds + iguanas)

add_and_stringify = |{ birds, iguanas: lizards }|  # Rename field
    Num.to_str(birds + lizards)

{ x, y } = { x: 5, y: 10 }  # Destructure in defs

Record Update

original = { birds: 5, zebras: 2, iguanas: 7, goats: 1 }
from_original = { original & birds: 4, iguanas: 3 }

& cannot introduce new fields or change types.

Debugging with dbg

pluralize = |singular, plural, count|
    dbg count  # Prints: [pluralize.roc 6:8] 5
    if count == 1 then singular else plural

dbg Str.concat(singular, plural)  # Any expression
inc = |n| 1 + dbg n               # As function in expression

Tuples

tuple = ("hello", 42, ["list"])
first = tuple.0   # "hello"
second = tuple.1  # 42

(first, second, third) = ("hello", 42, ["list"])  # Destructuring

Pattern Matching

Tags

stoplight_color =
    if something > 0 then Red
    else if something == 0 then Yellow
    else Green

Tags are capitalized literals (like numbers/strings, no definition needed).

when/is

stoplight_str =
    when stoplight_color is
        Red -> "red"
        Green -> "green"
        Yellow -> "yellow"

Catch-all with _:

when stoplight_color is
    Red -> "red"
    _ -> "not red"

Multiple tags with |:

when stoplight_color is
    Red -> "red"
    Green | Yellow -> "not red"

Guards with if:

when stoplight_color is
    Red -> "red"
    Green | Yellow if contrast > 75 -> "high contrast"
    Green | Yellow -> "not red"

Tags with Payloads

stoplight_color =
    if something > 100 then Red
    else if something > 0 then Yellow
    else Custom("some other color")

stoplight_str =
    when stoplight_color is
        Red -> "red"
        Green | Yellow -> "not red"
        Custom(description) -> description

Multi-value payloads: Custom(40, 60, 80) destructured as Custom(r, g, b) ->

Booleans

Bool.true and Bool.false (not keywords). Prefer tags for data modeling:

{ name: "Richard", role: Admin }  # Better than is_admin: Bool.true

Lists

names = ["Sam", "Lee", "Ari"]
List.append(names, "Jess")  # Returns new list (immutable)

List.map

List.map([1, 2, 3], |num| num * 2)  # [2, 4, 6]
List.map([1, 2, 3], Num.is_odd)     # [Bool.true, Bool.false, Bool.true]

All list elements must share a type. Use tags for mixed types:

List.map([StrElem "A", NumElem 1], |elem|
    when elem is
        NumElem(num) -> Num.is_negative(num)
        StrElem(str) -> Str.starts_with(str, "A")
)

Using Tags as Functions

List.map(["a", "b", "c"], Foo)  # Same as |str| Foo(str)

List.any / List.all

List.any([1, 2, 3], Num.is_odd)       # Bool.true
List.all([1, 2, 3], Num.is_positive)  # Bool.true

Removing Elements

List.drop_at(["Sam", "Lee", "Ari"], 1)  # ["Sam", "Ari"]
List.keep_if([1, 2, 3, 4, 5], Num.is_even)  # [2, 4]
List.drop_if([1, 2, 3, 4, 5], Num.is_even)  # [1, 3, 5]

Getting Elements

List.get(["a", "b", "c"], 1)    # Ok "b"
List.get(["a", "b", "c"], 100)  # Err(OutOfBounds)
List.first(list)                # Err(ListWasEmpty) if empty
List.last(list)                 # Err(ListWasEmpty) if empty

Error Handling

Result.with_default(List.get(["a", "b", "c"], 100), "")  # ""
Result.isOk(List.get(["a", "b", "c"], 1))  # Bool.true

? Postfix Operator

Unwraps Ok, returns early on Err:

get_letter : Str -> Result Str [OutOfBounds, InvalidNumStr]
get_letter = |index_str|
    index = Str.to_u64(index_str)?
    List.get(["a", "b", "c", "d"], index)

?? Infix Operator (Default on Error)

List.get(["a", "b", "c"], 100) ?? ""  # ""

List.walk

List.walk([1, 2, 3, 4, 5], { evens: [], odds: [] }, |state, elem|
    if Num.is_even(elem) then
        { state & evens: List.append(state.evens, elem) }
    else
        { state & odds: List.append(state.odds, elem) }
)
# { evens: [2, 4], odds: [1, 3, 5] }

Arguments: list, initial state, function (state, elem) -> state

Pattern Matching on Lists

when my_list is
    [] -> 0                      # empty
    [Foo, ..] -> 1               # starts with Foo
    [_, ..] -> 2                 # at least one element
    [Foo, Bar, Baz] -> 3         # exactly 3 elements
    [Foo, a, ..] -> 4            # second element named `a`
    [Ok a, ..] -> 5              # first is Ok with payload
    [.., Foo] -> 6               # ends with Foo
    [A, B, .., C, D] -> 7        # specific start and end
    [head, .. as tail] -> 8     # head and rest

Only one .. (rest pattern) per pattern.

Pipe Operator

["a", "b", "c"] |> List.get(1) |> Result.with_default("")
# Same as: Result.with_default(List.get(["a", "b", "c"], 1), "")

Types

Type Annotations

full_name : Str, Str -> Str
full_name = |first_name, last_name|
    "${first_name} ${last_name}"

first_name : Str
first_name = "Amy"

amy : { first_name : Str, last_name : Str }
amy = { first_name: "Amy", last_name: "Lee" }

Type Aliases

Musician : { first_name : Str, last_name : Str }

amy : Musician
amy = { first_name: "Amy", last_name: "Lee" }

Type Parameters

names : List Str
names = ["Amy", "Simone", "Tarja"]

Wildcard Type (*)

is_empty : List * -> Bool  # Works on any list type

Empty list [] has type List *.

Type Variables

reverse : List elem -> List elem  # Same element type in and out

Lowercase names (elem, a, value) are type variables.

Tag Union Types

color_from_str : Str -> [Red, Green, Yellow]
color_from_str = |string|
    when string is
        "red" -> Red
        "green" -> Green
        _ -> Yellow

Tag unions accumulate:

|str|
    if Str.is_empty(str) then Ok "it was empty"
    else Err ["it was not empty"]
# Type: Str -> [Ok Str, Err (List Str)]

Result ok err is alias for [Ok ok, Err err].

Opaque Types

Username := Str

from_str : Str -> Username
from_str = |str| @Username(str)

to_str : Username -> Str
to_str = |@Username(str)| str

@Username only usable in defining module.

Integers

Type Range
U8 0 to 255
I8 -128 to 127
U16 0 to 65,535
I16 -32,768 to 32,767
U32 0 to 4,294,967,295
I32 -2,147,483,648 to 2,147,483,647
U64 0 to 18+ quintillion
I64 -9+ to 9+ quintillion
U128 0 to 340+ undecillion
I128 ±170+ undecillion

Overflow crashes the program.

Fractions

Type Description
F32 32-bit floating-point
F64 64-bit floating-point
Dec 128-bit decimal fixed-point (18 decimal places)

Dec is best for currency/base-10. F32/F64 have precision loss with decimals.

Num, Int, Frac

abs : Num a -> Num a           # Any number
bitwise_xor : Int a, Int a -> Int a  # Integers only
cos : Frac a -> Frac a         # Fractions only

Number literals have type Num * (or Frac * with decimal point).

Number Literal Suffixes

1u8    # U8
5dec   # Dec
0xfe   # Hex (254)
0b1000 # Binary (8)

Suffixes: u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, f32, f64, dec

Default-Value Record Fields

table : { height : U64, width : U64, title ?? Str, description ?? Str } -> Table
table = |{ height, width, title ?? "oak", description ?? "a wooden table" }| ...

?? in types marks optional fields. Only accessible via destructuring.

Crashing

Crashes: integer overflow, out of memory, crash keyword.

when Str.from_utf8(bytes) is
    Ok(str) -> str
    Err(_) -> crash "This should never happen!"

# TODO marker
crash "TODO handle the x <= y case"

Not for error handling—use Result instead.

Testing

pluralize = |singular, plural, count|
    if count == 1 then "${Num.to_str(count)} ${singular}"
    else "${Num.to_str(count)} ${plural}"

expect pluralize("cactus", "cacti", 1) == "1 cactus"
expect pluralize("cactus", "cacti", 2) == "2 cacti"

Run: roc test

Inline Expects

pluralize = |singular, plural, count|
    if count == 1 then "${Num.to_str(count)} ${singular}"
    else
        expect count > 0
        "${Num.to_str(count)} ${plural}"
  • roc build: discards all expects
  • roc dev: runs inline expects during execution
  • roc test: runs top-level expects and triggered inline expects

Modules

Types: app (application), module, package, platform, hosted

Builtin Modules (auto-imported)

Str, Num, Bool, Result, List, Dict, Set, Decode, Encode, Hash, Box, Inspect

App Module Header

app [main!] { pf: platform "https://..." }

import pf.Stdout
import AdditionalModule
import uuid.Generate as Uuid
import pf.Stdout exposing [line!]

Importing Files

import "some-file" as some_str : Str
import "some-file" as some_bytes : List U8

Effectful Functions

Pure functions: ->, effectful: =>

with_extension : Str -> Str           # Pure
read_file! : Str => Str               # Effectful

Effectful functions can call pure or effectful. Pure can only call pure. ! suffix is naming convention (compiler-enforced).

Stdout.line! : Str => Result {} [StdoutErr IOErr]
Stdin.line! : {} => Result Str [EndOfFile, StdinErr IOErr]

Reading Input

main! = |_args|
    Stdout.line!("Type something:")?
    input = Stdin.line!({})?
    Stdout.line!("You entered: ${input}")

Handling Failure

main! : List Arg => Result {} [Exit I32 Str]
main! = |_args|
    Result.map_err(my_function!({}), |err|
        when err is
            StdoutErr(_) -> Exit(1i32, "Error writing to stdout.")
            StdinErr(_) -> Exit(2i32, "Error writing to stdin.")
            EndOfFile -> Exit(3i32, "End of file reached.")
    )

Tagging Errors

main! = |_args|
    Stdout.line!("Prompt") ? UnableToPrintPrompt
    input = Stdin.line!({}) ? UnableToReadInput
    Stdout.line!("You entered: ${input}") ? UnableToPrintInput
    Ok({})

Inspect.to_str

Inspect.to_str(any_value)  # String representation for debugging

Early return

if this_is_a_bad_time then
    return "Error message"
else
    continue_normally

Advanced Concepts

Open vs Closed Records

# Closed: exact fields only
full_name : { first_name : Str, last_name : Str } -> Str

# Open: at least these fields (note the *)
full_name : { first_name : Str, last_name : Str }* -> Str

# Constrained: same type in and out
add_https : { url : Str }a -> { url : Str }a

Inference:

  • Creating record → closed
  • Using as argument/destructuring → open
  • Record update → constrained

Type Alias with Variable

User a : { email : Str, first_name : Str, last_name : Str }a

is_valid : User * -> Bool        # Open
user_from_email : Str -> User {} # Closed
capitalize : User a -> User a    # Constrained

Open vs Closed Tag Unions

# Open: might have unknown tags (needs _ -> branch)
example : [Foo Str, Bar Bool]* -> Bool
example = |tag|
    when tag is
        Foo(str) -> Str.is_empty(str)
        Bar(bool) -> bool
        _ -> Bool.false

# Closed: exactly these tags
example : [Foo Str, Bar Bool] -> Bool
example = |tag|
    when tag is
        Foo(str) -> Str.is_empty(str)
        Bar(bool) -> bool

New tags are inferred as open unions and can accumulate through conditionals.

Constrained Tag Unions

example : [Foo Str, Bar Bool]a -> [Foo Str, Bar Bool]a
example = |tag|
    when tag is
        Foo(str) -> Bar(Str.is_empty(str))
        Bar(bool) -> Bar(Bool.false)
        other -> other

Record Builder

user_tab_matcher =
    { combine_matchers <-
        _: exact_segment("users"),  # Ignored field
        user_id: u64_segment,
        tab: any_segment,
    }

Desugars to nested combine_matchers calls.

Reserved Keywords

as, crash, dbg, else, expect, expect-fx, if, import, is, return, then, try, when

Operator Desugaring

Operator Desugars To
a + b Num.add(a, b)
a - b Num.sub(a, b)
a * b Num.mul(a, b)
a / b Num.div(a, b)
a // b Num.div_trunc(a, b)
a ^ b Num.pow(a, b)
a % b Num.rem(a, b)
-a Num.neg(a)
a == b Bool.is_eq(a, b)
a != b Bool.is_not_eq(a, b)
a && b Bool.and(a, b)
a || b Bool.or(a, b)
!a Bool.not(a)
a |> f f(a)