What's possible with Melange

Explain what's melange, its integration with dune and a bunch of benefits of using Melange to compile Reason and OCaml to JavaScript

JAN 2024

9 MINUTES

DAVESNX

Melange is a backend for the OCaml compiler that emits readable, modular, and highly performant JavaScript code. It integrates with all OCaml-related tooling, such as opam for installing packages and dune for building projects.

This post gives an overview of what Melange and dune make possible that the previous toolchain, ReScript v9, did not.

It isn't a direct comparison between the Melange v2 and ReScript v9 compilers. ReScript v9, also known as BuckleScript, is not the latest version, and its goals did not include solving the problems Melange solves.

Melange started as a fork of BuckleScript focused on OCaml compatibility, while ReScript continued to integrate more closely with the JavaScript ecosystem.

Works with the latest versions of OCaml

One blocker for some OCaml developers adopting ReScript is its use of an old version of the OCaml typechecker, 4.06. At the time of writing, that is several versions behind the latest OCaml release, 5.1.

This was also an issue in our backend because some packages needed to support both 4.06 and 5.1. A lot has changed between those versions:

Side note: A few features from those years of OCaml development don't apply to Melange. OCaml 5 introduced the long-awaited multicore features, for example. Melange does not currently support multicore because it cannot benefit extensively from it, given JavaScript's single-threaded nature.

Editor integration

Using the latest versions of the OCaml compiler also lets us use the OCaml LSP Server from the OCaml Platform. It's mature, well maintained, and has better support for editor features such as autocompletion, refactoring, type lenses, documentation lookup, debugging transformed ppx output, and an AST explorer.

Screenshot of VSCode with OCaml LSP

One of my favourite features is type lenses.

The previous LSP story in BuckleScript/Reason was a tragedy. Editor integration was limited and buggy. When ReScript started its own editor integration, it lacked the basics. Later versions of ReScript have improved this massively, but it is still very far from the OCaml LSP.

Using the same editor for both Melange and OCaml code is a quality-of-life improvement. There is no need to switch editors when working with both targets.

Dune is a great build tool

Dune is the de facto build tool for OCaml development these days. Generally speaking, it wraps OCaml tooling such as the Batch compiler (ocamlc) and Native compiler (ocamlopt), and it also manages libraries. It abstracts those low-level tools so users can define their libraries, executables, test suites, documentation, and the other pieces needed to develop a full application at a higher level.

  • Dune rebuilds only the libraries that change.
  • This means it compiles your opam dependencies only once from the same switch.
  • Dune won't rebuild dependent code when an interface hasn't changed.
  • Dune also builds in parallel as much as possible.
  • It integrates with the rest of the OCaml ecosystem, including ppxlib and menhir.

Dune has expanded beyond native OCaml programs to support other platforms, including js_of_ocaml, coq, binding C libraries, unikernels with MirageOS, and now Melange.

Dune deserves most of the credit for the features I'm attributing to Melange. The following are the ones I appreciate most.

Dune gives control to organize your libraries as you wish

The library stanza lets you split your application into smaller pieces and organize the codebase more clearly. Each library can have its own dependencies and ppx's. This is extremely useful for any application older than one month and almost a necessity for a monorepo or large application.

With ReScript, you previously had one large namespace that required every module name to be unique. All dependencies and ppx's applied to the entire codebase. ReScript has ways to define libraries and namespaces, but they are rarely used because they are buggy and broken.

As an anecdote, our Ahrefs monorepo contained many sub-applications separated by boundaries such as Keywords Explorer and Site Audit. Because they shared one global namespace, we had to prefix every file with an acronym. All files from Keyword Explorer used the Ke prefix: Header became KeHeader, Table became KeTable, and so on to avoid collisions.

$ ls frontend/packages/keywords-explorer/components/

