Functions in Roc
Learn how to define and call functions in Roc, including type annotations, recursion, the pipe operator, higher-order functions, and closures with Docker-ready examples
Functions are the heart of Roc. As a purely functional language, Roc has no statements that mutate shared state — a program is built almost entirely out of functions that take values and return new values. Every function is pure by default, meaning it always returns the same output for the same input and produces no hidden side effects. Functions that do perform effects (printing, reading files, networking) are explicitly marked with a ! suffix, so you can always see where effects happen.
In Roc, functions are first-class values. You can pass them as arguments, return them from other functions, store them in lists, and create them inline as anonymous lambdas (closures). There is no separate concept of a “method” tied to an object — there are only functions, and the data they operate on is passed in explicitly.
A function in Roc is defined by binding a name to a lambda written with the |params| body syntax. Type annotations are completely optional because Roc infers the most general type for every function, but you can add them for documentation and to make compiler errors clearer. This tutorial covers defining and calling functions, parameters and return values, type annotations, recursion, the pipe operator, and higher-order functions with closures.
Defining and Calling Functions
A function is just a named lambda. The parameters go between the pipes, and the body is the expression after them — whatever the body evaluates to is the return value (there is no return keyword). Function calls wrap their arguments in parentheses, like add(3, 4).
This first example shows basic functions, an explicit type annotation, string interpolation, recursion, and the pipe operator (|>), which feeds a value in as the first argument of the next function.
Create a file named functions.roc:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
# A function is a name bound to a lambda: |params| body.
# The body's value is returned automatically — no `return` keyword.
add = |a, b|
a + b
# Type annotations are optional. This one reads:
# multiply takes two I64 values and returns an I64.
multiply : I64, I64 -> I64
multiply = |a, b|
a * b
# Functions can return any type. This one returns a Str
# built with ${...} string interpolation.
greet = |name|
"Hello, ${name}!"
# Recursion replaces loops in functional code.
# factorial calls itself until n reaches 0.
factorial : U64 -> U64
factorial = |n|
if n == 0 then
1
else
n * factorial(n - 1)
main! = |_args|
# Calling a function wraps its arguments in parentheses.
sum = add(3, 4)
product = multiply(6, 7)
greeting = greet("Roc")
# The pipe operator |> passes the left value as the FIRST
# argument of the next call: 5 |> multiply(2) is multiply(5, 2).
doubled = 5 |> multiply(2)
report =
[
"add(3, 4) = ${Num.to_str(sum)}",
"multiply(6, 7) = ${Num.to_str(product)}",
"5 |> multiply(2) = ${Num.to_str(doubled)}",
"greet(\"Roc\") = ${greeting}",
"factorial(5) = ${Num.to_str(factorial(5))}",
]
|> Str.join_with("\n")
Stdout.line!(report)
Notice that all the pure computation happens first, and only the final line — Stdout.line!(report) — performs an effect. This separation of pure logic from effects is a core part of Roc’s design.
Higher-Order Functions and Closures
Because functions are first-class values, you can pass one function into another. A function that takes or returns a function is called a higher-order function. You can pass a named function by referring to its name, or pass an anonymous closure — an inline lambda that can capture variables from the surrounding scope.
Create a file named higher_order.roc:
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
# A higher-order function: `f` is itself a function.
# apply_twice runs f on x, then runs f again on the result.
apply_twice = |f, x|
f(f(x))
double = |n|
n * 2
main! = |_args|
# Pass a named function (double) as an argument.
quadrupled = apply_twice(double, 5)
# Pass a closure. add_step captures `step` from this scope.
step = 10
add_step = |n| n + step
stepped = apply_twice(add_step, 0)
# List.map applies a function to every element, returning a new list.
# Here we map an inline closure over a list of names, then join them.
shouted =
["ada", "alan", "grace"]
|> List.map(|name| "${name}!")
|> Str.join_with(", ")
report =
[
"apply_twice(double, 5) = ${Num.to_str(quadrupled)}",
"apply_twice(add_step, 0) = ${Num.to_str(stepped)}",
"shouted = ${shouted}",
]
|> Str.join_with("\n")
Stdout.line!(report)
Here apply_twice(double, 5) computes double(double(5)) = double(10) = 20. The closure add_step captures the local step value of 10, so apply_twice(add_step, 0) computes add_step(add_step(0)) = add_step(10) = 20. List.map is the functional replacement for an explicit loop: it transforms every element without you ever writing iteration by hand.
Running with Docker
| |
Note: On the first run, Roc downloads the
basic-cliplatform referenced in the source file. This may take a few seconds.
Expected Output
Running functions.roc:
add(3, 4) = 7
multiply(6, 7) = 42
5 |> multiply(2) = 10
greet("Roc") = Hello, Roc!
factorial(5) = 120
Running higher_order.roc:
apply_twice(double, 5) = 20
apply_twice(add_step, 0) = 20
shouted = ada!, alan!, grace!
Key Concepts
- Functions are named lambdas —
name = |params| body. The body’s value is the return value; there is noreturnkeyword. - Calls use parentheses —
add(3, 4). This syntax arrived with the alpha2 release alongside snake_case naming conventions. - Type annotations are optional — Roc infers the most general type for every function, but annotations like
multiply : I64, I64 -> I64aid readability and produce clearer errors. - Pure by default, effects marked with
!— pure functions likeaddhave no side effects; only effectful calls likeStdout.line!carry the!suffix. - Recursion replaces loops — there are no
for/whileloops in idiomatic Roc; functions likefactorialcall themselves instead. - The pipe operator
|>feeds a value in as the first argument of the next function, letting you read transformations left to right. - Functions are first-class — pass them as arguments, return them, and build anonymous closures inline that capture surrounding variables.
- Higher-order helpers like
List.mapapply a function across a whole collection, expressing iteration declaratively rather than step by step.
Running Today
All examples can be run using Docker:
docker pull roclang/nightly-ubuntu-2204:latest
Comments
Loading comments...
Leave a Comment