ReasonML: getting started

April 05, 2018 0 Comments

ReasonML: getting started

 

 

In this tutorial, we're going to build a small weather app using Reason. There's a link to the source code at the bottom of the page. This tutorial assumes a basic understanding of React, as we'll be using the ReasonReact bindings to build the app. If you haven't used React before, this article is a great place to start.

ReasonML

What is Reason?

Reason is a new syntax for OCaml, developed by Facebook with a heavy influence from JavaScript. It’s got 100% type coverage, which has resulted in a very powerful type system.

Reason is also suitable for cross-platform development. We can use BuckleScript to compile our code into (readable) JavaScript, which opens up the entire web platform. Thanks to OCaml, it's also possible to use Reason for native development.

In addition, Reason can access the entire JS and OCaml ecosystems, and offers ReasonReact for building UI components with ReactJS. There’s a useful page in the docs which explains the advantages in more detail!

Requirements

First of all, let's make sure we've got the right tools installed.

We'll be using Create React App to bootstrap the project. If you haven't used it before, install by running npm i -g create-react-app. There are two other packages we need to get started:

  • Reason CLI: the Reason toolchain. Check the installation docs.
    At the time of writing, macOS users can install by running npm i -g reason-cli@3.1.0-darwin

  • BuckleScript: npm i -g bs-platform

I'm also using the vscode-reasonml editor plugin. If you're using a different editor, check the list of plugins to find the right one for you.

Our first component

To get started, we'll create the boilerplate code for our app:

create-react-app weather-app --scripts-version reason-scripts

This gives us a basic App component:

We can compile & run this using yarn start. Let's take a look at a few interesting parts...

[%bs.raw {|require('./app.css')|}];

