Stack Builders logo
Arrow icon Insights

Bring your Haskell Types to Reason

Learn how to use your Haskell types in your Reason (OCaml) frontend (almost) for free. You will also get automatic code generation of JSON serialization functions with their respective golden files, and code for your serialization tests.

The different tools and technologies that we often use when developing software add complexity to our systems. One of the challenges that we face in web development is that we are often writing the backend and the frontend in different languages and type systems. To build reliable applications we need a way to connect both.

We need to make sure our applications are reliable and do not break when the API changes. We can build a bridge that helps us to traverse safely between the backend and the frontend. We can do that with a code generation tool that allows us to use the same types on both sides of the application.

You will see a real-life case, using Haskell as a backend language, how you can get Reason (OCaml) types and JSON serialization/deserialization almost for free using Generics and how you can make sure your serialization is correct with automatic golden test generation.

The topic of this blog post was presented at Compose Conference New York in July 2019. Check out this and the rest of the amazing talks here!

Why did we choose Reason?

Reason is a new syntax and toolchain for OCaml created by Facebook. OCaml is a decades old language that has been relegated mostly to academia but has inspired a lot of new languages (Rust, F#). Sadly, it does not have the kind of community we need for web development where things move at high speed.

Facebook built Reason to solve real-life problems in production apps - Facebook Messenger is over 50% migrated to Reason, and they reduced bugs and compilation times by doing so.

When we talk about toolchain, we're talking about BuckleScript. BuckleScript is part of the Reason ecosystem. It is a compiler that takes Reason code and produces JavaScript code. The code BuckleScript produces is ready to use, or, if you want, you can use your favorite bundler to deploy your app. One of the best BuckleScript features is that it produces really performant and readable code. When you open a "compiled" file you can understand most of it.

So... why did we choose Reason? Well, for several reasons (no pun intended):

  • Reason is an awesome way (if not the best way) to write React Apps: React was originally developed in StandardML (a cousin of OCaml), and React features like immutability and prop types feel a little foreign in JavaScript, but they are right at home in Reason.
  • It has a great type system, which makes developing and refactoring a breeze. OCaml implements the Hindley–Milner type system, which allows parametric polymorphism. The type inference engine is so sophisticated that there is almost no need to supply type annotations, thus making our code extra clean.
  • It's functional: Functional programming makes our code cleaner, more composable and easy to maintain. This, in combination with the type system, gives us compilation error messages that helps us to know not only where the error is but also what type of value we need.
  • JS interop: Unlike Elm - a pure and functional language based in Haskell - JS interop is very easy in Reason. The downside is that your code can have uncontrolled side effects and - possibly - runtime errors.
  • Reason leverages both the OCaml community and JS community and that gives us a unique ecosystem to work with.

There are other alternatives to work with types like TypeScript or Flow. While they are much better than using plain JS, they don’t provide the same guarantees. TypeScript and Flow add types to JavaScript too, but they do so by extending the language with all the problems and pitfalls JS already has. Reason, on the other hand, starts from the rock-solid foundation that is OCaml and then builds up its ease of use and community development that JS provide.

How did we implement it?

The types of our backend application were defined in Haskell and we wanted to use the same types in our frontend application. Since Haskell and OCaml types are similar in a semantic way, we decided to use a tool to transform them. ocaml-export is a tool that takes Haskell types and - using Generics - can analyze the type and make it an OCaml type so we can use it in our Reason frontend.

This is how ocaml-export works. It takes Haskell types and generates OCaml types ready to use in the frontend.

In Haskell (or in functional programming in general) we can define two kinds of types: Product Types and Sum Types.

Product types are what most people are familiar with. They correspond with records or classes in other languages. They describe a type that is composed of two or more other types.

Sum types are not very common in procedural languages. They are somewhat like union types in C. They describe a type that belongs to a set of possible values.

Here is an example of a product type in Haskell. It describes a User that contains both a String and an Int:

data User = User
  { name :: String
  , age  :: Int
  }

A sum type in Haskell looks like this:

data Status
  = Inactive String
  | Active

The type Status is either Active or Inactive. Notice that the Inactive constructor has a String attached to it.

OCaml has the same kinds of basic types as Haskell. In fact, you can do a one-to-one mapping of the types without losing information. Here is how ocaml-export automatically generates the above examples:

type user =
  { name : string
  ; age : int
  }

type status =
  | Inactive of string
  | Active

JSON encoding

This tool also generates JSON serialization and deserialization functions in OCaml. It does this by assuming we're using the bs-aeson library. Our previous examples would look like this:

let encodeUser x =
  Aeson.Ecode.Object_
    [ ("name", Aeson.Encode.string x.name)
    ; ("age", Aeson.Encode.int x.age)
    ]

This function will take a user object and serialize it into a JSON string of the form:

{
  "name": "Kurt Cobain",
  "age": 27
}

With the sum type the encoding function is a little trickier but works in the same fashion. We make the JSON with the type constructor description in the tag field and the contents (if any) in the contents field. These are the Aeson Haskell library defaults. The encodeStatus function will be generated as follows:

let encodeStatus x =
  match x with
  | Inactive y0 ->
      Aeson.Encode.object_
        [ ("tag", Aeson.Encode.string "Inactive")
        ; ("contents", Aeson.Encode.string y0)
        ]
  | Active ->
       Aeson.Encode.object_
        [ ("tag", Aeson.Encode.string "Active")
        ]

This will produce a JSON object of the form:

{
  "tag": "Inactive",
  "contents": "description"
}

Or:

{
  "tag": "Active"
}

Notice how all these functions are type safe. The variable y0 cannot be anything other than a string, yet - thanks to OCaml's powerful type inference engine - our code does not look polluted with type annotations. The engine knows that x is of type status because it has to match with its constructors and it knows y0 is a string because it has to match with the Inactive constructor. Also, OCaml forces us to deal with all the possible matches of the status; if we somehow forget to deal with the Active constructor, our code won't compile.

JSON decoding

The same way we get JSON encoding for free with ocaml-export, we also get decoding. That is when we get a JSON response from the backend, we have a function that allows us to transform that JSON object into OCaml in a type-safe way.

This is what the decodeUser function looks like:

let decodeUser json =
  match Aeson.Decode.
    { name = field "name" string json
    ; age = field "age" int json
    }
  with
  | v -> Belt.Result.Ok v
  | exception Aeson.Decode.DecodeError message -> Belt.Result.Error ("decodeUser: " ^ message)

Notice how this function doesn't fail. It will always return a Result. In this case it will return either an Ok wrapping the decoded value or an Error wrapping the error message. This is similar to how Either works in Haskell, and it forces us to handle the error state in a type-safe way.

The sum type decoding works in a similar fashion:

let decodeStatus json =
  match AesonDecode.(field "tag" string json) with
  | "Inactive" -> (
    match Aeson.Decode.(field "contents" string json) with
    | v -> Belt.Result.Ok (Inactive v)
    | exception Aeson.Decode.DecodeError message -> Belt.Result.Error ("Inactive: " ^ message)
    )
  | "Active" -> Belt.Result.Ok Active
  | tag -> Belt.Result.Error ("Unknown tag value found '" ^ tag ^ "'")
  | exception Aeson.Decode.DecodeError message -> Belt.Result.Error message

Again, all the OCaml code that is presented here is autogenerated by ocaml-export and it is production-ready code that can be used in your Reason app. :tada:

Testing

Although the type system helps us to make sure our program is correct, it can't replace tests. We need to make sure that the serialization/deserialization that happens in our frontend is the same as the one in our backend. And tests are the only way to achieve that.

Luckily, ocaml-export also generates JSON golden files for all the types we feed into it. These golden files are generated using Aeson and Arbitrary from the Haskell backend. These JSON objects are examples of how our backend serializes requests.

Now we have to make tests to prove that our OCaml code can read those files and then serialize the resulting object into the exact same JSON structure we started with. Of course, ocaml-export will generate the test suite too. Here is how it looks:

let () =
  AesonSpec.goldenDirSpec
    OnpingTypes.decodeUser
    OnpingTypes.encodeUser
    "user test suite"
    "golden/User";

We use bs-aeson-spec to automate our tests. The goldenDirSpec function takes a serialization and a deserialization function and runs them against the golden files, assuring that the serialization in our frontend works in the same way as in our backend.

What does it look like?

You can take a look at an example here. You’ll find a complete Haskell backend and Reason frontend, along with the bridge between them. When you compile the frontend, all the types from the backend will cross that bridge into the frontend and they will be tested and used by the frontend. Take a look and tell us what you think!

What's next?

There are a couple of issues still in development. The project is open source and welcomes contributions.

  • Support for recursive types

At the moment, you cannot generate types that reference themselves (think linked lists or tree-like structures). It is possible to build such types in OCaml, but we need a way to detect a cyclic structure in Generic Haskell to translate that to the migrated type.

  • Resolve type dependency order

The way OCaml works, a type has to be declared before you can use it. That is not the case for Haskell where the declaration order of dependencies does not matter. We would have to create a dependency graph of our types and then migrate those types so that there aren’t any dependency problems (and also to avoid cyclic dependency).

  • Automatically create bs-fetch calls using servant route types

Besides generating the types, the golden files and the tests, it would be a nice feature to generate fetch functions (using bs-fetch) for the servant route types. That way you could create a complete frontend infrastructure automatically.

Final words

Static typing is a very important aspect of software development. It helps you catch type mismatches sooner - at compile-time rather than run-time - and it can help reduce the likelihood of some kinds of errors. This technique does not only apply to this use case. You can use it when your boundaries have a different type system and you want to take advantage of an interface defined with strong types.

By bridging Haskell and OCaml this way, we are bringing type safety to the interaction between the backend and the frontend, and we are now reducing errors and mismatches at the integration level. This allows us to refactor with greater confidence since a large class of errors introduced during refactoring will end up as type errors.

Published on: Mar. 10, 2020
Last updated: Dec. 20, 2024

Written by:

Software Developer
Diego Balseiro

Subscribe to our blog

Join our community and get the latest articles, tips, and insights delivered straight to your inbox. Don’t miss it – subscribe now and be part of the conversation!
We care about your data. Check out our Privacy Policy.