Melange for X developers
If you are familiar with other languages/platforms, here you will find sections that compare Melange to a few of them, so it might help you get up and running quickly. In particular:
- JavaScript
- TypeScript
- Js_of_ocaml
- ReScript
For JavaScript developers
Melange is a thin layer over OCaml, a strongly typed functional programming language with an emphasis on expressiveness and safety. Melange’s goal is to help web developers build and maintain JavaScript applications safely, thanks to OCaml expressive and powerful type system.
Melange allows to build applications using either OCaml syntax or Reason syntax. If you don’t know which one to choose, we recommend Reason, as it has been designed with JavaScript developers in mind.
Reason syntax has first-class support for JSX, and there are bindings like ReasonReact that build on top of that functionality to provide a great developer experience.
Another advantage is that programs written using Reason syntax are fully compatible with those written in OCaml syntax.
Here is the cheat sheet with some equivalents between JavaScript and Reason syntaxes:
Variable
JavaScript | OCaml (Reason syntax) |
---|---|
const x = 5; | let x = 5; |
var x = y; | No equivalent |
let x = 5; x = x + 1; | let x = ref(5); x := x^ + 1; |
String & Character
JavaScript | OCaml (Reason syntax) |
---|---|
"Hello world!" | Same |
'Hello world!' | Strings must use " |
Characters are strings | 'a' |
"hello " + "world" | "hello " ++ "world" |
Boolean
JavaScript | OCaml (Reason syntax) |
---|---|
true , false | Same |
!true | Same |
` | |
a === b , a !== b | Same |
No deep equality (recursive compare) | a == b , a != b |
a == b | No equality with implicit casting |
Number
JavaScript | OCaml (Reason syntax) |
---|---|
3 | Same * |
3.1415 | Same |
3 + 4 | Same |
3.0 + 4.5 | 3.0 +. 4.5 |
5 % 3 | 5 mod 3 |
* JavaScript has no distinction between integer and float.
Object/Record
JavaScript | OCaml (Reason syntax) |
---|---|
no static types | type point = {x: int, mutable y: int} |
{x: 30, y: 20} | Same |
point.x | Same |
point.y = 30; | Same |
{...point, x: 30} | Same |
Array
JavaScript | OCaml (Reason syntax) |
---|---|
[1, 2, 3] | [|1, 2, 3|] |
myArray[1] = 10 | Same |
[1, "Bob", true] * | (1, "Bob", true) |
No immutable list | [1, 2, 3] |
* Tuples can be simulated in JavaScript with arrays, as JavaScript arrays can contain multiple types of elements.
Null
JavaScript | OCaml (Reason syntax) |
---|---|
null , undefined | None * |
* There are no nulls, nor null bugs in OCaml. But it does have an option type for when you actually need nullability.
Function
JavaScript | OCaml (Reason syntax) |
---|---|
arg => retVal | (arg) => retVal |
function named(arg) {...} | let named = (arg) => ... |
const f = function(arg) {...} | let f = (arg) => ... |
add(4, add(5, 6)) | Same |
Blocks
JavaScript | OCaml (Reason syntax) |
---|---|
|
|
Currying
JavaScript | OCaml (Reason syntax) |
---|---|
let add = a => b => a + b | let add = (a, b) => a + b |
Both JavaScript and OCaml support currying, but OCaml currying is built-in and optimized to avoid intermediate function allocation and calls, whenever possible.
If-else
JavaScript | OCaml (Reason syntax) |
---|---|
if (a) {b} else {c} | Same |
a ? b : c | Same |
switch | switch but with pattern matching |
Destructuring
JavaScript | OCaml (Reason syntax) |
---|---|
const {a, b} = data | let {a, b} = data |
const [a, b] = data | let [|a, b|] = data * |
const {a: aa, b: bb} = data | let {a: aa, b: bb} = data |
* This will cause the compiler to warn that not all cases are handled, because data
could be of length other than 2. Better switch to pattern-matching instead.
Loop
JavaScript | OCaml (Reason syntax) |
---|---|
for (let i = 0; i <= 10; i++) {...} | for (i in 0 to 10) {...} |
for (let i = 10; i >= 0; i--) {...} | for (i in 10 downto 0) {...} |
while (true) {...} | Same |
JSX
JavaScript | OCaml (Reason syntax) |
---|---|
<Foo bar=1 baz="hi" onClick={bla} /> | Same |
<Foo bar=bar /> | <Foo bar /> * |
<input checked /> | <input checked=true /> |
No children spread | <Foo>...children</Foo> |
* Note the argument punning when creating elements.
Exception
JavaScript | OCaml (Reason syntax) |
---|---|
throw new SomeError(...) | raise(SomeError(...)) |
try {a} catch (Err) {...} finally {...} | try (a) { | Err => ...} * |
* No finally.
Blocks
In OCaml, "sequence expressions" are created with {}
and evaluate to their last statement. In JavaScript, this can be simulated via an immediately-invoked function expression (since function bodies have their own local scope).
JavaScript | OCaml (Reason syntax) |
---|---|
|
|
Comments
JavaScript | OCaml (Reason syntax) |
---|---|
/* Comment */ | Same |
// Line comment | Same |
For TypeScript developers
The approach to typing applications using Melange differs somewhat from TypeScript. TypeScript has been designed with a focus on compatibility with JavaScript, as outlined in its design goals. On the other hand, Melange is built upon OCaml, a compiler known for its emphasis on expressiveness and safety.
These are some of the differences between both.
Type inference
In TypeScript, the types for the input parameters have to be defined:
let sum = (a: number, b: number) => a + b;
OCaml can infer types without barely any type annotations. For example, we can define a function that adds two numbers as:
let add x y = x + y
let add = (x, y) => x + y;
Algebraic data types
It is not possible to build an ADT in TypeScript the same way as in OCaml. Discriminated unions would be the closest analog to them, with libraries like ts-pattern as an alternative to the lack of support for pattern matching in the language.
In OCaml, algebraic data types (ADTs) are a commonly used functionality of the language. They allow you to build your own types from small blocks. And with pattern matching, it is easy to access this data.
Nominal typing
In TypeScript, all typing is structural. This means that it is hard sometimes to establish a boundary or separation between two types that have the same implementation. For these cases, nominal typing can be emulated using tags:
type Email = string & { readonly __tag: unique symbol };
type City = string & { readonly __tag: unique symbol };
In OCaml, nominal typing is fully supported. Some of the core language types like records and variants are nominal. This means that even if you declare exactly the same type twice, functions that operate on values from one type will not be compatible with the other type.
There is also structural typing, used for OCaml objects and polymorphic variants.
Immutability
TypeScript has two base primitives to work with immutability: const
and readonly
.
The first one is used to prevent variable reference change.
const a = 1;
a = 2; // Error: Cannot assign to 'a' because it is a constant.
And the second one is used to make properties immutable.
type A = {
readonly x: number;
}
const a: A = { x: 1 };
a.x = 12; // Error: Cannot assign to 'x' because it is a read-only property.
Nevertheless, there are some problems here. const
and readonly
only block reference changes but do nothing about values. With const a = [1, 2, 3]
or readonly x: number[]
, you can still change the contents of an array.
OCaml provides data types with immutability in mind, like lists, records, or maps.
Strictness and soundness
In TypeScript, you have the flexibility to use types like any
or other expansive types such as Function
. However, TypeScript provides the strict
option in the tsconfig.json
file to mitigate the usage of these less type-safe constructs. On the other hand, OCaml does not offer a similar option to enable or disable strictness. In OCaml, the language itself promotes type safety without the need for explicit configuration options to enforce stricter behavior.
TypeScript, as mentioned in its handbook, may sacrifice soundness for practicality when needed. In contrast, OCaml implementations provide unsound methods like the identity
primitive but they are generally discouraged and rarely used. The OCaml community places a strong emphasis on maintaining soundness and prefers safer alternatives to ensure code correctness.
Cheatsheet
The following are some conversions between TypeScript and OCaml idioms, in the OCaml side we use Reason syntax for familiarity, as mentioned in section for JavaScript developers.
Type aliases
TypeScript | OCaml (Reason syntax) |
---|---|
type Email = string; | type email = string; |
Abstract types
TypeScript | OCaml (Reason syntax) |
---|---|
|
|
Union types / Variants
TypeScript | OCaml (Reason syntax) |
---|---|
|
|
|
|
Immutability
TypeScript | OCaml (Reason syntax) |
---|---|
| Enabled by default |
Currying
TypeScript | OCaml (Reason syntax) |
---|---|
| Enabled by default |
Parametric polymorphism
TypeScript | OCaml (Reason syntax) |
---|---|
type length = <T>(_: T[]) => number; | let length: list('a) => int; |
For Js_of_ocaml developers
There are many similarities between Js_of_ocaml and Melange:
- Both compile OCaml to JavaScript.
- Both are available as libraries in the official opam repository.
- Both have access to the OCaml platform developer toolchain: the OCaml LSP server, Merlin, and the different editor extensions.
- Both have implemented extensive integration with Dune.
However, while Js_of_ocaml transforms OCaml bytecode into JavaScript, Melange starts the conversion process earlier in the compiler pipeline, as it transforms the compiler lambda representation into JavaScript.
Js_of_ocaml is a project with years of development and evolution behind it, while Melange appearance is relatively recent in comparison.
These aspects translate into different trade-offs. Compared to Js_of_ocaml:
- Melange can be installed in an OCaml 5 opam switch, but the editor integration is not working at the time (May 2023).
- Similarly, any OCaml 5 features like effects are currently unsupported in Melange.
- Js_of_ocaml allows to compile the compiler itself and create "toplevels", which is not possible with Melange.
Marshal
is well supported in Js_of_ocaml, while Melange does not support it.- Libraries like
Unix
orStr
are available in Js_of_ocaml but not in Melange. - Js_of_ocaml supports sourcemaps, which Melange do not support yet (as of May 2023).
On the upside, in Melange:
- Consuming existing JavaScript packages might be a bit easier in Melange, thanks to its compilation model and the extensive availability of mechanisms to bind to JavaScript code.
- There is great support for some of the most used JavaScript libraries like ReactJS or GraphQL clients.
- The generated JavaScript bundles are generally smaller.
- The generated JavaScript code is generally more readable.
- Melange can generate ES6 or commonjs while Js_of_ocaml generates an IIFE (Immediately Invoked Function Expression) (as of Sep 2023)
- Straight-forward integration with modern JavaScript tooling like Webpack, NextJS, etc. This is possible thanks to the 1 module <-> 1 JavaScript file compilation model.
For ReScript developers
As a project that shares a common ancestry with ReScript, Melange inherits a lot of its characteristics:
- The compilation model involves compiling a single module into a single JavaScript file.
- The libraries provided by ReScript (Belt and Js) are available in Melange too.
- The mechanisms provided for communicating with JavaScript code are mostly the same.
However, one of Melange’s goals is to maximize compatibility with the OCaml ecosystem. This goal translates into fundamental differences in how Melange and ReScript function from the perspective of both library authors and users.
Package manager
ReScript projects rely exclusively on npm for all packages they depend on. Melange projects, on the other hand, will use opam for native packages, and npm for JavaScript ones. Melange package management is explained in detail in the dedicated section.
Build system
ReScript has its own build system, originally based on Ninja.
Melange defers to Dune for build orchestration, as it is explained in detail in the corresponding section. By integrating with Dune, Melange can benefit from the multiple features provided. One of the most useful features is first-class supports for monorepos. But there are multiple others, like virtual libraries, watch mode, or integrations with tools like odoc.
The divergences caused by the different build systems have a lot of implications and nuances that might be too complex to explain in this section, but some of the specific details have been discussed in the OCaml forum.
Source-based vs pre-built distribution
While with ReScript every dependency can be downloaded with just npm, Melange projects will have to use opam and npm. This is a trade-off: on one hand, some Melange projects might need to include two package configuration files. But on the other hand they can benefit from opam’s source-based package distribution model for things like PPXs, linters, or any other OCaml tooling.
In comparison, consuming any OCaml tool in ReScript is more challenging. Since ReScript lacks a native toolchain, authors of the tools need to provide pre-built binaries for all the supported systems and architectures. This poses difficulties for the authors in terms of maintenance, and it can also result in certain users being unable to access these tools if their systems or architectures are not included in the pre-built binaries.
OCaml compiler version
ReScript is compatible with the 4.06 version of the OCaml compiler, while Melange is compatible with the version 5.2.0 (as of Sep 2024).
Editor integration
Melange is fully compatible with the OCaml platform editor tools, which makes possible to work in projects that include OCaml and Melange code using the same editor configuration.
ReScript has its own set of editor plugins.
Feature choice and alignment with OCaml
ReScript’s goal is to model the language to bring it as close to JavaScript as possible. From the website introduction section:
ReScript looks like JS, acts like JS, and compiles to the highest quality of clean, readable and performant JS (...)
New features added to ReScript might close its alignment with JavaScript, but some of these features can lead to greater divergence from OCaml. As Melange prioritizes compatibility with OCaml, it avoids incorporating those features that widen the gap between the two.
Here is a non-exhaustive list of the features that ReScript has added and will not be supported in Melange:
- The
async
/await
syntax: similar functionality can be achieved in Melange through the usage of binding operators (introduced in OCaml 4.13). - Optional fields in records, like
type t = { x : int, @optional y : int }
. - Uncurried by default.
The restriction above only applies to features that compromise compatibility with OCaml, but otherwise Melange can incorporate bugfixes or new functionality from ReScript.
On the other hand, as Melange goal is to keep up with the version of the OCaml compiler, there are features inherited from OCaml that are not supported by ReScript at the moment (May 2023), for example:
- Binding operators /
let
bindings - Better type errors for some specific cases
- Additions to the stdlib
The whole list of changes added to the OCaml compiler can be checked here.
Syntax
ReScript encourages using the new syntax for any new code. While OCaml syntax might be supported today, its usage is not documented. Reason syntax is no longer supported.
Melange supports and documents both Reason and OCaml syntaxes. It also includes a best-effort support for ReScript syntax for backwards compatibility, provided through the rescript-syntax
package, available in opam. To build any code written using ReScript syntax, the only requirement is to download this package, as Melange and Dune will already coordinate to make use of it when res
or resi
files are found.