BuckleScript allows us to mix raw JavaScript into our Reason code, from a one-liner to an entire library (if we're just hacking around). This should be used sparingly, but can be a useful escape hatch whilst we're getting started.

let component = ReasonReact.statelessComponent("App");

We'll be using two types of ReasonReact component: statelessComponent and reducerComponent. Stateless components do what they say on the tin. Reducer components are stateful, and have Redux-like reducers built-in. We'll come onto these later.

let make = (~message, _children) => { ... }

This is the method that defines our component. The two parameters have different symbols: ~ is a labelled argument, meaning we can reference the parameter by name, and _ is a more explicit way of showing that the parameter isn't used (the compiler will give us a warning otherwise).

The ...component spread operator means that our make function is building upon the component we just defined, overwriting the defaults.

<h2> (ReasonReact.stringToElement(message)) </h2>

JSX in Reason is more strict than in normal React. Instead of just writing <h2>{message}</h2>, we have to explicitly convert the message string to a JSX element.

We'll be using this boilerplate when we build our own components later on.

Types in Reason

Let's create a new file, WeatherData.re. This will define the data structure and any related methods for our Weather record. To begin with, let's create the type:

Within this file, we can create new records using this data structure, and the compiler will know that it's a Weather item. From other files, we'll need to tell the compiler what the type is. In Reason, files can be referenced as modules, meaning we don't have to explicitly import them! We can just do this:

I mentioned earlier that Reason has 100% type coverage, but we've only defined our Weather type... where does the rest of the coverage come from? We could explicitly define a type for every variable we use, e.g. let greeting: string = "Hello"; but fortunately the OCaml system can infer types for us. So if we write let greeting = "Hello"; the compiler will still know that greeting is a string. This is a key concept in Reason and guarantees type safety.

Keeping state

Moving back to our project, let's modify app.re so it can store the data we want to display. This will involve:


  1. Defining the type of our state

  2. Setting our initial state (with some dummy data, for now)

  3. Defining actions that can be applied to state

  4. Defining reducers for the component to handle these

Actions define the different things we can do to manipulate state. For example, Add or Subtract. Reducers are pure functions which define how state should be affected by these actions, just like in Redux. They take the action and our previous state as parameters, and return an update type.

There are two new Reason concepts here: variants and pattern matching.

type action =
| WeatherLoaded(WeatherData.weather);

This is a variant: a data structure which represents a choice of different values (like enums). Each case in a variant must be capitalised, and can optionally receive parameters. In ReasonReact, actions are represented as variants. These can be used with the switch expression:

switch action {
| WeatherLoaded(newWeather) =>
ReasonReact.Update({ ... })
}

This is one of the most useful features in Reason. Here we're pattern matching action, based on the parameter we receive in the reducer() method. The compiler knows that our switch statement needs to handle every case of action. If we forget to handle a case, the compiler knows, and will tell us!

Pattern matching error

The Reason compiler catching unhandled cases.

We used destructuring to access the value of newWeather in a previous example. We can also use this to match actions based on the values they contain. This gives us some very powerful behaviour!

Fetching data

So far, our app renders the dummy weather data - now let's load it from an API. We'll put the methods for fetching and parsing data in our existing WeatherData.re file.

Firstly, we need to install bs-fetch: npm i bs-fetch and bs-json: npm i @glennsl/bs-json. We also need to add them to our bsconfig.json:

{
...
"bs-dependencies": [
"bs-fetch"
"@glennsl/bs-json"
]
}

We'll be using the Yahoo Weather API to fetch our data. Our getWeather() method will call the API, then parse the result using parseWeatherResultsJson(), before resolving with a weather item:

Json.parseOrRaise(json) |> Json.Decode.(at([
...
], parseWeatherJson));

This parses the JSON string response, before traversing the data via the specified fields. It then uses the parseWeatherJson() method to parse the data found inside the condition field.

Json.Decode.{
summary: field("text", string, json),
temp: float_of_string(field("temp", string, json))
};

In this snippet, field and string are properties of Json.Decode. This new syntax "opens" Json.Decode, so its properties can be used freely within the curly brackets (instead of repeating Json.Decode.foo). The code generates a weather item, using the text and temp fields to assign summary and temp values.

floatofstring does exactly what you'd expect: it converts the temperature from a string (as we get from the API) into a float.

Updating state

Now we've got a getWeather() method which returns a promise, we need to call this when our App component loads. ReasonReact has a similar set of lifecycle methods to React.js, with a few small differences. We'll be using the didMount lifecycle method for making the API call to fetch the weather.

First of all, we need to change our state to show that it's possible to not have a weather item in state - we'll get rid of the dummy data. option() is a built-in variant in Reason, which describes a "nullable" value:

type option('a) = None | Some('a);

We need to specify None in our state type and initial state, and Some(weather) in our WeatherLoaded reducer:

Now we can actually make the API request when our component mounts. Looking at the code below, handleWeatherLoaded is a method which dispatches our WeatherLoaded action to the reducer. When the promise resolves, it will be handled by our reducer, and the state will be updated!

Note: it's important to return ReasonReact.NoUpdate from most component lifecycles. The reducer will handle all state changes at the next opportunity.

If we run our app now, we'll run into an error... We're currently trying to render information about self.state.weather, but this is set to None until we receive a response from the API. Let's update our App component to show a loading message while we wait:

And the result...

ReasonReact loading message

Error handling

One thing we haven't thought about is what happens if we can't load our data. What if the API is down, or it returns something we're not expecting? We'll need to recognise this and reject the promise:

switch (parseWeatherResultsJson(jsonText)) {
| exception e => reject(e);
| weather => resolve(weather);
};

This switch statement tries to parse the API response. If an exception is raised, it will reject the promise with that error. If the parsing was successful, the promise will be resolved with the weather item.

Next, we'll change our state to let us recognise if an error has occurred. Let's create a new type which adds an Error case to our previous Some('a) or None.

Whilst doing this, we'll also need to add an Error case to our render function - I'll let you add that yourself. Finally, we need to create a new action and reducer to be used when our getWeather() promise rejects.

These are concepts we've used already, but it's useful to let the user know if something goes wrong. We don't want to leave them hanging with a "loading" message!

There we have it, our first ReasonReact web app. Nice work! We've covered a lot of new concepts, but hopefully you can already see some of the benefits of using Reason.

If you found this interesting & would like to see another post building upon this, please let me know by clicking a reaction below! ❤️ 🦄 🔖

Further reading

A little more context, including a link to the source code.

Exploring ReasonML and functional programming - a free online book about (you guessed it) Reason and FP.

OSS projects



  • bs-jest - BuckleScript bindings for Jest.


  • lwt-node - a Reason implementation of the Node.js API


  • reason-apollo - bindings for Apollo client and React Apollo

Other


Tag cloud