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 expectsroc dev: runs inline expects during executionroc 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) |