What do we lose moving from Reason to mlx?

What Reason gives us beyond “just JSX”

SEP 2026

•

7 MINUTES

•

DAVESNX

Reason is technically a different syntax for writing OCaml programs. You can mix and match Reason and OCaml however you want in any project.

We often say that “Reason sits on top of OCaml”, so all the tooling just works: the compiler, build system, package manager, LSP, and documentation are exactly the same.

Besides this, I see Reason as a bit more than “just a different syntax” and more like an umbrella of syntax + style + accessibility to functional programming. One of the goals was to move OCaml much closer to the web.

mlx, on the other hand, is a very minimal extension to the OCaml syntax that adds just JSX, the possibility to express HTML-like elements as a first-class citizen in the language.

A component looks like this

let[@react.component] make ~name ~user =
  <section className="profile">
    <h2>(React.string name)</h2>
    <p>
      (React.string
         (match user with
         | Online -> "Online"
         | Offline -> "Offline"))
    </p>
  </section>

Does syntax matter?

People say that syntax doesn't matter, and they are wrong.

I consider myself a creative person. I appreciate beauty and great user experiences, and I often solve complex problems by drawing or creating clearer visualizations.

Syntax is the most important touch point of the interface of the language, followed by error messages. And as such, it needs to be treated as crucial.

There's some truth in the opposite view, where the meaning is the substance and at the end of the day, it will remain the only factor. Mega true, but in cases where the syntax stops you from getting the meaning... it stops being a detail.

Does syntax matter for LLMs?

Not much. Models used to be better at Python and JavaScript because they saw more of them in the public datasets.

Researchers consider OCaml a low-resource language, but agents have improved. Popularity isn't the only factor anymore: agents prefer fast compilers and clear error messages, since the flow is always 1) write code, 2) compile it, 3) read the error and go back to 1. José Valim makes this case for Elixir, which had the highest completion rate of 20 languages in Tencent's AutoCodeBench. He credits immutability, stable docs, and tooling built for that loop.

Bend 2 is the extreme case. It came out last month, so models barely saw it during training, and yet agents ported Rust projects to it on its second day. The language was built for agents from the start. You describe what the code must do, the AI writes it, and the compiler proves it right or tells the agent why not, in a tenth of a second. When the compiler can tell the agent exactly what's wrong, being new stops being a problem.

Popularity still counts, though. Agents spend more tokens in OCaml than in Python, partly because they write code that doesn't compile and then fix it. But that gap comes from training data and tooling, not from syntax.

Reason and mlx share the same compiler, the same type errors and the same tooling, so picking one over the other changes nothing here.

Here's the list

Anyway, here's the list of things we lose:

JavaScript objects

In Reason, JavaScript objects have an special syntax with record-like plus quoted keys.

let headers = {
  "user-agent": userAgent,
  "Content-Type": contentType,
  "type": requestType,
};

In mlx there's nothing like this. We need to write [%mel.obj] with a record inside, with a small caveat: record fields are OCaml identifiers, so any key that isn't a valid one needs [@mel.as] to get its JavaScript name back.

let headers =
  [%mel.obj
    {
      user_agent = userAgent [@mel.as "user-agent"];
      contentType = contentType [@mel.as "Content-Type"];
      type_ = requestType [@mel.as "type"];
    }]

Uncurried functions and calls

The curried function definition takes one argument and returns a function

let add = left => right => left + right;
let total = add(1, 2);

In Reason, a dot inside the argument list marks an uncurried function or call. Meaning that the function has two arguments. This is used in Melange to call JavaScript functions or to optimize the bundle.

let add = (. left, right) => left + right;
let total = add(. 1, 2);

In mlx, the same operations use attributes

let add = (fun left right -> left + right) [@u]
let total = (add 1 2 [@u])

It works, but the [@u] sits at the end of the expression.

Attribute placement

Reason puts a deriving attribute before its type declaration

