- Published on
Setting up a new Reason project
- Authors
- Name
- Chris Armstrong
- @ckarmstrong
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.
Create a new directory for your project
mkdir my-reason-project
Setup an empty
package.json
(instead of usingnpm 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 addsmerlin
andocaml-lsp-server
for editor integration.Modules prefixed with the
@opam
namespace are downloaded from OPAM, OCaml's package repository, while the rest from fromnpm
.Run
esy install
, which will download your dependencies (you should run it after updating the dependencies list)Create a
bin
directory and a newdune
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
.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
Create your entry-point file at
bin/MyReasonProject.re
bin/MyReasonProject.re
let () = print_endline("Hello World!");
Create a blank file in the root directory called
my-reason-project.opam
:touch my-reason-project.opam
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 foresy build
- all the esy commands have short equivalents)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.
Create your directory and add a new
dune
file to it:library/dune
( library (name Library) )
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, ())] } | [] => [] }
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'sdune
file - whitespace in
dune
files is ignored - we can rearrange it as needed
- the name of the library we specified here (
Consume the library in our code.
Note that for a libary called
Library
and a module in it calledListUtil
(as per our example), the namespace forreducei
will beLibrary.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)
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)
)