Published on

Setting up a new Reason project

Authors

Introduction

Reason is a syntax for OCaml that provides a more JavaScript like language, but with full access to the OCaml ecosystem. Since the emergence of ReScript as a separate project, it now only targets native development, so we'll focus on that.

Editor Support

If you're using VSCode, install the OCaml Platform plugin, which offers built-in support for native Reason development.

The plugin uses merlin with the ocaml-lsp-server to provide syntax highlighting, code completion, hover support and code navigation.

esy

Reason's build tooling inherits from the OCaml ecosystem, but extends it to make it easier to use.

esy is the main integrated package-management and build tool for Reason development. It wraps around OCaml tooling (such as dune and merlin) to simplify project build, editor integration and dependency management. It can be used for both OCaml and Reason development.

Package Management

OCaml packages are distributed on opam, but as you will see with esy, some Reason packages can be found on npm too. esy can be used to install both.

esy automatically resolves transitive dependencies, and generates a lock file containing their versions to make it easier to replicate your setup on multiple machines without storing the downloaded dependencies

Build

OCaml (and by extension, Reason) projects are typically built with dune, but also older projects may use ocamlbuild. esy will wrap around dune to build your project.

It also creates an opam sandbox, using a private version of the compiler for each project and copies of your dependencies. This makes it easier to have mulitple projects, each with their own versions of dependencies, running safely on the same system, without generating conflicts.

Prerequisites

Install the latest version of esy with npm:

npm i -g esy@latest

Creating a new project

It's normally recommended to fork the hello-reason example, but we'll create our project from scratch so that we learn how everything fits together.

  1. Create a new directory for your project

    mkdir my-reason-project
    
  2. Setup an empty package.json (instead of using npm init) with our initial dependencies:

    package.json

    {
      "dependencies": {
        "ocaml": "4.12.x",
        "@opam/reason": "*",
        "@opam/dune": "*"
      },
      "devDependencies": {
        "@opam/merlin": "*",
        "@opam/ocaml-lsp-server": "*"
      }
    }
    

    This installs the Reason compiler, the underlying OCaml version and dune, which is standard way for compiling and bundling OCaml/Reason executables. It also adds merlin and ocaml-lsp-server for editor integration.

    Modules prefixed with the @opam namespace are downloaded from OPAM, OCaml's package repository, while the rest from from npm.

  3. Run esy install, which will download your dependencies (you should run it after updating the dependencies list)

  4. Create a bin directory and a new dune file in it:

    bin/dune

    (executable (name MyReasonProject) (public_name MyReasonProject))
    

    This will build an executable called MyReasonProject.exe, looking for an entry-point file called MyReasonProject.re.

  5. Create your dune-project file at the project root:

    dune-project

    (lang dune 2.7)
    (name my-reason-project)
    

    This will set our project name for when we want to run builds

  6. Create your entry-point file at bin/MyReasonProject.re

    bin/MyReasonProject.re

    let () = print_endline("Hello World!");
    
  7. Create a blank file in the root directory called my-reason-project.opam:

    touch my-reason-project.opam
    
  8. We can build our project by running

    esy b dune build
    

    and then running it with

    esy b dune exec MyReasonProject
    

    esy builds your project in a sandbox. You can find the resulting file in your project _esy/default/build/default/bin/MyReasonProject.exe

    (note esy b is short for esy build - all the esy commands have short equivalents)

  9. The build and run commands can be a bit clumsy, so we can codify them as scripts in our package.json

    package.json

{
  "esy": {
    "build": "dune build -p #{self.name}"
  }
}

now we can run the build and execute with:

esy b
esy x MyReasonProject

Editing your project

Use VSCode with the OCaml Platform as mentioned above, or run esy vim to use vim with the merlin integration configured your local project.

Run your project

As noted earlier, esy outputs its executables under _esy/default/build/default/.

You can the executable directly e.g.

_esy/default/build/default/bin/MyReasonProject.exe

or using esy x <binary> e.g.

esy x MyReasonProject

Splitting up your project over multiple files

Each file is a module in Reason, so you can just create new files in the same directory and refer to them in your code by the file name (minus .re). Every let value you declare is automatically exported (unless you declare an rei interface file).

For example, if you create a file called Name.re:

bin/Name.re

let generate = () => "Chris";

you can call the generate function from another file with Name.generate().

Structuring your project over multiple directories with internal libraries

You can split up your code across multiple files in the bin directory, but you will get to the point where you will want to structure your code into multiple directories.

OCaml assumes all the source code files are in the same directory, so to split them up, we need to create an internal library for each directory with its own dune configuration.

  1. Create your directory and add a new dune file to it:

    library/dune

    (
    library
    (name Library)
    )
    
  2. Add code to your new directory (e.g. add a new ListUtil reducei function):

    library/ListUtil.re

    /* reduce with auto-incrementing integer argument */
    let rec reducei = (ls, reducer, ~index=?, ()) => switch ls {
      | [item, ...rest] => {
        let index = Option.value(index, ~default=0)
        let reduced = reducer(item, ~index);
        [reduced, ...reducei(rest, reducer, ~index=index + 1, ())]
        }
      | [] => []
    }
    
  3. In your main dune file, include the internal library as a dependency:

    bin/dune

    (executable
     (name MyReasonProject)
     (public_name MyReasonProject)
     (libraries Library)
    )
    

    note that:

    • the name of the library we specified here (Library) is the same as the name in the library's dune file
    • whitespace in dune files is ignored - we can rearrange it as needed
  4. Consume the library in our code.

    Note that for a libary called Library and a module in it called ListUtil (as per our example), the namespace for reducei will be Library.ListUtil.reducei:

    bin/MyReasonProject.re

    let myList = [1, 2, 3, 4]
    let myTransformedList = Library.ListUtil.reducei(
     myList,
     (item, ~index) => item + index,
     ()
    )
    
    // Helper to print out an integer list
    let printIntList =
      Format.pp_print_list(
        ~pp_sep=(fmt, ()) => Format.pp_print_string(fmt, ", "),
        Format.pp_print_int,
        Format.std_formatter,
      );
    
    let () => printIntList(myTransformedList)
    
    
  5. Re-run esy build to re-build the new library and main project executable

Add an external dependency

esy can either install packages from npm or opam (using the special @opam prefix) using esy add. The dependency will be resolved and added to your package.json file.

For example to install jsonm from opam:

esy add @opam/jsonm

or to install consolelib from npm:

esy add @reason-native/console

After that, you need to add the library to the libraries clause of your dune file (where the library will be used). The name that is used is library dependent (you will need to refer to the library documentation), as it will not necessarily be the same as the opam or npm name.

e.g.

(
  executable
  (name MyReasonProject)
  (libraries yojson console.lib)
)