Roc is still a work in progress and has not reached a 0.1 release, so expect missing features and bugs.
Helpful resources:
- Standard library docs
- Examples
- Language reference
- Roc syntax overview
- Learn roc by doing, with exercism
If you want help, the best place to get it is Roc Zulip. You're welcome to ask any questions in #beginners.
With those disclaimers in mind, let's get into the adventure!
First, follow the installation guide to install a nightly build.
It includes an executable named roc (or roc.exe on Windows). You'll know it is available on your PATH
if roc version works.
Next, copy/paste this into a new file named main.roc:
main! = |_args| {
echo!("Hello, World!")
Ok({})
}You can run this with:
roc main.rocYou should see this:
Hello, World!
Hooray!
Tip: If you don't provide a path to a .roc file,
rocwill default to "main.roc" - so since your Roc program is named main.roc, you can run your program by just runningrocin the future.
You can also try Roc expressions and definitions without creating a file by running roc repl.
roc replInside the REPL, enter an expression to evaluate it:
1 + 1You can define names and use them in later inputs:
x = 1 + 1
x * 2The REPL also works well with piped input. In piped mode it prints only evaluation output to stdout; prompts, the welcome banner, and the goodbye message are not printed.
In PowerShell, send multiple input lines with an array:
@("x = 1 + 1", "x * 2") | roc replYou should see:
assigned `x`
4.0
In Unix shells, use printf for the same thing:
printf 'x = 1 + 1\nx * 2\n' | roc replDiagnostics from piped input are written to stderr, so tools can read successful results from stdout separately from parse and type errors. If you want plain diagnostics without ANSI color codes, use roc repl --no-color or set NO_COLOR environment variable.
Let's take a look at main! next:
main! = |_args| {
echo!("Hello, World!")
Ok({})
}This defines our program's entrypoint. The code inside the curly braces will run when we do roc main.roc.
That |_args| { ... } is Roc syntax for an anonymous function with one argument, named _args. (If it had multiple arguments,
it would look like |arg1, arg2| { ... } instead.) Functions in Roc are ordinary values, just
like numbers, strings, or booleans; we can pass them around, put them in collections, and use = to name them.
We named it main! (and not the traditional main) because it's a function that runs the
side effect of printing to
stdout.
In Roc, we call functions that can run side effects effectful functions, and by convention
we always name them with a ! at the end of their names. In contrast,
pure functions don't have a ! at the end of their names.
This line defines a new constant called name:
name = "Rocco"Constants should not be reassigned or shadowed, if you try to do name = again in the same scope,
roc will give a compile-time warning with exit code 2. That way, you can quickly write something with
shadowing if you want but the non-zero exit code prevents it from ending up in production code because CI will fail.
The string "Hello, ${name}!" will evaluate to "Hello, Rocco!".
In an ordinary string interpolation, each interpolated expression must also produce a string; Roc does not automatically convert other types. For example, if you wanted to print an integer you'd need to call to_str on it like so:
echo!("Number of things: ${thing_count.to_str()}")You can put any expression you like inside a string interpolation. If you really wanted to, you could do something like this:
echo!("Answer: ${((numerator / denominator) + 1).negate().to_str()}")...but at that point it'd probably be easier to read if you extracted that expression into a constant.
Sometimes code is easier to understand in a functional style, and other times it's easier to understand in an imperative style. Roc has support for both styles, even though its APIs are designed around immutable values and a functional style.
To see both styles, let's write a function called digits_to_num which takes a list of digits and returns the number they represent. When we're done, we'll be able to run roc test and see these expects pass:
expect digits_to_num([1, 2, 3]) == 123
expect digits_to_num([4, 2]) == 42
expect digits_to_num([7]) == 7expect is Roc's lightweight testing keyword. You can put a boolean expression after it, and if that expression
evaluates to True, the test will pass, and if it evaluates to False, the test will fail. When you run roc test,
it runs all of top-level expects in your files, as well as all the files they import (unless they belong to downloaded packages).
A file run with roc test does not need a main! function (or an app header, which will be introduced shortly).
Another useful command is
roc fmt, which formats your source code according to standard Roc style. By design,roc fmthas no configuration options at all.
Here's an imperative-style implementation of digits_to_num, using a for loop and reassignable vars:
digits_to_num = |digits| {
var $num = 0
for digit in digits {
$num = ($num * 10) + digit
}
$num
}Unlike a constant, a var like $num can be reassigned. Similarly to how effectful functions have names
ending in ! to distinguish them from pure functions, vars begin with $ to distinguish them from constants.
This means any time you see something beginning with $, you know it might be reassigned somewhere (such as
in a for loop), whereas if you don't see the $, there's no need to think about that possibility.
Note that vars can only be reassigned inside the function where they were declared. This code would give an error:
var $count = 0
things.for_each!(|thing| {
$count = $count + 1
# other logic goes here
})The error would say that $count can't be reassigned inside a different function from where it was declared with var.
As such, if you see a for_each! being used instead of a for loop, that tells you whatever logic inside is guaranteed
not to modify any vars in the outer scope—whereas if you see a for loop, you know that it might be modifying vars.
You can also implement digits_to_num in a functional style using fold:
digits_to_num = |digits| digits.fold(0, |num, digit| (num * 10) + digit)Note that there are no curly braces in the digits_to_num definition. That's because every Roc function has one expression
after its arguments (e.g. you can write |arg| arg.to_str()), but that expression can be a block if you like.
Here's an example of a block expression:
answer = {
foo = 43
inner_constant = foo - 1
echo!("Inner constant: ${inner_constant.to_str()}")
inner_constant.abs()
}Block expressions go inside curly braces. They end with an expression (like inner_constant.abs() here),
and the entire block evaluates to whatever that expression evaluates to. This is why, when we use curly
braces in functions, the function returns whatever is at the end of the curly braces—that's just how blocks work!
Everything before the expression at the end of a block is a statement. The difference between statements and expressions is:
- An expression evaluates to a value.
- A statement does not.
for loops and constant declarations like inner_constant = are statements. Since statements don't evaluate to
values, you can't do things like pass them as function arguments. You couldn't write echo!(for ...) or
echo!(inner_constant = ...) - you'd get a compile-time error if you did.
Blocks are expressions, so you could write something like this inside an effectful function:
echo!({
var $total = 0
for number in 1..=10 {
$total = $total + number
}
$total.to_str()
})An easy way to think of it is that blocks are a way to incorporate statements into your expressions.
Roc's return keyword works the same way it works in most languages: it causes the function to immediately return.
digits_to_num = |digits| {
if digits.is_empty() {
return 0
}
# ...the rest of the function would go here
}If you use this in a block, you may get a warning if any statements or expressions come after it in the block, as they will not be executed!
Roc's crash keyword does what it says: it crashes the currently-running Roc application.
digits_to_num = |digits| {
if digits.is_empty() {
crash "Expected a nonempty list."
}
# ...the rest of the function would go here
}crash is not for error handling.
The reason Roc has a crash keyword is for scenarios where it's expected that no error will ever happen (like in unreachable branches), or where graceful error handling is infeasible (like running out of memory).
Errors that are recoverable should be represented using normal Roc types (like Try) and then handled without crashing. For example, by having the application report that something went wrong, and then continue running from there.
What happens after a crash is determined by the platform. Some may gracefully recover and have some way of continuing the process, but others may terminate the process immediately.
Just like return, if you use crash in a block, you may get a warning if any statements or expressions
come after it in the block, as they will not be executed!
We mentioned expect earlier - if you put these at the top level of your file, they will be run whenever roc test runs.
You can also put them in blocks, in which case they will work essentially like a crash when you're doing roc test or
a debug build of roc, but when you do roc --opt=speed, they will be skipped.
digits_to_num = |digits| {
if digits.is_empty() {
return 0
}
# From here on, we assume digits is nonempty!
expect !digits.is_empty()
# ...the rest of the function would go here
}Importantly, these are not production assertions! The point is that these are checks of things you assume will be true, and if they turn out not to be true, you would like to be alerted about the assumption proving false during development or when running test. It is the responsibility of other code to handle (or not) the situation where these assumptions turn out to be false.
For example, here are three different ways you can handle an assumption turning out to be false in production:
- Detect it and gracefully recover from it, because that's the best experience for people using the software
- Do not attempt to detect it, because doing so is either impractical to implement or too costly in terms of runtime performance; accept that if it happens, it will be bad, but trying to detect it defensively would be worse overall
- Detect it and crash the program using the
crashkeyword
All three of these have different tradeoffs, and different situations can reasonably call for one over the others.
The point of expect working the way it does is that it does not run in --opt=speed builds at all,
so it does not have production tradeoffs! You can use it as often as you like, and the consequences will
only be felt during development.
You can use the dbg keyword for the classic technique of printline debugging, like so:
main! = |_args| {
x = 5
dbg x
# dbg(x) works too
Ok({})
}Although printing is an I/O operation, the dbg statement can be used even in pure functions. It works in two different ways:
- Roc evaluates top-level constants at compile time. If evaluating one calls a pure function containing
dbg, you'll see thedbgoutput at compile time. (Evaluating known expressions ahead of time is called constant folding.) - At runtime,
dbgmay work differently depending on where you're running the program. For example, in this command-line application it will be printed to stderr. However, when running a Roc program that's compiled to WebAssembly and running in the browser, it would likely appear in the browser console instead.
Note that Roc's compile-time evaluation means there is no guarantee that a given dbg will run at runtime or at compile time.
In Roc, if can be used as an expression like so:
name = if str.is_empty() "n/a" else strYou can optionally use blocks to make the different branches stand out more:
name = if str.is_empty() {
"n/a"
} else {
str
}You can also create records, like so:
record = if str.is_empty() {
{ name: "n/a", has_name: Bool.False }
} else {
{ name: str, has_name: Bool.True }
}You can "update" a record by creating a new one that has some of its fields changed:
new_record = { ..record, name: "New Name" }You can also destructure a record to bring some of its fields into scope as constants:
{ name, has_name } = recordIn Roc, a List looks like this:
animals = ["bird", "crab", "lizard"]You can get the length of the list by calling List.len(animals), or as a shortcut, you can just call animals.len().
Both of them do the same thing. When you call .len(), Roc's type inference knows that the type of animals is List,
so it translates that animals.len() call into List.len(animals) at compile time. .len() is known as a method because
it's a function that is associated with a particular type (in this case, List).
You can get the first element in the list using animals.first(), but you can also do it using pattern matching:
points = match animals {
["bird", "crab", "lizard"] => 10 # exact match
["bird", "crab", ..] => 5 # partial match
["bird", ..] => 1 # partial match
[first, second, "lizard", ..] => count_points(first, second)
_ => 0 # default
}Patterns can nest as deeply as you like; if you had a list of lists of strings, you could do a pattern like [first_list, ["bird", ..], ..] => etc.
The default branch (_ =>) at the end is necessary so that it's always clear what value points should become,
since lists can be any length and have lots of different contents.
All items in a list must have compatible types.
The following will give a compile error because it's a list with both strings and numbers in it:
animals = ["eagle", 1]That said, you can tag each of them like so:
birds_or_numbers = [Bird("eagle"), Number(1)]You can use pattern matching to access the contents of tags:
label = match birds_or_numbers {
[Bird(bird), Number(num)] => "${bird} number ${num.to_str()}"
_ => "" # default
}Roc does not have null, nil, undefined, or anything similar. Instead, it uses the tags Ok and Err to
represent whether an operation succeeded or failed:
numbers = [1, 2 ,3 ]
number = match numbers.first() {
Ok(first) => first + 1
Err(ListWasEmpty) => 0
}Here, ListWasEmpty is a tag that isn't wrapping anything. The List.first method is just returning it inside the Err
to describe what the failure was. This both makes the code more self-documenting and also lets you distinguish between
different error types.
For example:
num_or_err = if numbers.is_empty() {
I64.from_str("1")
} else {
numbers.first()
}
answer = match num_or_err {
Ok(num) => num + 1
Err(ListWasEmpty) => 0
Err(BadNumStr) => -1
}Here we have an extra Err branch, because List.first can return Err(ListWasEmpty) if the list was empty,
whereas I64.from_str can return Err(BadNumStr).
Note:
I64is a number type—specifically, a 64-bit integer. Roc also supports 8-bit, 16-bit, 32-bit, and 128-bit integers, and they can be either signed (likeI64) or unsigned (likeU64). For non-integer types, Roc hasF32andF64for the classic 32-bit and 64-bit binary floating point numbers, and alsoDecfor a 128-bit fixed-point decimal. If you don't specify a number type, Roc usesDecas the default number—which is why in Roc,0.1 + 0.2 == 0.3isTrue, whereas in most languages it isn't.
Neither of these match expressions had a default _ => branch. They didn't need it because they are already exhaustive,
which means they have covered all possible cases. If a match on a known type omits a possible case, the compiler reports
a non-exhaustive match error. If you add _ => after earlier branches have already covered every case, the compiler warns
that the pattern is redundant.
Roc code tends to avoid _ => default branches because these exhaustiveness errors can be helpful for telling you when
you've forgotten to handle something. For example, in our second match above, if we'd written _ => 0 instead of
Err(ListWasEmpty) => 0, we would have been silently handling the Err(BadNumStr) case using the same logic. That might
not have been what we wanted! By writing out Err(ListWasEmpty) instead of _ =>, the compiler would let us know if we
were forgetting to handle any cases that could come up at runtime.
Both List.first and I64.from_str are returning an extremely common Roc type, named Try. We define and pattern-match
Str values using "…", List values using […], and Try values using Ok and Err. So if I had a function that
accepted a Try with strings for both its Ok and Err types, then I could pass it Ok("foo") or Err("bar").
Just like Str and List, Try has methods. Here's an example of one:
number = numbers.first().ok_or(0)
The Try.ok_or method is defined like this:
ok_or = |try, fallback| match try {
Ok(val) => val
Err(_) => fallback
}It returns the value inside the Ok tag, or else the provided fallback value if the Try was an Err tag instead of Ok.
The underscore pattern inside Err(_) => essentially means to ignore that part of the pattern. You can put underscores
anywhere in any pattern, including for the entire pattern. This method doesn't want to match a more specific pattern
(like Err(ListWasEmpty) => earlier) because it wants to be flexible. If it matched a more restrictive pattern, like
Err(ListWasEmpty) =>, then you couldn't use .ok_or with I64.from_str because it returns Err(BadNumStr).
It's common to want to early-return an Err from a Try. It's so common, Roc has a dedicated operator for it:
increment_first = |strings| {
first_str = strings.first()?
first_num = I64.from_str(first_str)?
Ok(first_num + 1)
}The ? postfix operator is syntax sugar for "if this is an Ok, evaluate to its value; otherwise, return the Err."
The desugared version of the above function would be:
increment_first = |strings| {
first_str = match strings.first() {
Ok(val) => val
Err(err) => return Err(err)
}
first_num = match I64.from_str(first_str) {
Ok(val) => val
Err(err) => return Err(err)
}
Ok(first_num + 1)
}The ? version is quite a bit more concise!
So far we haven't seen any types. That's because although Roc is a statically type-checked language, it infers the types of everything you write. All type annotations in Roc are optional, but the compiler still infers every type, so you'll still get compile-time errors if you mix up types. Technically, Roc has sound, decidable, principal static type inference. All Roc values are semantically immutable, making them free of data races as well.
Roc has a "nonblocking compilation" design philosophy. This means that roc will still run your program and roc test
will still run your tests when possible, even if you have compile-time errors—including static type mismatches. The
diagnostics are still printed and the command exits with a nonzero status, so CI will treat the run as a failure.
This lets you try code despite known problems, although reaching invalid code at runtime may cause it to crash.
Use roc check && roc if you want to check for errors first and run the program only when there are none.
You can write type annotations above any constant or var, like so:
name : Str
name = "Sam"
is_empty : Bool
is_empty = name.is_empty()We noted earlier how all items in a List must have compatible types. That's represented in the List type
using a type parameter like so:
strings : List(Str)
strings = ["a", "b", "c"]
integers : List(I64)
integers = [1, 2, 3]Pure functions use a thin arrow (->) and effectful functions use a thick arrow (=>) to separate parameter types from
return types:
average : Dec, Dec -> Dec
average = |a, b| (a + b) / 2
# Note that you don't have to write out 2.0 to perform decimal division in Roc; you can just write 2 like normal!
read_str! : Path => Try(Str, ReadFileErr)
read_str! = |path| # ...Try type is a parameterized type with two type parameters. The first one is the Ok type and the second one is the Err type.
Type variables allow you to write functions that work with any type. They are written as lowercase identifiers (like a, b, elem, etc.) in type annotations:
# This function works for a list of any type
type_var : List(a) -> List(a)
type_var = |lst| lstThe type variable a indicates that the function accepts a List containing elements of any type, and returns a List containing elements of that same type.
You can also constrain type variables to types that have specific methods using where:
stringify : a -> Str where [a.to_str : a -> Str]
stringify = |value| value.to_str()This function works for any type a that has a to_str method which takes an a and returns a Str.
It's very common to see a Try with a tag union for its error type, even if it only has one tag in it. This allows
multiple errors to neatly combine into a tag union of all the possible errors that could occur, so you can pattern
match on them later like we saw with I64.from_str and List.first.
As we saw with ListWasEmpty and BadNumStr, tags don't have to wrap anything. You can also use them as enumerations, like so:
Color := [Red, Green, Blue]This is a nominal type definition.
If you've been writing Roc before it was mainstream and you want to upgrade your code from alpha-4, compare the two all syntax examples:
The initial Hello World example is a headerless app. Headerless apps automatically use Roc's built-in
Echo Platform, which provides the unqualified echo! function and is intentionally minimal.
Other Roc apps select a platform explicitly. For example, this header uses
basic-cli, a platform for command-line applications:
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.22.2/9zUBxb1LtXYVc4eR4hAtd1WQDwBYDhM6HQdZz1UFCm2m.tar.zst" }Let's break the header down:
appmeans this .roc file specifies a Roc application - an executable, as opposed to a bundle of reusable code like a package[main!]specifies the application's entrypoint. Some applications have multiple entrypoints, but it's most common to have just one—and also it's most common for that one to be namedmain!{ pf: platform "https://..." }specifies the application's platform. If we wanted to add other dependencies, this is where we'd specify them - e.g. we might writeuni: "https://..."to add a dependency on unicode to process Unicode strings, at which point we'd be able to do things likeimport uni.Graphemeand so on.
See the modules documentation for more examples.
Roc has a first-class concept of platforms and applications. You can read about the design philosophy, but for our purposes what matters is:
- Every Roc application specifies exactly one platform that it will be built on. A headerless app implicitly uses the Echo Platform.
- The selected platform provides the app's I/O primitives. For example, the
basic-cliplatform gives access to the standard in and out usingStdoutandStdin, which are imported using the platform's shorthand, such aspf.Stdoutandpf.Stdin. - Roc's standard library does not include any effectful functions; they all come from the platform. Here are some links to popular platforms:
For more, check out: