Friendly

Besides having a friendly community, Roc also prioritizes being a user-friendly language. This impacts the syntax, semantics, and tools Roc ships with.

Syntax and Formatter

Roc's syntax isn't trivial, but there also isn't much of it to learn. It's designed to be uncluttered and unambiguous. A goal is that you can normally look at a piece of code and quickly get an accurate mental model of what it means, without having to think through several layers of indirection. Here are some examples:

Roc also ships with a source code formatter that helps you maintain a consistent style with little effort. The roc format command neatly formats your source code according to a common style, and it's designed with the time-saving feature of having no configuration options. This feature saves teams all the time they would otherwise spend debating which stylistic tweaks to settle on!

Helpful compiler

Roc's compiler is designed to help you out. It does complete type inference across all your code, and the type system is sound. This means you'll never get a runtime type mismatch if everything type-checked (including null exceptions; Roc doesn't have the billion-dollar mistake), and you also don't have to write any type annotations—the compiler can infer all the types in your program.

If there's a problem at compile time, the compiler is designed to report it in a helpful way. Here's an example:

── TYPE MISMATCH ─────── /home/my-roc-project/main.roc ─

Something is off with the then branch of this if:

4│      someInt : I64
5│      someInt =
6│          if someDecimal > 0 then
7│              someDecimal + 1
                ^^^^^^^^^^^^^^^

This branch is a fraction of type:

    Dec

But the type annotation on `someInt` says it should be:

    I64

Tip: You can convert between integers and fractions
using functions like `Num.toFrac` and `Num.round`.

If you like, you can run a program that has compile-time errors like this. (If the program reaches the error at runtime, it will crash.)

This lets you do things like trying out code that's only partially finished, or running tests for one part of your code base while other parts have compile errors. (Note that this feature is only partially completed, and often errors out; it has a ways to go before it works for all compile errors!)

Serialization inference

When dealing with serialized data, an important question is how and when that data will be decoded from a binary format (such as network packets or bytes on disk) into your program's data structures in memory.

A technique used in some popular languages today is to decode without validation. For example, some languages parse JSON using a function whose return type is unchecked at compile time (commonly called an any type). This technique has a low up-front cost, because it does not require specifying the expected shape of the JSON data.

Unfortunately, if there's any mismatch between the way that returned value ends up being used and the runtime shape of the JSON, it can result in errors that are time-consuming to debug because they are distant from (and may appear unrelated to) the JSON decoding where the problem originated. Since Roc has a sound type system, it does not have an any type, and cannot support this technique.

Another technique is to validate the serialized data against a schema specified at compile time, and give an error during decoding if the data doesn't match this schema. Serialization formats like protocol buffers require this approach, but some languages encourage (or require) doing it for all serialized data formats, which prevents decoding errors from propagating throughout the program and causing distant errors. Roc supports and encourages using this technique.

In addition to this, Roc also supports serialization inference. It has some characteristics of both other approaches:

This technique works by using Roc's type inference to infer the expected shape of serialized data based on how it's used in your program. Here's an example, using Decode.fromBytes to decode some JSON:

when Decode.fromBytes data Json.codec is
    Ok decoded -> # (use the decoded data here)
    Err err -> # handle the decoding failure

In this example, whether the Ok or Err branch gets taken at runtime is determined by the way the decoded value is used in the source code.

For example, if decoded is used like a record with a username field and an email field, both of which are strings, then this will fail at runtime if the JSON doesn't have fields with those names and those types. No type annotations are needed for this; it relies entirely on Roc's type inference, which by design can correctly infer types for your entire program even without annotations.

Serialization inference has a low up-front cost in the same way that the decode-without-validating technique does, but it doesn't have the downside of decoding failures propagating throughout your program to cause distant errors at runtime. (It also works for encoding; there is an Encode.toBytes function which encodes similarly to how Decode.fromBytes decodes.)

Explicitly writing out a schema has its own benefits that can balance out the extra up-front time investment, but having both techniques available means you can choose whatever will work best for you in a given scenario.

Testing

The roc test command runs a Roc program's tests. Each test is declared with the expect keyword, and can be as short as one line. For example, this is a complete test:

## One plus one should equal two.
expect 1 + 1 == 2

If the test fails, roc test will show you the source code of the expect, along with the values of any named variables inside it, so you don't have to separately check what they were.

If you write a documentation comment right before it (like ## One plus one should equal two here), it will appear in the test output, so you can use that to add some descriptive context to the test if you want to.

Inline expectations

You can also use expect in the middle of functions. This lets you verify assumptions that can't reasonably be encoded in types, but which can be checked at runtime. Similarly to assertions in other languages, these will run not only during normal program execution, but also during your tests—and they will fail the test if any of them fails.

Unlike assertions (and unlike the crash keyword), failed expects do not halt the program; instead, the failure will be reported and the program will continue. This means all expects can be safely removed during --optimize builds without affecting program behavior—and so --optimize does remove them. This means you can add inline expects without having to weigh each one's helpfulness against the performance cost of its runtime check, because they won't have any runtime cost after --optimize removes them.

In the future, there are plans to add built-in support for benchmarking, generative tests, snapshot tests, simulated I/O (so you don't have to actually run the real I/O operations, but also don't have to change your code to accommodate the tests), and "reproduction replays"—tests generated from a recording of what actually happened during a particular run of your program, which deterministically simulate all the I/O that happened.

Functional

Besides being designed to be fast and friendly, Roc is also a functional programming language.

What does functional mean here?