This tutorial intends to provide a comprehensive, but relatively short introduction to Reason.
Reason is a programming language built on top of OCaml. It provides functional and object-oriented features with type-safety and focus on performance. It was created at Facebook. Its syntax is similar to JavaScript. The intention is to make interoperation with JavaScript and adoption by JavaScript programmers easier. Reason can access both JavaScript and OCaml ecosystems. OCaml was created in 1996. It is a functional programming language with infered types.
The Reason website contains an online playground. It allows to play with the language and see how the generated JavaScript looks like. It can also convert from OCaml to Reason.
Reason is compiled to OCaml’s abstract syntax tree. This makes Reason a transpiler. OCaml cannot be run directly in the browser. The AST can be converted to various targets. BuckleScript can be used to compile that AST to JavaScript. It also provides the interop between OCaml and JavaScript ecosystems.
BuckleScript is extremly fast and generates readable JavaScript. It also provides the Foreign Function Interface (FFI) to allow interoperability with the JavaScript existing libraries. Check BuckleScript benchmarks. BuckleScript is used at Facebook by the Messanger team and at Google by WebAssembly spec interpreter. Check the Bucklescript demo here. BuckleScript was created by Hongbo Zhang.
We will use BuckleScript to generate a Reason project. The tool provides ready-to-use project templates known as themes
.
Let’s start by installing bs-platform
globally:
npm install -g bs-platform
We can now use bsb
binary provided by bs-platform
to generate a project scaffold. We will use basic-reason
template to start with the most basic Reason project structure.
bsb -init reason-1 -theme basic-reason
Making directory reason-1 Symlink bs-platform in /Users/zaiste/code/reason-1
Here’s the Reason directory structure generated from basic-reason
template via BuckleScript:
. ├── README.md ├── bsconfig.json ├── lib ├── node_modules ├── package.json └── src └── Demo.re
bsconfig.json
contains BuckleScript configuration for a Reason project. It allows to specify files to compile via sources
, BuckleScript dependencies via bs-dependencies
, additional flags for the compiler and more.
Next step is to build the project. This will take Reason code and pass it through BuckleScript to generate JavaScript. By default the compiler will target Node.js.
npm run build
> [email protected] build /Users/zaiste/code/reason-1 > bsb -make-world ninja: Entering directory `lib/bs' [3/3] Building src/Demo.mlast.d [1/1] Building src/Demo-MyFirstReasonml.cmj
Finally we can run our application by using node
on the files generated by BuckleScript.
node src/Demo.bs.js
Hello, BuckleScript and Reason!
In this section, I will go over the syntax elements that I found the peculiar, new or just different.
In Reason files are modules. There are no require
or import
statements as in JavaScript or similar programming languages. The module definitions must be prefixed with the module name to work externally. This feature comes from OCaml. As a result you can freely move the module files in the filesystem without the need to modify the code.
Functions are defined using let
and =>
.
let greet = name => Js.log("Hello, " ++ name "!");
greet("Zaiste");
The ++
operator is used to concatenate strings.
Function’s input arguments can be labelled. This makes the function invocation more explicit: passed-in values no longer need to follow the arguments order from the function definition. Prefixing the argument name with ~
makes it labelled.
let greet = (~name, ~location) => Js.log("Hello, " ++ name "! You're in " ++ location);
greet(~location="Vienna", ~name="Zaiste")
A variant is a data structure that holds a value from a fixed set of values. This is also known as tagged or disjoint union or algebraic data types. Each case in a variant must be capitalised. Optionally, it can receive parameters.
type animal = | Dog | Cat | Bird;
This is a record
let p = { name: "Zaiste", age: 13 }
Records need explicit type definition.
type person = { name: string, age: int };
In the scope of a module, the type will be inherited: the p
binding will be recognized as person
type. Outside of a module, you can reference the type by just prefixing it with file name.
let p: Person.person = { name: 'Sean', age: 12 };
There is a convention to create a module per type and name the type as t
to avoid the repetition i.e. Person.t
instead of Person.person
.
There is a built-in support for Promises via BuckleScript, provided as JS.Promise
module. Here's an example of making an API call using Fetch API:
Js.Promise.( Fetch.fetch(endpoint) |> then_(Fetch.Response.json) |> then_(json => doSomethingOnResponse(json) |> resolve) )
You need to use then_
as then
is reserved word in OCaml.
Pattern matching is a dispatch mechanism based on the shape of the provided value. In Reason, pattern matching is implemented with switch
statement. It can be used with a variant type or as destructuring mechanism.
switch pet { | Dog => "woof" | Cat => "meow" | Bird => "chirp" };
We can use pattern matching for list destructuring:
let numbers = ["1", "2", "3", "4"]; switch numbers { | [] => "Empty" | [n1] => "Only one number: " ++ n1 | [n1, n2] => "Only two numbers" | [n1, _, n3, ...rest] => "At least three numbers" };
Or, we can use it for record destructuring
let project = { name: "Huncwot", size: 101101, forks: 42, deps: [{name: "axios"}, {name: "sqlite3"}]}
switch project { | {name: "Huncwot", deps} => "Matching by `name`" | {location, years: [{name: "axios"}, ...rest]} => "Matching by one of `deps`" | project => "Any other situation" }
option()
is a built-in variant in Reason describing "nullable" values:
type option('a) = None | Some('a);
unit
means "nothing"unit => unit
is a signature of a function that doesn't accept any input parameters and doesn't return any values; mostly used for callback functionsReasonReact is a Reason built-in feature for creating React applications.
Let’s create a ReasonReact project using BuckleScript and its react
template.
bsb -init reasonreact-1 -theme react
This method is recommended by Reason team for scaffolding ReasonReact projects. It is also possible to use yarn with reason-scripts template for a more complete starting point.
ReasonReact provides two types of components: statelessComponent
and reducerComponent
. Contrary to stateless components, reducer components are stateful providing Redux-like reducers.
let s = ReasonReact.string
let component = ReasonReact.statelessComponent("App");
let make = (~message, _children) => { ...component, render: _self => <h1 class="header">(s(message))</h1> };
As described earlier ~
designates a labelled argument to freely order function's input parameters. _
in the binding name tells the compiler that the argument isn't used in the body of that function. The spread operator (...
) alongside of component
means that we extend an existing component. In this example we also overwrite the render
function.
JSX in Reason is more strict than in React: we need to explicitly wrap strings with ReasonReact.string()
. For convenience, I've created a shorter binding called s
to use it conveniently inside JSX block.
Let’s build a ReactReason application that goes beyond displaying predefined data. We will create a GitHub viewer for trending repositories. The intention is to showcase how to integrate with an external API, how to manage state and how to use React’s lifecycle methods methods.
For the purpose of this example we will use reason-scripts to bootstrap our Reason project.
yarn create react-app reasonreact-github --scripts-version reason-scripts
Install dependencies:
cd reasonreact-github yarn
Start it with:
yarn start
Repository is the central concept in this application. Let’s start by defining a type to describe that entity. We will put it inside a separate module called Repo
.
type t = { name: string, size: int, forks: int };
From now on we can refer to this type with Repo.t
from any Reason file in our application without the need of requiring it.
We’ve already seen a stateless component. Now let’s create a component that has state. In our context we will be using RepoList
component managing a list of trending repositories fetched from GitHub's API.
Let’s start by defining the type for the state managed by RepoList
component.
type state = { repos: list(Repo.t) };
There is, however, a catch. Initially, before the list of trending repositories is fetched from GitHub API, the repos
is undefined. Reason type system doesn't allow us to have undefined value though. We could model that initial state with an empty list, but this is not optimal. Empty list could also mean that our query for fetching trending repositories didn't return any results.
Let’s use Reason’s optional values to deal with that situation.
type state = { repos: option(list(Repo.t)) }
Next step is to define possible actions for that component. In ReasonReact, actions are represented as variants. For now we will only have one action called ReposFetched
.
type action = | ReposFetched(list(Repo.t));
In order to create a stateful component in ReasonReact we need to use reducerComponent()
function.
let component = ReasonReact.reducerComponent("App");
Such component allows to define a reducer which describes how the state is transformed in response to actions. A reducer takes an action along with the current state as input and returns the new state as output. Reducers must be pure functions.
reducer: (action, _prevState) => { switch action { | ReposFetched(repos) => ReasonReact.Update({repos: Some(repos)}) } }
We’re pattern matching action, based on the parameter we receive in the reducer() method. Pattern matching must be exhaustive. All variant values must be matched. reducer
definition is placed inside component's main
function.
To finish off component’s definition, let’s define its initial state:
initialState: () => { repos: Some([ {name: "Huncwot", size: 11011, forks: 42} ]) }
We will use [bs-fetch](https://github.com/reasonml-community/bs-fetch)
to fetch data from an external API. It is a BuckleScript library that acts as a thin layer on top of the Fetch API. Once the data is fetched, we will use bs-json
to extract fields we are interested in.
Start by installing bs-fetch
and bs-json
:
npm i bs-fetch @glennsl/bs-json
Add them to bs-dependencies
in your bsconfig.json
:
{ "bs-dependencies": [ ..., "bs-fetch", "@glennsl/bs-json" ] }
We defined our Repo
type as a set of three fields: name
, size
and forks
. Once the payload is fetched from GitHub API we parse it to extract those three fields.
let parse = json => Json.Decode.{ name: json |> field("name", string), size: json |> field("size", int), forks: json |> field("forks", int), };
field
is a method of Json.Decode
. The Json.Decode.{ ... }
(mind the dot) opens Json.Decode
module. Its properties can now be used within these curly brackets without the need of prefixing with Json.Decode
.
Since GitHub returns repos under items
, let's define another function to get that list.
let extract = (fields, json) => Json.Decode.( json |> at(fields, list(parse)));
Finally we can make a request and pass the returned data through our parsing functions:
let list = () => Js.Promise.( Fetch.fetch(endpoint) |> then_(Fetch.Response.json) |> then_(text => extract(["items"], text) |> resolve) );
Let’s use didMount
lifecycle method to trigger the fetch of repositories from GitHub API.
didMount: self => { let handle = repos => self.send(ReposFetched(repos)); Repo.list() |> Js.Promise.then_(repos => { handle(repos); Js.Promise.resolve(); }) |> ignore; }
handle
is a method that dispatches ReposFetched
action to the reducer. Once the promise resolves, the action will carry fetched repositories to the reducer. This will update our state.
Since we distinguish between non initialized state and an empty list of repositories, it is straightforward to handle the initial loading in progress message.
render: self => <div> ( switch self.state.repos { | None => s("Loading repositories..."); | Some([]) => s("Emtpy list") | Some(repos) => <ul> ( repos |> List.map((repo: Repo.t) => <li> (s(repo.name)) </li>) |> Array.of_list |> ReasonReact.array ) </ul> } ) </div> };
TBW
Types for CSS with [bs-css](https://github.com/SentiaAnalytics/bs-css)
.
yarn add bs-css
"bs-dependencies": [ ..., "bs-css" ]
let style = Css.( { "header": style([backgroundColor(rgba(111, 37, 35, 1.0)), display(Flex)]), "title": style([color(white), fontSize(px(28)), fontWeight(Bold)]), } );
let make = _children => { ...component, render: _self => <header className=style##header> <h1 className=style##title> (s("This is title")) </h1> </header> };
rtop
is an interactive command line for Reason.[@bs...]
Bucklescript annotations for FFImodule History = { type h; [@bs.send] external goBack : h => unit = ""; [@bs.send] external goForward : h => unit = ""; [@bs.send] external go : (h, ~jumps: int) => unit = ""; [@bs.get] external length : h => int = ""; };
BuckleScript allows us to mix raw JavaScript with Reason code.
[%bs.raw {|require('./app.css')|}];
Originally published at zaiste.net.