Operators in Roc
Learn arithmetic, comparison, logical, and pipe operators in Roc - a fast, friendly functional language - with Docker-ready examples
Operators are the symbols that combine values into expressions: adding numbers, comparing them, and chaining boolean conditions. Because Roc is a purely functional language, an operator is really just a compact way of calling a function — 1 + 2 is sugar for a call into the Num module, and the result is always a new value rather than a mutated one.
Roc deliberately keeps its operator set small and predictable. There is no operator overloading and no surprising precedence. Anything that isn’t a common arithmetic or comparison symbol is expressed as a plain function call in the Num, Bool, or Str modules. This is why integer division and remainder are written as Num.div_trunc and Num.rem rather than as standalone symbols — keeping the operator table short makes Roc code easy to read for newcomers.
This tutorial covers the four families of operators you will use most: arithmetic, comparison, logical, and the pipe operator (|>) that is central to functional Roc. Every example is a complete program you can run with Docker. Note that all builtin functions use snake_case (for example, Num.to_str), matching the conventions introduced in Roc’s alpha releases.
Arithmetic Operators
Roc has four arithmetic operators: +, -, *, and /. The first three work on any numeric type. The / operator is special — it always produces a fraction, so it cannot be used on integers directly. Integer division (truncating toward zero) and remainder are provided as Num functions instead.
Create a file named arithmetic.roc:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
main! = |_args|
a = 17
b = 5
# The basic operators work on numbers
sum = a + b # 22
difference = a - b # 12
product = a * b # 85
# Integer division and remainder are functions, not symbols
quotient = Num.div_trunc(a, b) # 3
remainder = Num.rem(a, b) # 2
# Exponentiation is also a function
power = Num.pow_int(a, 2) # 289
# The / operator always produces a fraction
fraction = 17.0 / 5.0 # 3.4
Stdout.line!("a = ${Num.to_str(a)}, b = ${Num.to_str(b)}")?
Stdout.line!("a + b = ${Num.to_str(sum)}")?
Stdout.line!("a - b = ${Num.to_str(difference)}")?
Stdout.line!("a * b = ${Num.to_str(product)}")?
Stdout.line!("Num.div_trunc(a,b) = ${Num.to_str(quotient)}")?
Stdout.line!("Num.rem(a,b) = ${Num.to_str(remainder)}")?
Stdout.line!("Num.pow_int(a,2) = ${Num.to_str(power)}")?
Stdout.line!("17.0 / 5.0 = ${Num.to_str(fraction)}")
Here Num.to_str converts each number into a string so it can be embedded into the output with ${...} interpolation. Notice the ? after every Stdout.line! except the last — printing is an effect that returns a Result, and ? unwraps the success value while propagating any error. The final line is the expression that main! returns.
Operator Precedence
Multiplication and division bind more tightly than addition and subtraction, just as in ordinary math. Parentheses override the default grouping.
Create a file named precedence.roc:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
main! = |_args|
without_parens = 2 + 3 * 4 # 3 * 4 first, then + 2 -> 14
with_parens = (2 + 3) * 4 # 2 + 3 first, then * 4 -> 20
Stdout.line!("2 + 3 * 4 = ${Num.to_str(without_parens)}")?
Stdout.line!("(2 + 3) * 4 = ${Num.to_str(with_parens)}")
Comparison Operators
Comparison operators take two values and return a Bool (Bool.true or Bool.false). Roc supports the usual six: ==, !=, <, >, <=, and >=. Because a Bool cannot be printed directly with Num.to_str, each result is turned into text with a small if ... then ... else ... expression.
Create a file named comparison.roc:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
main! = |_args|
x = 10
y = 7
eq = if x == y then "true" else "false"
neq = if x != y then "true" else "false"
lt = if x < y then "true" else "false"
gt = if x > y then "true" else "false"
lte = if x <= y then "true" else "false"
gte = if x >= y then "true" else "false"
Stdout.line!("x = ${Num.to_str(x)}, y = ${Num.to_str(y)}")?
Stdout.line!("x == y -> ${eq}")?
Stdout.line!("x != y -> ${neq}")?
Stdout.line!("x < y -> ${lt}")?
Stdout.line!("x > y -> ${gt}")?
Stdout.line!("x <= y -> ${lte}")?
Stdout.line!("x >= y -> ${gte}")
Logical Operators
Roc uses the keywords and and or for boolean logic — there are no && or || symbols. Both short-circuit: and stops at the first Bool.false, and or stops at the first Bool.true. To negate a boolean, use the Bool.not function or its ! prefix shorthand.
Create a file named logical.roc:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
main! = |_args|
age = 25
has_ticket = Bool.true
# 'and' is true only when both sides are true
can_enter = age >= 18 and has_ticket
# 'or' is true when at least one side is true
free_day = Bool.false or Bool.true
# Bool.not (or the ! prefix) inverts a boolean
turned_away = Bool.not(can_enter)
also_turned_away = !can_enter
show = |b| if b then "Bool.true" else "Bool.false"
Stdout.line!("age >= 18 and has_ticket -> ${show(can_enter)}")?
Stdout.line!("Bool.false or Bool.true -> ${show(free_day)}")?
Stdout.line!("Bool.not(can_enter) -> ${show(turned_away)}")?
Stdout.line!("!can_enter -> ${show(also_turned_away)}")
The show binding is a small local function (|b| ...) that converts a Bool into readable text, reused for each line. Comparisons such as age >= 18 bind tighter than and, so the expression groups as (age >= 18) and has_ticket without needing parentheses.
The Pipe Operator
The pipe operator |> is the operator that most defines functional Roc. It feeds the value on its left into the function on its right as the first argument: x |> f(y) is exactly the same as f(x, y). Chaining pipes lets you read a sequence of transformations top-to-bottom instead of inside-out.
Create a file named pipe.roc:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
main! = |_args|
# Without the pipe: nested calls must be read inside-out
nested = Num.to_str(Num.sub(Num.mul(Num.add(10, 5), 2), 6))
# With the pipe: data flows left to right
# x |> f(y) is the same as f(x, y)
piped_num =
10
|> Num.add(5) # Num.add(10, 5) = 15
|> Num.mul(2) # Num.mul(15, 2) = 30
|> Num.sub(6) # Num.sub(30, 6) = 24
piped = Num.to_str(piped_num)
Stdout.line!("Nested calls: ${nested}")?
Stdout.line!("Piped chain: ${piped}")
Both expressions compute the same result, but the piped version reads in the order the operations actually happen: start with 10, add 5, multiply by 2, subtract 6. This left-to-right flow is why |> appears throughout idiomatic Roc code.
Running with Docker
You can run any of these examples without installing Roc locally. Pull the image once, then run each file by name.
| |
Note: On the first run, Roc downloads the
basic-cliplatform referenced in each file. This may take a few seconds.
Expected Output
Running arithmetic.roc:
a = 17, b = 5
a + b = 22
a - b = 12
a * b = 85
Num.div_trunc(a,b) = 3
Num.rem(a,b) = 2
Num.pow_int(a,2) = 289
17.0 / 5.0 = 3.4
Running precedence.roc:
2 + 3 * 4 = 14
(2 + 3) * 4 = 20
Running comparison.roc:
x = 10, y = 7
x == y -> false
x != y -> true
x < y -> false
x > y -> true
x <= y -> false
x >= y -> true
Running logical.roc:
age >= 18 and has_ticket -> Bool.true
Bool.false or Bool.true -> Bool.true
Bool.not(can_enter) -> Bool.false
!can_enter -> Bool.false
Running pipe.roc:
Nested calls: 24
Piped chain: 24
Key Concepts
- Operators are sugar for functions —
a + bis a call into theNummodule, and like all Roc expressions it produces a new value instead of mutating anything. /always yields a fraction — useNum.div_truncfor truncating integer division andNum.remfor the remainder; the operator table stays small on purpose.- Precedence follows math —
*and/bind tighter than+and-, and parentheses override the default grouping. - Comparisons return
Bool— the six operators==,!=,<,>,<=,>=produceBool.trueorBool.false, which you branch on withif ... then ... else. - Logic uses keywords — Roc spells boolean logic as
andandor(both short-circuiting), withBool.notor the!prefix for negation. There are no&&/||symbols. - The pipe
|>drives functional style —x |> f(y)equalsf(x, y), letting transformation chains read top-to-bottom instead of inside-out. Num.to_strbridges numbers and strings — convert numeric results before interpolating them into output with${...}.
Running Today
All examples can be run using Docker:
docker pull roclang/nightly-ubuntu-2204:latest
Comments
Loading comments...
Leave a Comment