KeAddKlistEntry.re      KeAddToKlist.re                 KeAdHistoryTable_Css.re     KeAiInput_Css.re
KeAiInput.re            KeAiSuggestionsSelect_Css.re    KeAiSuggestionsSelect.re    KeApiExample.re
KeClicksVolume.re       KeClicksVolumeChart.re          KeClustersBanner.re         KeClustersTreemap_Css.re
KeClustersTreemap.re    KeCountryVolume_Css.re          KeCountryVolume.re          KeDetailedAdHistory_Css.re
KeDetailedAdHistory.re  KeDetailedAdHistoryChart.re     KeDetailedAdsHistories.re   KeDetailedAdsHistoriesByGroups.re
KeEntry_Css.re          KeEntry.re                      KeEntryCounter_Css.re       KeEntryCounter.re
KeEntrySearchEngines.re KeEntryTextarea_Css.re          KeEntryTextarea.re          KeErrorPlaceholder.re
KeFilterInput.re        KeGlobalVolume_Css.re           KeGlobalVolume.re           KeHeader_Css.re
KeHeader.re             KeHistory_Css.re                KeHistory.re                KeHorizontalBarChart_Css.re
KeHorizontalBarChart.re KeIdeasOverview.re              KeIdeasOverviews.re         KeInnerModal_Css.re
KeInnerModal.re         KeInnerModalFooter_Css.re       KeInnerModalFooter.re       KeKeywordDifficulty_Css.re
KeKeywordDifficulty.re  KeKlists.re                     KeLegendItem_Css.re         KeLegendItem.re
KeListContent.re        KeMainPreloader_Css.re          KeMainPreloader.re          KeMissingKeywords_Css.re
KeMissingKeywords.re    KeMissingKeywordsAlert.re       KeNotAvailableChart.re      KeNotification_Css.re
KeNotification.re       KeOverviewChartTooltip.re       KeOverviewUpdateStatus.re   KeParentTopic.re
KePlainWidget.re        KePreloader_Css.re              KePreloader.re              KeRowsPerReportLimitAlert_Css.re
KeSearchBar_Css.re      KeSearchBar.re                  KeSearchVolume.re           KeSearchVolumeChart.re
KeSearchVolumeGoogle.re KeSerpFeatures.re               KeSerpOverview_Css.re       KeSerpOverview.re
KeSideMenu_Css.re       KeSideMenu.re                   KeSideMenuKlists.re         KeTablePagination_Css.re
KeTablePagination.re    KeTrafficTable.re               KeTrafficTableRow_Css.re    KeTrafficTableRow.re
KeUpdateButton.re       KeWidget_Css.re                 KeWidget.re                 KeWidgetChartCaption.re
KeWidgetContent_Css.re  KeWidgetContent.re              KeWidgetHeader_Css.re       KeWidgetHeader.re
KeWidgetPlaceholder.re  KeWidgetPlaceholder_Css.re      KeWidgetPreloader_Css.re    KeWidgetPreloader.re

The unique namespace works well for small applications, but it becomes challenging to manage in a large codebase without losing your hair. All dependencies and ppx's also applied across the entire monorepo, which made no sense and caused problems throughout it.

With Melange, each app can drop its prefix and modularise each sub-application along logical boundaries instead. Each set of dependencies and ppx's is now scoped correctly.

Enables universal code

When I say “universal code,” I mean code that can compile to both native code and JavaScript. It is useful for fullstack Reason development and a variety of use cases:

  • Type-safe data across client/server
  • Universal routing with type-safe routes, links and parameters
  • Re-use the same validation logic
  • Server rendering via server-reason-react
  • RPC (Remote Procedure Call) approach

This has been one of Reason's, and BuckleScript's, promises since the beginning, but it never materialised. I have seen some forms of it, but BuckleScript and dune never worked well together.

Dune now supports a new melange mode for libraries alongside native mode, enabling universal code.

Given a library without any system dependency (C bindings, unix module, etc), you can use it to emit JavaScript and run it as an executable.

Some parts are still in the design phase, but more has been written in the server-reason-react documentation: How to organise the universal code and Make sure your code is universal.

Example of a react component rendered with both reason-react and server-reason-react

Preprocessing via ppx becomes better and faster

ReScript doesn't recommend using ppx's and has been looking to replace them since the split. Its reasoning is valid: ppx's are a black box, are limited, slow compilation, and are hard to implement correctly.

