Speeding up ReScript compilation using interface files
Tags: rescript, compilers, performance
Published: 2022-10-20 16:18:52

[ReScript](https://rescript-lang.org/) compilation speeds are really fast out of the box. But, sometimes editing certain files can take longer to compile than you'd expect it to. 

In this article we'll explore a strategy for speeding up compilation by leveraging interface files. We'll also dive into _why_ using interface files can speed up compilation. And, by doing that we'll dig a bit into how the ReScript compiler works internally.

Let's start from the beginning though, with a few concepts that are necessary to understand to be able to follow the rest of the article. You can skip the coming sections if you're already familiar with modules, interfaces and implementation files in ReScript.

**TLDR;** If you have files that, when edited, cause longer compile times than you'd expect, see if adding interface files for those files help. If you're changing the implementation often, but not the interface, you might see massive speed increases from adding interface files.

## Modules and files
> Read the official docs on modules: [Modules in ReScript](https://rescript-lang.org/docs/manual/latest/module)

Quoting the official docs, "modules are like mini files!". A module is a collection of types, values, functions, and so on.

For this article's sake, pay special attention to the fact that every _file_ is automatically a module. So, if you have `MyFile.res`, that file's contents will be available for you to use under the `MyFile` module globally.

We'll use the words module and file interchangeably, so please keep in mind that every file is a module.

## Implementation and interface files
> Read the official docs: [Interface and implementation files in ReScript](https://rescript-lang.org/docs/gentype/latest/usage#interface-resi-and-implementation-res-files)

ReScript lets you separate interface and implementation in various ways. 

Interface here means a description of what the outside world has access to in your module. This is typically called the _signature_ of the code. It's essentially type information.

Implementation means the code you write to fulfill that description. 

A simple example: in your interface, you tell the world there's going to be a value called `usernames` available that's an array of strings. And in the implementation, you produce that actual array of strings called `usernames`.

```rescript
// AllUsers.resi
let usernames: array<string>
```

```rescript
// AllUsers.res
let usernames = allUsersInSystem->Belt.Array.map(user => user.name)
```

Now, the outside world doesn't need to care about _how_ the `usernames` array is produced. That's the _implementation_. It just needs to know that that array will be there.

In ReScript, implementation files are suffixed with `.res`, and interface files are suffixed with `.resi`. So, if you have `AllUsers.res`, you add an interface by adding a `AllUsers.resi` file.

## So, how can interface files speed up compilation?
Why does all of this matter to compilation speed? Let's dive into how the ReScript compiler works a bit.

When you make a change to your code and then recompile, the ReScript compiler needs to type check all affected parts of your program to make sure everything still adds up. The ReScript compiler is quite intelligent and mostly knows how to check the minimum set of files needed as you change things. This is a huge part of what makes the compiler so fast.

Type checking is, very simplified, about ensuring that all the types match each other in the project. Is the parts of your program expecting (and receiving) the right things?

The compiler looks at each file (module), and then does one of two things depending on whether that file has an interface file or not.

1. If it has an interface file, the compiler checks the implementation file to make sure that the implementation matches the interface. Are you delivering what you promised in the interface? If all checks out, it moves on. If not, it errors.
2. If it doesn't have an interface file, the compiler needs to derive an interface by itself for the file by looking at the implementation file directly. So it looks at the code you've written, infers the signature of that code ("`let myArray = ["some string"]` is an array of strings"), and then uses that inferred signature as an interface for that file as it moves on. 

And this is because the type checker works on the type information from the _interface_, not on the actual code implementing that interface.

## Compilation slows down because the compiler needs to derive the interface again and again
This is what it all comes down to. If there's no interface file, the compiler needs to derive that interface itself by looking at the implementation. 

And it needs to do that every time the file changes.

And after it has done that, it needs to recheck all of the modules depending on the file you changed to make sure things still type check.

But, if there is an interface file, all the compiler needs to check is that your implementation still matches the interface. It doesn't need to check anything else. It can safely assume that any other file that's depending on this file and has compiled successfully with the current interface is still valid. Because the _interface_ hasn't changed, just the implementation for that interface.

## A good strategy for interface files
How much you make use of interface files or not is largely a philosophical question that I'm going to refrain from discussing here. Some people never use them. Some people use them a lot. I personally use them some, but not a lot.

One strategy I will leave you with though is to be mindful of modules you have that are used by many other modules. Those might be a good idea to use interface files for, not the least because of the speed benefits discussed in this article.

So, be mindful of when editing certain files cause longer compilation times than you'd expect. See if adding an interface file might help.

## Editor tooling helps make life with interface files easier
The [ReScript VSCode extension](https://github.com/rescript-lang/rescript-vscode) has two goodies that help make life with interface files easier that I want to mention.

### Automatically generating an interface file from an implementation file
If you have a `.res` file that you want to create an interface file for, you can use the command `> ReScript: Create an interface file for this implementation file` (`Cmd/Ctrl + P` brings up the command prompt). You'll get a fully filled out `.resi` file right next to your `.res` file that you can edit to your liking.

Quite convenient.

### Switching between interface and implementation files
The extension makes it easy to jump between the `.res` and `.resi` file for a module via the command `> ReScript: Switch implementation/interface`. Running that command will jump to the `.resi` file if you're in a `.res` file (and the `.resi` file exists of course), and running it from a `.resi` file will jump to the corresponding `.res` file.

## Wrapping up
There's a ton more to say when it comes to what you can use interface files for, both in terms of features, but also in terms of techniques for a good developer workflow. This article focuses on the performance aspects of interface files, but I'm likely going to cover the other points too at some point.

Until then, thank you for reading!