If you're feeling adventurous, you can try Roc's new bleeding-edge rewritten compiler. We're really excited about it!
Be warned, this is far from a polished experience! Expect missing features, bugs, and almost no documentation (everything on roc-lang.org is referring to the old compiler and the old design; we haven't updated any of it yet).
The purpose of this document is to give you a sense of how the revised language and new compiler work, since roc-lang.org is all about the old stuff.
By the way, if you want help, the best place to get it is Roc Zulip - you're welcome to ask any questions you have in #beginners.
With those disclaimers in mind, let's get into the adventure!
First, grab a nightly build of the new compiler. It'll have an executable named roc (or roc.exe on Windows). You'll want to [put that executable on your PATH] - you'll know it worked if you can run roc version in a terminal and see it print something like Roc compiler version release-fast-123c5d78 (the hash at the very end there will be different from this one, as it changes with each nightly release).
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.
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!".
Note that name must be a string! Roc's string interpolation does not automatically convert other types
to strings. 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 string interpolation, although it all has to be on a single line.
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.
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 this one. That's because actually 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 = {
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:
echo!({
for ... {
}
some_expression
})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 "TODO add proper error handling."
}
# ...the rest of the function would go here
}The old tutorial for Roc alpha4 has a useful section on crash. The section on crashing for error handling
is especially important, and has been copy/pasted here:
crashis not for error handling. The reason Roc has acrashkeyword 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 (likeTry) and then handled without crashing. For example, by having the application report that something went wrong, and then continue running from there.
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.
Note:
--opt=speeddoes not discardexpects yet but it could be implemented at any moment.
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 all pure functions that are run on constants at compile time when possible. (This is known as constant folding, and Roc does it as much as possible.) When you put a
dbginside a pure function, and that pure function gets run at compile time, you'll see thedbgoutput at compile tie. - 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 animials 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 exhaustively covered all possible cases. In fact, if you tried to add an _ => branch, the compiler
would give a warning that it was unreachable.
Note: As of December 1, 2025, exhaustiveness checking has not been ported over from the old compiler to the new one, so you won't actually get these errors yet!
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.
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:
Bool := [True, False]This is a nominal type definition.
| Old | New |
|---|---|
List U8 |
List(U8) |
if/then/else |
if/else |
Bool.true/Bool.false |
Bool.True/Bool.False |
Result |
Try |
Inspect.to_str |
Str.inspect |
Num.to_str(123) |
123.to_str() |
The initial hello world example at the start is a special kind of app, a headerless app. Many Roc apps you encounter will have a header similar to this one:
app [main!] { pf: platform "https://github.com/lukewilliamboswell/roc-platform-template-zig/releases/download/0.6/2BfGn4M9uWJNhDVeMghGeXNVDFijMfPsmmVeo6M4QjKX.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 writepg: "https://..."to add a dependency on roc-pg (uses Roc alpha4) for PostgreSQL access, at which point we'd be able to do things likeimport pg.Cmdand so on.
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
- The platform provides all the I/O primitives (such as
StdoutandStdin- they are imported aspf.Stdoutandpf.Stdin) - Roc's standard library does not include any effectful functions; they all come from the platform. Most published platforms still use the old version of the compiler (e.g. basic-cli and basic-webserver) but ports are in progress!
For more, check out:
- All builtin functions
- A single file demonstrating all Roc syntax
- Roc getting started guide (community contributed)