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 were better at Python and JavaScript because they had seen more of them. Today a fast compiler or a fast linter with clear errors does more for a model than a familiar syntax: it writes, reads the error, and fixes it in seconds. Reason and mlx share the compiler and the errors, so nothing changes here.
Here's the list
Anyway, here's the list of things we lose:
JavaScript objects
Reason lets us write JavaScript objects with quoted keys. The keys can contain punctuation or words that OCaml reserves.
let headers = {
"user-agent": userAgent,
"Content-Type": contentType,
"type": requestType,
};In the converted code, the object uses [%mel.obj]. Each renamed field
needs [@mel.as] to keep its JavaScript key. Since record fields are idents, and can't express all the JavaScript object fields.
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 form takes one argument and returns a function for the next
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.
let add = (. left, right) => left + right;
let total = add(. 1, 2);In mlx with Melange, the same operations use attributes
let add = (fun left right -> left + right) [@u]
let total = (add 1 2 [@u])We lose the dedicated notation: the dot beside the arguments becomes an attribute after 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]This does not apply to every attribute or ppx extension. Forms such as
let[@react.component], let%browser_only, and match%platform can still
put the annotation near the start.
Precedence
In Reason, the parentheses around a function's arguments make the call boundary explicit
let value = React.Event.Form.target(event)##value;OCaml uses spaces for function application. To read a property from the result, we put parentheses around the call
let value = (React.Event.Form.target event)##valueWithout those parentheses
React.Event.Form.target event##valuethe expression groups as
React.Event.Form.target (event##value)This is a different operation: it reads value from the event first, then passes it to target.
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 have descriptive names. For example, promise operators
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 form in mlx.
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 doesn't have an equivalent. It passes an existing value as the children, without wrapping it in a new list
<Layout> ...children </Layout>In mlx, children always become a list. The closest form is to pass children as a prop
<Layout children />The list of Reason features is long, but there are always tradeoffs, what are the good bits of mlx?
mlx is awesome, actually
It's an extension of OCaml that generates OCaml code, like .mly (Menhir) or .mll (ocamllex). Its JSX syntax 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).
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 we should work on, a new beginning, or at least a fresh start. What do you think?