[@deriving (enumerate, toString)]
type hAlign =
  | Left
  | Center
  | Right;

In mlx, the attribute follows the declaration

type hAlign =
  | Left
  | Center
  | Right
[@@deriving enumerate, toString]

The same happens with Melange bindings

[@mel.get]
external tagName: Dom.element => string = "tagName";
external tagName : Dom.element -> string = "tagName"
[@@mel.get]

Not every attribute needs to sit at the end, though. Some of them can remain at the start, such as: let[@react.component], let%browser_only and match%platform.

Precedence

In Reason, the parentheses around the arguments tell you where the call ends

let value = React.Event.Form.target(event)##value;

OCaml uses spaces for function application, so we need to wrap the call ourselves

let value = (React.Event.Form.target event)##value

Without those parentheses

React.Event.Form.target event##value

the expression groups as

React.Event.Form.target (event##value)

which reads value from the event and passes it to target. Easy to get wrong when you come from Reason.

Data-first pipe

In Reason we have ->, called data-first pipe, it applies the value to the function, as a first argument on the function

value->f(arg);
/* Equivalent to f(value, arg). */

With Melange, the data-first pipe is written |.

value |. f arg

|> remains unchanged.

Descriptive binding operators

Reason lets binding operators can have descriptive names, like let.await for promises

let view = id => {
  let.await user = fetchUser(id)
  and.await settings = fetchSettings(id);
  render(user, settings);
};

OCaml has binding operators too, but only with symbolic names such as let* and and+. There's no descriptive names.

Spread operator

Reason uses the spread operator ... in three places: lists, records, and JSX children.

let all = [first, ...rest];
let next = {...person, age: person.age + 1};

OCaml have its own notation ::. It puts an element in front of a list, and with copies a record with some fields changed. Patterns follow the same rule: [head, ...tail] becomes head :: tail.

let all = first :: rest
let next = { person with age = person.age + 1 }

JSX children spread works the same way in Reason and in mlx.

<Layout> ...children </Layout>

The list of Reason features where it tries to improve the OCaml syntax is long. But what if we didn't need to have a separate language? What are the tradeoffs? Why does mlx exist?

mlx is awesome, actually

It's an extension of OCaml that generates OCaml code, like .mly (Menhir) or .mll (ocamllex). In this case .mlx it's a super-set of OCaml, all OCaml files are mlx files. It could could even be proposed upstream.

Less to maintain

mlx wins in maintenance. It is OCaml's parser plus the JSX rules, and ocamlformat-mlx is ocamlformat plus a small patch. For Reason, each OCaml release means adapting Reason to the changes.

Clearer story

Melange, Reason, ReScript, BuckleScript, js_of_ocaml: people who are not familiar with these projects struggle to understand the differences, and I don't blame them. The ecosystem is modular, which may be great for experts, but alien for newcomers. With mlx, you just write OCaml (with a bit of JSX), it resolves the syntax layer.

Community effort

This fits the wider work on dune and Melange (with single context), and my work on transforming the ecosystem into universal: libraries to be easy to compile to both JavaScript and native executables. Today we rely on copy_files and custom directories for that. The single-context work in Dune is part of making this simpler. Reason benefits from that work too, ofc, but mlx gives us a clearer way to describe the language we are using.

What about Reason, then?

Reason maintains a substantially different syntax and its own formatter while staying compatible with OCaml's Parsetree. mlx also targets that AST, but stays much closer to OCaml's grammar. Reason has accumulated technical debt, including formatting bugs. Most of those are fixable, and I think LLMs could help. We could support more features by encoding them with Reason-specific attributes in OCaml's Parsetree, but that breaks the abstraction and adds more machinery to maintain.

It has been in maintenance mode for a few years, and the momentum is long gone. The Discord has been quiet for a long time. It's sad to say that about a project I helped maintain and believed in for such a long time. But maybe mlx is the next chapter. What do you think?

Thanks for reading!
Any feedback is appreciated.

@davesnx