Beginner

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the official Roc nightly image
docker pull roclang/nightly-ubuntu-2204:latest

# Run the arithmetic example
docker run --rm -v $(pwd):/app -w /app roclang/nightly-ubuntu-2204:latest roc arithmetic.roc

# Run the other examples by swapping the filename
docker run --rm -v $(pwd):/app -w /app roclang/nightly-ubuntu-2204:latest roc precedence.roc
docker run --rm -v $(pwd):/app -w /app roclang/nightly-ubuntu-2204:latest roc comparison.roc
docker run --rm -v $(pwd):/app -w /app roclang/nightly-ubuntu-2204:latest roc logical.roc
docker run --rm -v $(pwd):/app -w /app roclang/nightly-ubuntu-2204:latest roc pipe.roc

Note: On the first run, Roc downloads the basic-cli platform 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 functionsa + b is a call into the Num module, and like all Roc expressions it produces a new value instead of mutating anything.
  • / always yields a fraction — use Num.div_trunc for truncating integer division and Num.rem for 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 ==, !=, <, >, <=, >= produce Bool.true or Bool.false, which you branch on with if ... then ... else.
  • Logic uses keywords — Roc spells boolean logic as and and or (both short-circuiting), with Bool.not or the ! prefix for negation. There are no &&/|| symbols.
  • The pipe |> drives functional stylex |> f(y) equals f(x, y), letting transformation chains read top-to-bottom instead of inside-out.
  • Num.to_str bridges 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
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining