Reducer composition in Reason

May 21, 2016

Reason is a new syntax and set of developer tools for the OCaml programming language. Of all the statically typed functional languages available I've always found OCaml the most interesting. OCaml (and I hope, by extension Reason) seems to be a very pragmatic language, used in many real world situations (fftw, mirage, flow etc.) in a wide variety of fields. There is also really good online learning materials for beginners like myself. When Reason was released last week I couldn't wait to dig in and learn more about both Reason itself and the underlying concepts in OCaml.

A word of warning: I am not very familiar with statically typed functional programming. I've dabbled a little bit in Haskell, OCaml and Elm but never created anything substantial. Different languages often solve problems in very different ways and what I've attempted in this post might very well be something you would never do in this kind of programming language. This is a learning exercise for me and I have not much prior experience to guide me.

The problem I'm trying to solve is the same one as in my previous post (Redux reducers in Clojure) which is a reimplementation of the combineReducers function from the redux library.

The idea is that you have a potentially large immutable store and a reducer function which, given some action will create a new version of the store.

The store can contain many different kinds of entities but often has a rigid base structure. In this example the store looks kind of like

{
  contacts: _
  emails: {
    inbox: _
    sent: _
  }
}

There are also many different types of actions, for example

  • Add an email to the inbox
  • Remove a contact
  • etc.

To recap: There is one immutable store and one reducer function which should handle many different types of actions and route the desired update to the correct position in the store.

The challenge is to somehow build this top level reducer function out of smaller pieces in an attempt to keep things more manageable. This is solved quite elegantly in redux with its combineReducer higher level function. You create smaller reducer functions and combine them into the top level one:

const rootReducer = combineReducers({
  contacts: contactsReducer,
  emails: combineReducers({
    inbox: inboxReducer,
    sent: sentReducer
  })
});

But enough with the Javascript. This post will attempt to build something similar in Reason.

In Clojure (and Javascript) you often use heterogeneous collections such as maps, vectors and sets to represent things like (in this example) contacts and emails. This has the advantage that you can jump straight to the implementation. In a statically typed language like Reason you instead invest more time in modelling and specifying the data types your program handles. The hope is that the result will be less prone to certain kinds of type related bugs. With data and function types rigourously specified the code can perhaps even be more maintainable as many design decisions are more explicit.

Domain types

A contact is a record with an id and an email:

type contact = {
  id: int64,
  email: string
};

We will model our contacts as an immutable map where the id of a contact maps to the contact itself. For this we need to create a new Contacts module:

let module Contacts = Map.Make Int64;

The Contacts module can be thought of as a map implementation specialised at handlings maps with keys of type int64. This module will contain all the functions we need to add/remove contacts, and of course lookup contacts by id. I find it very interesting that you need to specify the type of the key at this point, but not the value type.

An email is also represented as a record:

type email = {
  from_addr: string,
  to_addr: string,
  title: string,
  body: string
};

For some reason I chose in the previous post to model the inbox as a set of emails. This turned out to be an excellent challenge. Records in Reason are sortable if the corresponding values are. Since all values in our email type are strings we can use the compare function directly:

let email1 = {
  from_addr: "flint@example.com",
  to_addr: "strax@example.com",
  title: "Hi!",
  body: "..."
};

let email2 = {
  from_addr: "strax@example.com",
  to_addr: "flint@example.com",
  title: "Re: Hi!",
  body: "..."
};

compare email1 email2;

The specific sort order doesn't actually matter for our use case but it is important that some consistent sort order is defined for the sake of efficient set operations (for example, is a given email in the inbox or not).

To be able to create "a set of emails" we need to define an Email module that describes how to sort emails. If we wanted to we could define our own way of sorting at this point.

let module Email = {
  type t = email;
  let compare = compare;
};

We can now create an Inbox module which will, similarly to the Contacts map above, contain a set implementation that can efficiently store and retrieve emails.

let module Inbox = Set.Make Email;

We will use functions like Inbox.add and Inbox.remove later for adding/removing emails.

Finally, we will also store "sent" emails in the store. This will simply be represented as a linked list of emails (which has the type list email in Reason).

Actions

Actions will also be statically typed, modelled as variants instead of records. We could have a single action type which lists all the possible actions, but I chose instead to split the actions into contact_action and inbox_action.

type contact_action =
  | AddContact of contact
  | RemoveContact of int64;

type inbox_action =
  | AddToInbox of email
  | RemoveFromInbox of email;

These two types are composed into a top level action type:

type action =
  | ContactAction of contact_action
  | InboxAction of inbox_action
  | EmailSentAction of email;

Reducers

A "reducing function" or "reducer" is the function that's the first argument to List.fold_left (often called reduce in other languages). The reducers we will write will take some piece of the store as the first argument and an action as the second. The result will be a new store value with the action incorporated. Let's start with the contact_reducer:

let contact_reducer contacts action =>
  switch action {
  | ContactAction (AddContact contact) =>
    Contacts.add contact.id contact contacts
  | ContactAction (RemoveContact id) =>
    Contacts.remove id contacts
  | _ => contacts
  };