Melange takes the opposite approach by empowering ppx's through ppxlib and dune, which integrate smoothly. ppxlib is the framework for creating high-quality preprocessors. It integrates with dune through (preprocess ...) and offers several other benefits:

  • Ppx runs faster. There is no need to serialise and deserialise the AST on each ppx call or run a binary.
  • Ppx's are smarter. Dune has methods that run ppx's in a single pass, while ppxlib supports context-free transformations. Rewrites therefore avoid unnecessary work.
  • There is no need to distribute pre-built binaries. Ppx's are dune libraries, so GitHub Actions do not need to create artifacts for each distribution.
  • This provides compatibility with more distributions, including M1, M2, any Linux distro, and even Windows.
  • We have access to all ppx's from opam, such as ppx_enumerate, ppx_log, ppx_expect, ppx_inline_test, ppx_blob, ppx_fields_conv, ppx_yojson_conv, and ppx_deriving_rpc.
Some of these ppx's still need to support Melange and aren't available out of the box.

Different configs for development and production

This may seem like a basic feature to showcase, but BuckleScript could not use different configurations for production and development. Dune can. Its profiles let you configure everything differently for each environment. Warnings, instrumentation, and libraries are the most common differences.

Previously, we had a script that generated the configuration from a parameter to provide this basic functionality.

Integration with documentation tooling (odoc)

Integration with the OCaml Platform also makes its documentation tooling available. odoc generates a documentation website from your interface files, complete with navigation, cross-linking, and other utilities.

It can eventually be published on ocaml.org. See ocaml.org/p/melange-json, for example.

Downsides

These are massive improvements that make a new way of working with Reason possible. At Ahrefs, we have been exploring many ways to push this environment forward. There are still a few downsides from a ReScript perspective.

Learning a new tool

Dune gives users more power at the cost of learning a new tool. Despite its capabilities, dune also has rough edges, and learning about them takes noticeable effort.

The initial blocker may be its very Lisp-like syntax. Hopefully, your editor supports colorized parentheses to help with it.

(library
 (name url)
 (wrapped false)
 (libraries uri js))

Jane Street, Tarides, and OCamlPro maintain dune, improve it frequently, and are open to feedback and feature requests.

After working with it for almost 10 months, I'm convinced it is a big improvement over the previous situation.

To learn more about dune specifically, I recommend → A handy guide to Dune

Different package manager: opam

The recommended package manager for Melange development is opam. opam is OCaml's package manager, comparable to npm, RubyGems, pip, and cargo, and its compiler version manager, comparable to nvm and rvm. In opam, a compiler version and its dependencies form a switch.

It is another new tool, with a few key differences from npm based on a safer model:

  • Safer. Opam aims to ensure that the project will compile correctly after a successful installation.
  • More strict. You can have only one version of a dependency in your project.
  • But slower. Opam downloads and builds all dependencies from source, so installation takes much longer than npm because it does more work.

Most advances from ReScript won’t be available

ReScript is evolving quickly toward better JavaScript interoperability and preparing to compete with TypeScript on features. The ReScript team is working on v11, which includes an impressive list of features:

  • Extensible records
  • Variants representation as strings
  • async/await keywords
  • uncurry by default

Those features cannot be implemented or supported with the same design in Melange because doing so would break compatibility with OCaml and its entire ecosystem.

Final words

I'm particularly excited about exploring universal code further, and I've come up with solutions I wish had existed when I started working with Reason. If you are passionate about this, DM me and let's work together!

The “benefits” are very clear from our experience using Melange in the Ahrefs codebase and other projects. If you are curious about Melange, take a look at the Getting started guide.

For a fast start, try the Melange opam template, read the documentation, or read the Melange book for React developers for a better step-by-step guide.

I would like to thank Antonio Nuno Monteiro, Javier Chávarri, and Rudi for their work in making all of this possible. Integrating Melange and Dune is just the start.

Thanks again to Antonio for reviewing a draft of this post.

Thanks for reading!
Any feedback is appreciated.

@davesnx