The reducer takes an action of type action and not the smaller type contact_action. This is because we are modelling how reducers work in redux. The rationale is that a single action can affect many parts of the store. Say for example that the contact record also contained a count where we counted the number of interactions (sent and received emails). We could then hook into other kinds of actions, e.g. an SentEmailAction to be able to keep that count without creating additional InboxActions. Also, if the action is not handled the contacts are returned unchanged (handled byt the "wildcard" case _ => contacts).

Another requirement for reducers in redux is that if the store is undefined the reducer should return the initial value for the store. I couldn't figure out a nice way to model this so I have to cheat a bit and define the initial state of the entire store explicitly later.

The inbox_reducer works similarly but updates Inbox sets instead of Contacts maps:

let inbox_reducer inbox action =>
  switch action {
  | InboxAction ia => {
    switch ia {
    | AddToInbox email => Inbox.add email inbox
    | RemoveFromInbox email => Inbox.remove email inbox
    }
  | _ => inbox
  };

Previously, in contacts_reducer we used only one switch statement to handle all the actions. For inbox_reducer we're instead using nested switch statements to handle InboxActions separately. There is an advantage to this that's perhaps not immediately apparent. If we create a new kind of InboxAction later, and forget to handle it in the inbox_reducer the compiler will actually be able to warn us about this potential mistake.

Finally, the sent_reducer simply adds emails to the front of a linked list

let sent_reducer sent action =>
  switch action {
  | EmailSentAction email => [email, ...sent]
  | _ => sent
  };

The store itself is modelled as nested records

type email_store = {
  inbox: Inbox.t,
  sent: list email
};

type store = {
  contacts: Contacts.t,
  emails: email_store
};

Higher order reducers are combined manually until we finally define the root_reducer:

let email_reducer email_store action => {
  inbox: inbox_reducer email_store.inbox action,
  sent: sent_reducer email_store.sent action
};

let root_reducer store action => {
  contacts: contact_reducer store.contacts action,
  emails: email_reducer store.emails action
};

To test this out we explicitly define an initial store and a list of actions

let initial_store = {
  contacts: Contacts.empty,
  emails: {
    sent: [],
    inbox: Inbox.empty
  }
};

let actions = [
  EmailSentAction {
    from_addr: "flint@example.com",
    to_addr: "strax@example.com",
    title: "Hi!",
    body: "..."
  },
  ContactAction (
    AddContact {id: 101L, email: "vastra@example.com"}
  ),
  InboxAction (
    AddToInbox {
      from_addr: "strax@example.com",
      to_addr: "flint@example.com",
      title: "Re: Hi",
      body: "..."
    }
  ),
  ContactAction (
    AddContact {id: 102L, email: "flint@example.com"}
  ),
  ContactAction (RemoveContact 101L),
  ContactAction (
    AddContact {id: 103L, email: "strax@example.com"}
  )
];

We can load all these top level expressions into the rtop repl using #use "Reducers.re" or simply by copy pasting each form one by one into the repl. Finally, we can apply the list of actions to the initial store using List.fold_left (Reason# is the repl prompt):

Reason# let result = List.fold_left root_reducer initial_store actions;
let result : store =
  {contacts : <abstr>,
   emails :
    {inbox : <abstr>,
     sent :
      [{from_addr : "flint@example.com", to_addr : "strax@example.com",
        title : "Hi!", body : "..."}]}}

Unfortunately maps and sets are printed as <abstr> by the rtop repl but I'm just going to trust the "if it compiles, it works" mantra and assume it's the correct result. It is possible to tell the repl how to print custom data types, but this can be a topic for a future blog post.

For now we can also manually check contacts with

Reason# Contacts.bindings result.contacts;
- : list (int64, contact) =
  [(102L, {id : 102L, email : "flint@example.com"}),
   (103L, {id : 103L, email : "strax@example.com"})]

and the inbox using

Reason# Inbox.elements result.emails.inbox;
- : list email =
  [{from_addr : "strax@example.com", to_addr : "flint@example.com",
    title : "Reply: Hi", body : "..."}]

Conclusions

You might have noticed that I never actually defined the combineReducers function. This is simply because I couldn't figure out how to do it. Instead I manually built the root_reducer out of other reducers. I'm sure it's possible to build a generic function like combineReducers in Reason and perhaps with more experience I'll be able to do it.

On the other hand there isn't much of a practical difference between

let email_reducer email_store action => {
  inbox: inbox_reducer email_store.inbox action,
  sent: sent_reducer email_store.sent action
};

let root_reducer store action => {
  contacts: contact_reducer store.contacts action,
  emails: email_reducer store.emails action
};

and

const rootReducer = combineReducers({
  contacts: contactsReducer,
  emails: combineReducers({
    inbox: inboxReducer,
    sent: sentReducer
  })
});

What we achieved instead might be just as valuable since everything is type checked at compile time, including the actions and the store itself.

Finally I would like to thank @pycurious, @_chenglou, @jordwalke, @jeffmo and @reynir (on irc, #reasonml) for very valuable feedback on earlier drafts of this post.

Tags: Reason