Communicate with JavaScript
Melange interoperates very well with JavaScript, and provides a wide array of features to consume foreign JavaScript code. To learn about these techniques, we will first go through the language concepts that they build upon, then we will see how types in Melange map to JavaScript runtime types. Finally, we will provide a variety of use cases with examples to show how to communicate to and from JavaScript.
Language concepts
The concepts covered in the following sections are a small subset of the OCaml language. However, they are essential for understanding how to communicate with JavaScript, and the features that Melange exposes to do so.
Attributes and extension nodes
In order to interact with JavaScript, Melange needs to extend the language to provide blocks that express these interactions.
One approach could be to introduce new syntactic constructs (keywords and such) to do so, for example:
javascript add : int -> int -> int = {|function(x,y){
return x + y
}|}
But this would break compatibility with OCaml, which is one of the main goals of Melange.
Fortunately, OCaml provides mechanisms to extend its language without breaking compatibility with the parser or the language. These mechanisms are composed by two parts:
- First, some syntax additions to define parts of the code that will be extended or replaced
- Second, compile-time OCaml native programs called PPX rewriters, that will read the syntax additions defined above and proceed to extend or replace them
The syntax additions come in two flavors, called extension nodes and attributes.
Extension nodes
Extension nodes are blocks that are supposed to be replaced by a specific type
of PPX rewriters called extenders. Extension nodes use the %
character to be
identified. Extenders will take the extension node and replace it with a valid
OCaml AST (abstract syntax tree).
An example where Melange uses extension nodes to communicate with JavaScript is to produce "raw" JavaScript inside a Melange program:
[%%mel.raw "var a = 1; var b = 2"]
let add = [%mel.raw "a + b"]
[%%mel.raw "var a = 1; var b = 2"];
let add = [%mel.raw "a + b"];
Which will generate the following JavaScript code:
var a = 1; var b = 2
var add = a + b
The difference between one and two percentage characters is detailed in the OCaml documentation.
Attributes
Attributes are "decorations" applied to specific parts of the code to provide additional information. In Melange, attributes are used in two ways to enhance the expressiveness of generating JavaScript code: either reusing existing OCaml built-in attributes or defining new ones.
Reusing OCaml attributes
The first approach is leveraging the existing OCaml’s built-in attributes to be used for JavaScript generation.
One prominent example of OCaml attributes that can be used in Melange projects
is the unboxed
attribute, which optimizes the compilation of single-field
records and variants with a single tag to their raw values. This is useful when
defining type aliases that we don’t want to mix up, or when binding to
JavaScript code that uses heterogeneous collections. An example of the latter is
discussed in the variadic function arguments
section.
For instance:
type name =
| Name of string [@@unboxed]
let student_name = Name "alice"
[@unboxed]
type name =
| Name(string);
let student_name = Name("alice");
Compiles into:
var student_name = "alice";
Other OCaml pre-built attributes like alert
or deprecated
can be used with
Melange as well.
Defining new attributes
The second approach is introducing new attributes specifically designed for
Melange, such as the mel.set
attribute used to
bind to properties of JavaScript objects. The complete list of attributes
introduced by Melange can be found
here.
Attribute annotations can use one, two or three @
characters depending on
their placement in the code and which kind of syntax tree node they are
annotating. More information about attributes can be found in the dedicated
OCaml manual page.
Here are some samples using Melange attributes
mel.set
and mel.as
:
type document
external setTitleDom : document -> string -> unit = "title" [@@mel.set]
type t = {
age : int; [@mel.as "a"]
name : string; [@mel.as "n"]
}
type document;
[@mel.set] external setTitleDom: (document, string) => unit = "title";
type t = {
[@mel.as "a"]
age: int,
[@mel.as "n"]
name: string,
};
To learn more about preprocessors, attributes and extension nodes, check the section about PPX rewriters in the OCaml docs.
External functions
Most of the system that Melange exposes to communicate with JavaScript is built
on top of an OCaml language construct called external
.
external
is a keyword for declaring a value in OCaml that will interface with
C code:
external my_c_function : int -> string = "someCFunctionName"
external my_c_function: int => string = "someCFunctionName";
It is like a let
binding, except that the body of an external is a string.
That string has a specific meaning depending on the context. For native OCaml,
it usually refers to a C function with that name. For Melange, it refers to the
functions or values that exist in the runtime JavaScript code, and will be used
from Melange.
In Melange, externals can be used to bind to global JavaScript
objects. They can also be decorated with
certain [@mel.xxx]
attributes to facilitate the creation of bindings in
specific scenarios. Each one of the available
attributes will be further explained
in the next sections.
Once declared, one can use an external
as a normal value. Melange external
functions are turned into the expected JavaScript values, inlined into their
callers during compilation, and completely erased afterwards. They don’t appear
in the JavaScript output, so there are no costs on bundling size.
Note: it is recommended to use external functions and the [@mel.xxx]
attributes in the interface files as well, as this allows some optimizations
where the resulting JavaScript values can be directly inlined at the call sites.
Special identity external
One external worth mentioning is the following one:
type foo = string
type bar = int
external danger_zone : foo -> bar = "%identity"
type foo = string;
type bar = int;
external danger_zone: foo => bar = "%identity";
This is a final escape hatch which does nothing but convert from the type foo
to bar
. In the following sections, if you ever fail to write an external
,
you can fall back to using this one. But try not to.
Abstract types
In the subsequent sections, you will come across examples of bindings where a type is defined without being assigned to a value. Here is an example:
type document
type document;
These types are referred to as "abstract types" and are commonly used together with external functions that define operations over values when communicating with JavaScript.
Abstract types enable defining types for specific values originating from
JavaScript while omitting unnecessary details. An illustration is the document
type mentioned earlier, which has several
properties. By
using abstract types, one can focus solely on the required aspects of the
document
value that the Melange program requires, rather than defining all its
properties. Consider the following example:
type document
external document : document = "document"
external set_title : document -> string -> unit = "title" [@@mel.set]
type document;
external document: document = "document";
[@mel.set] external set_title: (document, string) => unit = "title";
Subsequent sections delve into the details about the
mel.set
attribute and how to bind to global
values like document
.
For a comprehensive understanding of abstract types and their usefulness, refer to the "Encapsulation" section of the OCaml Cornell textbook.
Pipe operators
There are two pipe operators available in Melange:
-
A pipe last operator
|>
, available in OCaml and inherited in Melange -
A pipe first operator
|.
->
, available exclusively in Melange
Let’s see the differences between the two.
Pipe last
Since version 4.01, OCaml includes a reverse application or "pipe" (|>
)
operator, an infix operator that applies the result from the previous expression
the next function. As a backend for OCaml, Melange inherits this operator.
The pipe operator could be implemented like this (the real implementation is a bit different):
let ( |> ) f g = g f
let (|>) = (f, g) => g(f);
This operator is useful when multiple functions are applied to some value in sequence, with the output of each function becoming the input of the next (a pipeline).
For example, assuming we have a function square
defined as:
let square x = x * x
let square = x => x * x;
We are using it like:
let ten = succ (square 3)
let ten = succ(square(3));
The pipe operator allows to write the computation for ten
in left-to-right
order, as it has left
associativity:
let ten = 3 |> square |> succ
let ten = 3 |> square |> succ;
When working with functions that can take multiple arguments, the pipe operator works best when the functions take the data we are processing as the last argument. For example:
let sum = List.fold_left ( + ) 0
let sum_sq =
[ 1; 2; 3 ]
|> List.map square (* [1; 4; 9] *)
|> sum (* 1 + 4 + 9 *)
let sum = List.fold_left((+), 0);
let sum_sq =
[1, 2, 3]
|> List.map(square) /* [1; 4; 9] */
|> sum; /* 1 + 4 + 9 */
The above example can be written concisely because the List.map
function in
the OCaml standard library takes
the list as the second argument. This convention is sometimes referred to as
"data-last", and is widely adopted in the OCaml ecosystem. Data-last and the
pipe operator |>
work great with currying, so they are a great fit for the
language.
However, there are some limitations when using data-last when it comes to error handling. In the given example, if we mistakenly used the wrong function:
let sum_sq =
[ 1; 2; 3 ]
|> List.map String.cat
|> sum
let sum_sq = [1, 2, 3] |> List.map(String.cat) |> sum;
The compiler would rightfully raise an error:
4 | [ 1; 2; 3 ]
^
Error: This expression has type int but an expression was expected of type
string
1 | [ 1, 2, 3 ]
^
Error: This expression has type int but an expression was expected of type
string
Note that instead of telling us that we are passing the wrong function in
List.map
(String.cat
), the error points to the list itself. This behavior
aligns with the way type inference works, as the compiler infers types from left
to right. Since [ 1; 2; 3 ] |> List.map String.cat
is equivalent to List.map
String.cat [ 1; 2; 3 ]
, the type mismatch is detected when the list is type
checked, after String.cat
has been processed.
With the goal of addressing this kind of limitations, Melange introduces the
pipe first operator |.
->
.
Pipe first
To overcome the constraints mentioned above, Melange introduces the pipe first
operator |.
->
.
As its name suggests, the pipe first operator is better suited for functions where the data is passed as the first argument.
The functions in the Belt
libraryBelt
library included with Melange have been designed with the data-first
convention in mind, so they work best with the pipe first operator.
For example, we can rewrite the example above using Belt.List.map
and the pipe
first operator:
let sum_sq =
[ 1; 2; 3 ]
|. Belt.List.map square
|. sum
let sum_sq = [1, 2, 3]->(Belt.List.map(square))->sum;
We can see the difference on the error we get if the wrong function is passed to
Belt.List.map
:
let sum_sq =
[ 1; 2; 3 ]
|. Belt.List.map String.cat
|. sum
let sum_sq = [1, 2, 3]->(Belt.List.map(String.cat))->sum;
The compiler will show this error message:
4 | |. Belt.List.map String.cat
^^^^^^^^^^
Error: This expression has type string -> string -> string
but an expression was expected of type int -> 'a
Type string is not compatible with type int
2 | let sum_sq = [1, 2, 3]->(Belt.List.map(String.cat))->sum;
^^^^^^^^^^
Error: This expression has type string -> string -> string
but an expression was expected of type int -> 'a
Type string is not compatible with type int
The error points now to the function passed to Belt.List.map
, which is more
natural with the way the code is being written.
Melange supports writing bindings to JavaScript using any of the two conventions, data-first or data-last, as shown in the "Chaining" section.
For further details about the differences between the two operators, the data-first and data-last conventions and the trade-offs between them, one can refer to this related blog post.
Data types and runtime representation
This is how each Melange type is converted into JavaScript values:
Melange | JavaScript |
---|---|
int | number |
nativeint | number |
int32 | number |
float | number |
string | string |
array | array |
tuple (3, 4) |
array [3, 4] |
bool | boolean |
Js.Nullable.tJs.Nullable.t | null / undefined |
Js.Re.tJs.Re.t | RegExp |
Option.t None |
undefined |
Option.t Some( Some .. Some (None)) Some(Some( .. Some(None))) |
internal representation |
Option.t Some 2 Some(2) |
2 |
record {x = 1; y = 2} {x: 1; y: 2} |
object {x: 1, y: 2} |
int64 | array of 2 elements [high, low] high is signed, low unsigned |
char | 'a' -> 97 (ascii code) |
bytes | number array |
list [] |
0 |
list [ x; y ] [x, y] |
{ hd: x, tl: { hd: y, tl: 0 } } |
variant | See below |
polymorphic variant | See below |
Variants with a single non-nullary constructor:
type tree = Leaf | Node of int * tree * tree
(* Leaf -> 0 *)
(* Node(7, Leaf, Leaf) -> { _0: 7, _1: 0, _2: 0 } *)
type tree =
| Leaf
| Node(int, tree, tree);
/* Leaf -> 0 */
/* Node(7, Leaf, Leaf) -> { _0: 7, _1: 0, _2: 0 } */
Variants with more than one non-nullary constructor:
type t = A of string | B of int
(* A("foo") -> { TAG: 0, _0: "Foo" } *)
(* B(2) -> { TAG: 1, _0: 2 } *)
type t =
| A(string)
| B(int);
/* A("foo") -> { TAG: 0, _0: "Foo" } */
/* B(2) -> { TAG: 1, _0: 2 } */
Polymorphic variants:
let u = `Foo (* "Foo" *)
let v = `Foo(2) (* { NAME: "Foo", VAL: "2" } *)
let u = `Foo; /* "Foo" */
let v = `Foo(2); /* { NAME: "Foo", VAL: "2" } */
Let’s see now some of these types in detail. We will first describe the shared types, which have a transparent representation as JavaScript values, and then go through the non-shared types, that have more complex runtime representations.
NOTE: Relying on the non-shared data types runtime representations by reading or writing them manually from JavaScript code that communicates with Melange code might lead to runtime errors, as these representations might change in the future.
Shared types
The following are types that can be shared between Melange and JavaScript almost "as is". Specific caveats are mentioned on the sections where they apply.
Strings
JavaScript strings are immutable sequences of UTF-16 encoded Unicode text. OCaml strings are immutable sequences of bytes and nowadays assumed to be UTF-8 encoded text when interpreted as textual content. This is problematic when interacting with JavaScript code, because if one tries to use some unicode characters, like:
let () = Js.log "你好"
let () = Js.log("你好");
It will lead to some cryptic console output. To rectify this, Melange allows to
define quoted string
literals using the
js
identifier, for example:
let () = Js.log {js|你好,
世界|js}
let () = Js.log({js|你好,
世界|js});
For convenience, Melange exposes another special quoted string identifier: j
.
It is similar to JavaScript’ string interpolation, but for variables only (not
arbitrary expressions):
let world = {j|世界|j}
let helloWorld = {j|你好,$world|j}
let world = {j|世界|j};
let helloWorld = {j|你好,$world|j};
You can surround the interpolation variable in parentheses too: {j|你
好,$(world)|j}
.
To work with strings, the Melange standard library provides some utilities in
the Stdlib.String
moduleStdlib.String
module.
The bindings to the native JavaScript functions to work with strings are in the
Js.String
moduleJs.String
module.
Floating-point numbers
OCaml floats are IEEE
754
with a 53-bit mantissa and exponents from -1022 to 1023. This happens to be the
same encoding as JavaScript
numbers,
so values of these types can be used transparently between Melange code and
JavaScript code. The Melange standard library provides a Stdlib.Float
moduleStdlib.Float
module. The
bindings to the JavaScript APIs that manipulate float values can be found in the
Js.Float
moduleJs.Float
module.
Integers
In Melange, integers are limited to 32 bits due to the fixed-width
conversion
of bitwise operations in JavaScript. While Melange integers compile to
JavaScript numbers, treating them interchangeably can result in unexpected
behavior due to differences in precision. Even though bitwise operations in
JavaScript are constrained to 32 bits, integers themselves are represented using
the same floating-point format as
numbers,
allowing for a larger range of representable integers in JavaScript compared to
Melange. When dealing with large numbers, it is advisable to use floats instead.
For instance, floats are used in bindings like Js.Date
.
The Melange standard library provides a Stdlib.Int
moduleStdlib.Int
module. The
bindings to work with JavaScript integers are in the Js.Int
moduleJs.Int
module.
Arrays
Melange arrays compile to JavaScript arrays. But note that unlike JavaScript arrays, all the values in a Melange array need to have the same type.
Another difference is that OCaml arrays are fixed-sized, but on Melange side
this constraint is relaxed. You can change an array’s length using functions
like Js.Array.push
, available in the bindings to the JavaScript APIs in the Js.Array
moduleJs.Array
module.
Melange standard library also has a module to work with arrays, available in the
Stdlib.Array
moduleStdlib.Array
module.
Tuples
OCaml tuples are compiled to JavaScript arrays. This is convenient when writing bindings that will use a JavaScript array with heterogeneous values, but that happens to have a fixed length.
As a real world example of this can be found in ReasonReact, the Melange bindings for React. In these bindings, component effects dependencies are represented as OCaml tuples, so they get compiled cleanly to JavaScript arrays by Melange.
For example, some code like this:
let () = React.useEffect2 (fun () -> None) (foo, bar)
let () = React.useEffect2(() => None, (foo, bar));
Will produce:
React.useEffect(function () {}, [foo, bar]);
Booleans
Values of type bool
compile to JavaScript booleans.
Records
Melange records map directly to JavaScript objects. If the record fields include non-shared data types (like variants), these values should be transformed separately, and not be directly used in JavaScript.
Extensive documentation about interfacing with JavaScript objects using records can be found in the section below.
Regular expressions
Regular expressions created using the %mel.re
extension node compile to their
JavaScript counterpart.
For example:
let r = [%mel.re "/b/g"]
let r = [%mel.re "/b/g"];
Will compile to:
var r = /b/g;
A regular expression like the above is of type Js.Re.t
. The Js.Re
moduleJs.Re
module provides the
bindings to the JavaScript functions that operate over regular expressions.
Non-shared data types
The following types differ too much between Melange and JavaScript, so while they can always be manipulated from JavaScript, it is recommended to transform them before doing so.
- Variants and polymorphic variants: Better transform them into readable JavaScript values before manipulating them from JavaScript, Melange provides some helpers to do so.
- Exceptions
- Option (a variant type): Better use the
Js.Nullable.fromOption
andJs.Nullable.toOption
functions in theJs.Nullable
moduleJs.Nullable
module to transform them into eithernull
orundefined
values. - List (also a variant type): use
Array.of_list
andArray.to_list
in theStdlib.Array
moduleStdlib.Array
module. - Character
- Int64
- Lazy values
List of attributes and extension nodes
Attributes:
These attributes are used to annotate external
definitions:
mel.get
: read JavaScript object properties statically by name, using the dot notation.
mel.get_index
: read a JavaScript object’s properties dynamically by using the bracket notation[]
mel.module
: bind to a value from a JavaScript modulemel.new
: bind to a JavaScript class constructormel.obj
: create a JavaScript objectmel.return
: automate conversion from nullable values toOption.t
valuesmel.send
: call a JavaScript object method using pipe first conventionmel.send.pipe
: call a JavaScript object method using pipe last conventionmel.set
: set JavaScript object properties statically by name, using the dot notation.
mel.set_index
: set JavaScript object properties dynamically by using the bracket notation[]
mel.scope
: reach to deeper properties inside a JavaScript objectmel.splice
: a deprecated attribute, is an alternate form ofmel.variadic
mel.variadic
: bind to a function taking variadic arguments from an array
These attributes are used to annotate arguments in external
definitions:
u
: define function arguments as uncurried (manual)mel.int
: compile function argument to an intmel.string
: compile function argument to a stringmel.this
: bind tothis
based callbacksmel.uncurry
: define function arguments as uncurried (automated)mel.unwrap
: unwrap variant values
These attributes are used in places like records, fields, arguments, functions, and more:
mel.as
: redefine the name generated in the JavaScript output code. Used in constant function arguments, variants, polymorphic variants (either inlined in external functions or in type definitions) and record fields.deriving
: generate getters and setters for some typesmel.inline
: forcefully inline constant valuesoptional
: omit fields in a record (combines withderiving
)
Extension nodes:
In order to use any of these extension nodes, you will have to add the melange
PPX preprocessor to your project. To do so, add the following to the dune
file:
(library
(name lib)
(modes melange)
(preprocess
(pps melange.ppx)))
The same field preprocess
can be added to melange.emit
.
Here is the list of all the extension nodes supported by Melange:
mel.debugger
: insertdebugger
statementsmel.external
: read global valuesmel.obj
: create JavaScript object literalsmel.raw
: write raw JavaScript codemel.re
: insert regular expressions
Generate raw JavaScript
It is possible to directly write JavaScript code from a Melange file. This is unsafe, but it can be useful for prototyping or as an escape hatch.
To do it, we will use the mel.raw
extension:
let add = [%mel.raw {|
function(a, b) {
console.log("hello from raw JavaScript!");
return a + b;
}
|}]
let () = Js.log (add 1 2)
let add = [%mel.raw
{|
function(a, b) {
console.log("hello from raw JavaScript!");
return a + b;
}
|}
];
let () = Js.log(add(1, 2));
The {||}
strings are called "quoted
strings". They are similar
to JavaScript’s template literals, in the sense that they are multi-line, and
there is no need to escape characters inside the string.
Using one percentage signthe extension name between square brackets
([%mel.raw <string>]
) is useful to define expressions (function bodies, or
other values) where the implementation is directly JavaScript. This is useful as
we can attach the type signature already in the same line, to make our code
safer. For example:
let f : unit -> int = [%mel.raw "function() {return 1}"]
let f: unit => int = ([%mel.raw "function() {return 1}"]: unit => int);
Using two percentage signs ([%%mel.raw
<string>]
)the extension name without square
brackets (%mel.raw <string>
) is reserved for definitions in a
structure
or signature.
For example:
[%%mel.raw "var a = 1"]
[%%mel.raw "var a = 1"];
Debugger
Melange allows you to inject a debugger;
expression using the mel.debugger
extension:
let f x y =
[%mel.debugger];
x + y
let f = (x, y) => {
[%mel.debugger];
x + y;
};
Output:
function f (x,y) {
debugger; // JavaScript developer tools will set a breakpoint and stop here
return x + y | 0;
}
Detect global variables
Melange provides a relatively type safe approach to use globals that might be
defined either in the JavaScript runtime environment: mel.external
.
[%mel.external id]
will check if the JavaScript value id
is undefined
or
not, and return an Option.t
value accordingly.
For example:
let () = match [%mel.external __DEV__] with
| Some _ -> Js.log "dev mode"
| None -> Js.log "production mode"
let () =
switch ([%mel.external __DEV__]) {
| Some(_) => Js.log("dev mode")
| None => Js.log("production mode")
};
Another example:
let () = match [%mel.external __filename] with
| Some f -> Js.log f
| None -> Js.log "non-node environment"
let () =
switch ([%mel.external __filename]) {
| Some(f) => Js.log(f)
| None => Js.log("non-node environment")
};
Inlining constant values
Some JavaScript idioms require special constants to be inlined since they serve
as de-facto directives for bundlers. A common example is process.env.NODE_ENV
:
if (process.env.NODE_ENV !== "production") {
// Development-only code
}
becomes:
if ("development" !== "production") {
// Development-only code
}
In this case, bundlers such as Webpack can tell that the if
statement always
evaluates to a specific branch and eliminate the others.
Melange provides the mel.inline
attribute to achieve the same goal in
generated JavaScript. Let’s look at an example:
external node_env : string = "NODE_ENV" [@@mel.scope "process", "env"]
let development = "development"
let () = if node_env <> development then Js.log "Only in Production"
let development_inline = "development" [@@mel.inline]
let () = if node_env <> development_inline then Js.log "Only in Production"
[@mel.scope ("process", "env")] external node_env: string = "NODE_ENV";
let development = "development";
let () =
if (node_env != development) {
Js.log("Only in Production");
};
[@mel.inline]
let development_inline = "development";
let () =
if (node_env != development_inline) {
Js.log("Only in Production");
};
As we can see in the generated JavaScript presented below:
- the
development
variable is emitted- it gets used as a variable
process.env.NODE_ENV !== development
in theif
statement
- it gets used as a variable
- the
development_inline
variable isn’t present in the final output- its value is inlined in the
if
statement:process.env.NODE_ENV !== "development"
- its value is inlined in the
var development = "development";
if (process.env.NODE_ENV !== development) {
console.log("Only in Production");
}
if (process.env.NODE_ENV !== "development") {
console.log("Only in Production");
}
Bind to JavaScript objects
JavaScript objects are used in a variety of use cases:
- As a fixed shape record.
- As a map or dictionary.
- As a class.
- As a module to import/export.
Melange separates the binding methods for JavaScript objects based on these four use cases. This section documents the first three. Binding to JavaScript module objects is described in the "Using functions from other JavaScript modules" section.
Objects with static shape (record-like)
Using OCaml records
If your JavaScript object has fixed fields, then it’s conceptually like an OCaml record. Since Melange compiles records into JavaScript objects, the most common way to bind to JavaScript objects is using records.
type person = {
name : string;
friends : string array;
age : int;
}
external john : person = "john" [@@mel.module "MySchool"]
let john_name = john.name
type person = {
name: string,
friends: array(string),
age: int,
};
[@mel.module "MySchool"] external john: person = "john";
let john_name = john.name;
This is the generated JavaScript:
var MySchool = require("MySchool");
var john_name = MySchool.john.name;
External functions are documented in a previous section.
The mel.module
attribute is documented
here.
If you want or need to use different field names on the Melange and the
JavaScript sides, you can use the mel.as
decorator:
type action = {
type_ : string [@mel.as "type"]
}
let action = { type_ = "ADD_USER" }
type action = {
[@mel.as "type"]
type_: string,
};
let action = {type_: "ADD_USER"};
Which generates the JavaScript code:
var action = {
type: "ADD_USER"
};
This is useful to map to JavaScript attribute names that cannot be expressed in Melange, for example, where the JavaScript name we want to generate is a reserved keyword.
It is also possible to map a Melange record to a JavaScript array by passing
indices to the mel.as
decorator:
type t = {
foo : int; [@mel.as "0"]
bar : string; [@mel.as "1"]
}
let value = { foo = 7; bar = "baz" }
type t = {
[@mel.as "0"]
foo: int,
[@mel.as "1"]
bar: string,
};
let value = {foo: 7, bar: "baz"};
And its JavaScript generated code:
var value = [
7,
"baz"
];
Using Js.t
objects
Alternatively to records, Melange offers another type that can be used to
produce JavaScript objects. This type is 'a Js.t
, where 'a
is an OCaml
object.
The advantage of objects versus records is that no type declaration is needed in advance, which can be helpful for prototyping or quickly generating JavaScript object literals.
Melange provides some ways to create Js.t
object values, as well as accessing
the properties inside them. To create values, the [%mel.obj]
extension is
used, and the ##
infix operator allows to read from the object properties:
let john = [%mel.obj { name = "john"; age = 99 }]
let t = john##name
let john = {"name": "john", "age": 99};
let t = john##name;
Which generates:
var john = {
name: "john",
age: 99
};
var t = john.name;
Note that object types allow for some flexibility that the record types do not have. For example, an object type can be coerced to another with fewer values or methods, while it is impossible to coerce a record type to another one with fewer fields. So different object types that share some methods can be mixed in a data structure where only their common methods are visible.
To give an example, one can create a function that operates in all the object
types that include a field name
that is of type string, e.g.:
let name_extended obj = obj##name ^ " wayne"
let one = name_extended [%mel.obj { name = "john"; age = 99 }]
let two = name_extended [%mel.obj { name = "jane"; address = "1 infinite loop" }]
let name_extended = obj => obj##name ++ " wayne";
let one = name_extended({"name": "john", "age": 99});
let two = name_extended({"name": "jane", "address": "1 infinite loop"});
To read more about objects and polymorphism we recommend checking the OCaml docs or the OCaml manual.
Using external functions
We have already explored one approach for creating JavaScript object literals by
using Js.t
values and the mel.obj
extension.
Melange additionally offers the mel.obj
attribute, which can be used in
combination with external functions to create JavaScript objects. When these
functions are called, they generate objects with fields corresponding to the
labeled arguments of the function.
If any of these labeled arguments are defined as optional and omitted during function application, the resulting JavaScript object will exclude the corresponding fields. This allows to create runtime objects and control whether optional keys are emitted at runtime.
For example, assuming we need to bind to a JavaScript object like this:
var homeRoute = {
type: "GET",
path: "/",
action: () => console.log("Home"),
// options: ...
};
The first three fields are required and the options
field is optional. You can
declare a binding function like:
external route :
_type:string ->
path:string ->
action:(string list -> unit) ->
?options:< .. > ->
unit ->
_ = ""
[@@mel.obj]
[@mel.obj]
external route:
(
~_type: string,
~path: string,
~action: list(string) => unit,
~options: {..}=?,
unit
) =>
_;
Note that the empty string at the end of the function is used to make it syntactically valid. The value of this string is ignored by the compiler.
Since there is an optional argument options
, an additional unlabeled argument
of type unit
is included after it. It allows to omit the optional argument on
function application. More information about labeled optional arguments can be
found in the OCaml
manual.
The return type of the function should be left unspecified using the wildcard
type _
. Melange will automatically infer the type of the resulting JavaScript
object.
In the route function, the _type
argument starts with an underscore. When
binding to JavaScript objects with fields that are reserved keywords in OCaml,
Melange allows the use of an underscore prefix for the labeled arguments. The
resulting JavaScript object will have the underscore removed from the field
names. This is only required for the mel.obj
attribute, while for other cases,
the mel.as
attribute can be used to rename fields.
If we call the function like this:
let homeRoute = route ~_type:"GET" ~path:"/" ~action:(fun _ -> Js.log "Home") ()
let homeRoute =
route(~_type="GET", ~path="/", ~action=_ => Js.log("Home"), ());
We get the following JavaScript, which does not include the options
field
since its argument wasn’t present:
var homeRoute = {
type: "GET",
path: "/",
action: (function (param) {
console.log("Home");
})
};
Bind to object properties
If you need to bind only to the property of a JavaScript object, you can use
mel.get
and mel.set
to access it using the dot notation .
:
(* Abstract type for the `document` value *)
type document
external document : document = "document"
external set_title : document -> string -> unit = "title" [@@mel.set]
external get_title : document -> string = "title" [@@mel.get]
let current = get_title document
let () = set_title document "melange"
/* Abstract type for the `document` value */
type document;
external document: document = "document";
[@mel.set] external set_title: (document, string) => unit = "title";
[@mel.get] external get_title: document => string = "title";
let current = get_title(document);
let () = set_title(document, "melange");
This generates:
var current = document.title;
document.title = "melange";
Alternatively, if some dynamism is required on the way the property is accessed,
you can use mel.get_index
and mel.set_index
to access it using the bracket
notation []
:
type t
external create : int -> t = "Int32Array" [@@mel.new]
external get : t -> int -> int = "" [@@mel.get_index]
external set : t -> int -> int -> unit = "" [@@mel.set_index]
let () =
let i32arr = (create 3) in
set i32arr 0 42;
Js.log (get i32arr 0)
type t;
[@mel.new] external create: int => t = "Int32Array";
[@mel.get_index] external get: (t, int) => int;
[@mel.set_index] external set: (t, int, int) => unit;
let () = {
let i32arr = create(3);
set(i32arr, 0, 42);
Js.log(get(i32arr, 0));
};
Which generates:
var i32arr = new Int32Array(3);
i32arr[0] = 42;
console.log(i32arr[0]);
Objects with dynamic shape (dictionary-like)
Sometimes JavaScript objects are used as dictionaries. In these cases:
- All values stored in the object belong to the same type
- Key-value pairs can be added or removed at runtime
For this particular use case of JavaScript objects, Melange exposes a specific
type Js.Dict.t
. The values and functions to work with values of this type are
defined in the Js.Dict
moduleJs.Dict
module, with operations like get
, set
, etc.
Values of the type Js.Dict.t
compile to JavaScript objects.
JavaScript classes
JavaScript classes are special kinds of objects. To interact with classes,
Melange exposes mel.new
to emulate e.g. new Date()
:
type t
external create_date : unit -> t = "Date" [@@mel.new]
let date = create_date ()
type t;
[@mel.new] external create_date: unit => t = "Date";
let date = create_date();
Which generates:
var date = new Date();
You can chain mel.new
and mel.module
if the JavaScript class you want to
work with is in a separate JavaScript module:
type t
external book : unit -> t = "Book" [@@mel.new] [@@mel.module]
let myBook = book ()
type t;
[@mel.new] [@mel.module] external book: unit => t = "Book";
let myBook = book();
Which generates:
var Book = require("Book");
var myBook = new Book();
Bind to JavaScript functions or values
Using global functions or values
Binding to a JavaScript function available globally makes use of external
,
like with objects. But unlike objects, there is no need to add any attributes:
(* Abstract type for `timeoutId` *)
type timeoutId
external setTimeout : (unit -> unit) -> int -> timeoutId = "setTimeout"
external clearTimeout : timeoutId -> unit = "clearTimeout"
let id = setTimeout (fun () -> Js.log "hello") 100
let () = clearTimeout id
/* Abstract type for `timeoutId` */
type timeoutId;
external setTimeout: (unit => unit, int) => timeoutId = "setTimeout";
external clearTimeout: timeoutId => unit = "clearTimeout";
let id = setTimeout(() => Js.log("hello"), 100);
let () = clearTimeout(id);
NOTE: The bindings to
setTimeout
andclearTimeout
are shown here for learning purposes, but they are already available in theJs.Global
moduleJs.Global
module.
Generates:
var id = setTimeout(function (param) {
console.log("hello");
}, 100);
clearTimeout(id);
Global bindings can also be applied to values:
(* Abstract type for `document` *)
type document
external document : document = "document"
let document = document
/* Abstract type for `document` */
type document;
external document: document = "document";
let document = document;
Which generates:
var doc = document;
Using functions from other JavaScript modules
mel.module
allows to bind to values that belong to another JavaScript module.
It accepts a string with the name of the module, or the relative path to it.
external dirname : string -> string = "dirname" [@@mel.module "path"]
let root = dirname "/User/github"
[@mel.module "path"] external dirname: string => string = "dirname";
let root = dirname("/User/github");
Generates:
var Path = require("path");
var root = Path.dirname("/User/github");
Binding to properties inside a module or global
For cases when we need to create bindings for a property within a module or a
global JavaScript object, Melange provides the mel.scope
attribute.
For example, if we want to write some bindings for a specific property
commands
from the vscode
package, we
can do:
type param
external executeCommands : string -> param array -> unit = ""
[@@mel.scope "commands"] [@@mel.module "vscode"] [@@mel.variadic]
let f a b c = executeCommands "hi" [| a; b; c |]
type param;
[@mel.scope "commands"] [@mel.module "vscode"] [@mel.variadic]
external executeCommands: (string, array(param)) => unit;
let f = (a, b, c) => executeCommands("hi", [|a, b, c|]);
Which compiles to:
var Vscode = require("vscode");
function f(a, b, c) {
Vscode.commands.executeCommands("hi", a, b, c);
}
The mel.scope
attribute can take multiple arguments as payload, in case we
want to reach deeper into the object from the module we are importing.
For example:
type t
external back : t = "back"
[@@mel.module "expo-camera"] [@@mel.scope "Camera", "Constants", "Type"]
let camera_type_back = back
type t;
[@mel.module "expo-camera"] [@mel.scope ("Camera", "Constants", "Type")]
external back: t = "back";
let camera_type_back = back;
Which generates:
var ExpoCamera = require("expo-camera");
var camera_type_back = ExpoCamera.Camera.Constants.Type.back;
It can be used without mel.module
, to created scoped bindings to global
values:
external imul : int -> int -> int = "imul" [@@mel.scope "Math"]
let res = imul 1 2
[@mel.scope "Math"] external imul: (int, int) => int = "imul";
let res = imul(1, 2);
Which produces:
var res = Math.imul(1, 2);
Or it can be used together with mel.new
:
type t
external create : unit -> t = "GUI"
[@@mel.new] [@@mel.scope "default"] [@@mel.module "dat.gui"]
let gui = create ()
type t;
[@mel.new] [@mel.scope "default"] [@mel.module "dat.gui"]
external create: unit => t = "GUI";
let gui = create();
Which generates:
var DatGui = require("dat.gui");
var gui = new (DatGui.default.GUI)();
Labeled arguments
OCaml has labeled arguments,
which can also be optional, and work with external
as well.
Labeled arguments can be useful to provide more information about the arguments of a JavaScript function that is called from Melange.
Let’s say we have the following JavaScript function, that we want to call from Melange:
// MyGame.js
function draw(x, y, border) {
// let’s assume `border` is optional and defaults to false
}
draw(10, 20)
draw(20, 20, true)
When writing Melange bindings, we can add labeled arguments to make things more clear:
external draw : x:int -> y:int -> ?border:bool -> unit -> unit = "draw"
[@@module "MyGame"]
let () = draw ~x:10 ~y:20 ~border:true ()
let () = draw ~x:10 ~y:20 ()
[@module "MyGame"]
external draw: (~x: int, ~y: int, ~border: bool=?, unit) => unit = "draw";
let () = draw(~x=10, ~y=20, ~border=true, ());
let () = draw(~x=10, ~y=20, ());
Generates:
var MyGame = require("MyGame");
MyGame.draw(10, 20, true);
MyGame.draw(10, 20, undefined);
The generated JavaScript function is the same, but now the usage in Melange is much clearer.
Note: in this particular case, a final param of type unit, ()
must be
added after border
, since border
is an optional argument at the last
position. Not having the last param unit
would lead to a warning, which is
explained in detail in the OCaml
documentation.
Note that you can freely reorder the labeled arguments when applying the function on the Melange side. The generated code will maintain the original order that was used when declaring the function:
external draw : x:int -> y:int -> ?border:bool -> unit -> unit = "draw"
[@@module "MyGame"]
let () = draw ~x:10 ~y:20 ()
let () = draw ~y:20 ~x:10 ()
[@module "MyGame"]
external draw: (~x: int, ~y: int, ~border: bool=?, unit) => unit = "draw";
let () = draw(~x=10, ~y=20, ());
let () = draw(~y=20, ~x=10, ());
Generates:
var MyGame = require("MyGame");
MyGame.draw(10, 20, undefined);
MyGame.draw(10, 20, undefined);
Calling an object method
If we need to call a JavaScript method, Melange provides the attribute
mel.send
.
In the following snippets, we will be referring to a type
Dom.element
, which is provided within the librarymelange.dom
. You can add it to your project by including(libraries melange.dom)
to yourdune
file:
(* Abstract type for the `document` global *)
type document
external document : document = "document"
external get_by_id : document -> string -> Dom.element = "getElementById"
[@@mel.send]
let el = get_by_id document "my-id"
/* Abstract type for the `document` global */
type document;
external document: document = "document";
[@mel.send]
external get_by_id: (document, string) => Dom.element = "getElementById";
let el = get_by_id(document, "my-id");
Generates:
var el = document.getElementById("my-id");
When using mel.send
, the first argument will be the object that holds the
property with the function we want to call. This combines well with the pipe
first operator |.
->
, see the "Chaining" section
below.
If we want to design our bindings to be used with OCaml pipe last operator |>
,
there is an alternate mel.send.pipe
attribute. Let’s rewrite the example above
using it:
(* Abstract type for the `document` global *)
type document
external document : document = "document"
external get_by_id : string -> Dom.element = "getElementById"
[@@mel.send.pipe: document]
let el = get_by_id "my-id" document
/* Abstract type for the `document` global */
type document;
external document: document = "document";
[@mel.send.pipe: document]
external get_by_id: string => Dom.element = "getElementById";
let el = get_by_id("my-id", document);
Generates the same code as mel.send
:
var el = document.getElementById("my-id");
Chaining
It is common to find this kind of API in JavaScript: foo().bar().baz()
. This
kind of API can be designed with Melange externals. Depending on which
convention we want to use, there are two attributes available:
- For a data-first convention, the
mel.send
attribute, in combination with the pipe first operator|.
->
- For a data-last convention, the
mel.send.pipe
attribute, in combination with OCaml pipe last operator|>
.
Let’s see first an example of chaining using data-first convention with the pipe
first operator |.
->
:
(* Abstract type for the `document` global *)
type document
external document : document = "document"
external get_by_id : document -> string -> Dom.element = "getElementById"
[@@mel.send]
external get_by_classname : Dom.element -> string -> Dom.element
= "getElementsByClassName"
[@@mel.send]
let el = document |. get_by_id "my-id" |. get_by_classname "my-class"
/* Abstract type for the `document` global */
type document;
external document: document = "document";
[@mel.send]
external get_by_id: (document, string) => Dom.element = "getElementById";
[@mel.send]
external get_by_classname: (Dom.element, string) => Dom.element =
"getElementsByClassName";
let el = document->(get_by_id("my-id"))->(get_by_classname("my-class"));
Will generate:
var el = document.getElementById("my-id").getElementsByClassName("my-class");
Now with pipe last operator |>
:
(* Abstract type for the `document` global *)
type document
external document : document = "document"
external get_by_id : string -> Dom.element = "getElementById"
[@@mel.send.pipe: document]
external get_by_classname : string -> Dom.element = "getElementsByClassName"
[@@mel.send.pipe: Dom.element]
let el = document |> get_by_id "my-id" |> get_by_classname "my-class"
/* Abstract type for the `document` global */
type document;
external document: document = "document";
[@mel.send.pipe: document]
external get_by_id: string => Dom.element = "getElementById";
[@mel.send.pipe: Dom.element]
external get_by_classname: string => Dom.element = "getElementsByClassName";
let el = document |> get_by_id("my-id") |> get_by_classname("my-class");
Will generate the same JavaScript as the pipe first version:
var el = document.getElementById("my-id").getElementsByClassName("my-class");
Variadic function arguments
Sometimes JavaScript functions take an arbitrary amount of arguments. For these
cases, Melange provides the mel.variadic
attribute, which can be attached to
the external
declaration. However, there is one caveat: all the variadic
arguments need to belong to the same type.
external join : string array -> string = "join"
[@@mel.module "path"] [@@mel.variadic]
let v = join [| "a"; "b" |]
[@mel.module "path"] [@mel.variadic]
external join: array(string) => string = "join";
let v = join([|"a", "b"|]);
Generates:
var Path = require("path");
var v = Path.join("a", "b");
If more dynamism is needed, there is a way to inject elements with different
types in the array and still have Melange compile to JavaScript values that are
not wrapped using the OCaml
unboxed
attribute, which was
mentioned in the OCaml attributes section:
type hide = Hide : 'a -> hide [@@unboxed]
external join : hide array -> string = "join" [@@mel.module "path"] [@@mel.variadic]
let v = join [| Hide "a"; Hide 2 |]
[@unboxed]
type hide =
| Hide('a): hide;
[@mel.module "path"] [@mel.variadic]
external join: array(hide) => string = "join";
let v = join([|Hide("a"), Hide(2)|]);
Compiles to:
var Path = require("path");
var v = Path.join("a", 2);
Bind to a polymorphic function
Some JavaScript libraries will define functions where the arguments can vary on both type and shape. There are two approaches to bind to those, depending on how dynamic they are.
Approach 1: Multiple external functions
If it is possible to enumerate the many forms an overloaded JavaScript function can take, a flexible approach is to bind to each form individually:
external drawCat : unit -> unit = "draw" [@@mel.module "MyGame"]
external drawDog : giveName:string -> unit = "draw" [@@mel.module "MyGame"]
external draw : string -> useRandomAnimal:bool -> unit = "draw"
[@@mel.module "MyGame"]
[@mel.module "MyGame"] external drawCat: unit => unit = "draw";
[@mel.module "MyGame"] external drawDog: (~giveName: string) => unit = "draw";
[@mel.module "MyGame"]
external draw: (string, ~useRandomAnimal: bool) => unit = "draw";
Note how all three externals bind to the same JavaScript function, draw
.
Approach 2: Polymorphic variant + mel.unwrap
In some cases, the function has a constant number of arguments but the type of
the argument can vary. For cases like this, we can model the argument as a
variant and use the mel.unwrap
attribute in the external.
Let’s say we want to bind to the following JavaScript function:
function padLeft(value, padding) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
As the padding
argument can be either a number or a string, we can use
mel.unwrap
to define it. It is important to note that mel.unwrap
imposes
certain requirements on the type it is applied to:
- It needs to be a polymorphic variant
- Its definition needs to be inlined
- Each variant tag needs to have an argument
- The variant type can not be opened (can’t use
>
)
external padLeft:
string
-> ([ `Str of string
| `Int of int
] [@mel.unwrap])
-> string
= "padLeft"
let _ = padLeft "Hello World" (`Int 4)
let _ = padLeft "Hello World" (`Str "Message from Melange: ")
external padLeft:
(string, [@mel.unwrap] [ | `Str(string) | `Int(int)]) => string =
"padLeft";
let _ = padLeft("Hello World", `Int(4));
let _ = padLeft("Hello World", `Str("Message from Melange: "));
Which produces the following JavaScript:
padLeft("Hello World", 4);
padLeft("Hello World", "Message from Melange: ");
As we saw in the Non-shared data types section, we
should rather avoid passing variants directly to the JavaScript side. By using
mel.unwrap
we get the best of both worlds: from Melange we can use variants,
while JavaScript gets the raw values inside them.
Using polymorphic variants to bind to enums
Some JavaScript APIs take a limited subset of values as input. For example,
Node’s fs.readFileSync
second argument can only take a few given string
values: "ascii"
, "utf8"
, etc. Some other functions can take values from a
few given integers, like the createStatusBarItem
function in VS Code API,
which can take an alignment
parameter that can only be 1
or
2
.
One could still type these parameters as just string
or int
, but this would
not prevent consumers of the external function from calling it using values that
are unsupported by the JavaScript function. Let’s see how we can use polymorphic
variants to avoid runtime errors.
If the values are strings, we can use the mel.string
attribute:
external read_file_sync :
name:string -> ([ `utf8 | `ascii ][@mel.string]) -> string = "readFileSync"
[@@mel.module "fs"]
let _ = read_file_sync ~name:"xx.txt" `ascii
[@mel.module "fs"]
external read_file_sync:
(~name: string, [@mel.string] [ | `utf8 | `ascii]) => string =
"readFileSync";
let _ = read_file_sync(~name="xx.txt", `ascii);
Which generates:
var Fs = require("fs");
Fs.readFileSync("xx.txt", "ascii");
This technique can be combined with the mel.as
attribute to modify the strings
produced from the polymorphic variant values. For example:
type document
type style
external document : document = "document"
external get_by_id : document -> string -> Dom.element = "getElementById"
[@@mel.send]
external style : Dom.element -> style = "style" [@@mel.get]
external transition_timing_function :
style ->
([ `ease
| `easeIn [@mel.as "ease-in"]
| `easeOut [@mel.as "ease-out"]
| `easeInOut [@mel.as "ease-in-out"]
| `linear ]
[@mel.string]) ->
unit = "transitionTimingFunction"
[@@mel.set]
let element_style = style (get_by_id document "my-id")
let () = transition_timing_function element_style `easeIn
type document;
type style;
external document: document = "document";
[@mel.send]
external get_by_id: (document, string) => Dom.element = "getElementById";
[@mel.get] external style: Dom.element => style = "style";
[@mel.set]
external transition_timing_function:
(
style,
[@mel.string] [
| `ease
| [@mel.as "ease-in"] `easeIn
| [@mel.as "ease-out"] `easeOut
| [@mel.as "ease-in-out"] `easeInOut
| `linear
]
) =>
unit =
"transitionTimingFunction";
let element_style = style(get_by_id(document, "my-id"));
let () = transition_timing_function(element_style, `easeIn);
This will generate:
var element_style = document.getElementById("my-id").style;
element_style.transitionTimingFunction = "ease-in";
Aside from producing string values, Melange also offers mel.int
to produce
integer values. mel.int
can also be combined with mel.as
:
external test_int_type :
([ `on_closed | `on_open [@mel.as 20] | `in_bin ][@mel.int]) -> int
= "testIntType"
let value = test_int_type `on_open
external test_int_type:
([@mel.int] [ | `on_closed | [@mel.as 20] `on_open | `in_bin]) => int =
"testIntType";
let value = test_int_type(`on_open);
In this example, on_closed
will be encoded as 0, on_open
will be 20 due to
the attribute mel.as
and in_bin
will be 21, because if no mel.as
annotation is provided for a variant tag, the compiler continues assigning
values counting up from the previous one.
This code generates:
var value = testIntType(20);
Using polymorphic variants to bind to event listeners
Polymorphic variants can also be used to wrap event listeners, or any other kind of callback, for example:
type readline
external on :
readline ->
([ `close of unit -> unit | `line of string -> unit ][@mel.string]) ->
readline = "on"
[@@mel.send]
let register rl =
rl |. on (`close (fun event -> ())) |. on (`line (fun line -> Js.log line))
type readline;
[@mel.send]
external on:
(
readline,
[@mel.string] [ | `close(unit => unit) | `line(string => unit)]
) =>
readline =
"on";
let register = rl =>
rl->(on(`close(event => ())))->(on(`line(line => Js.log(line))));
This generates:
function register(rl) {
return rl
.on("close", function($$event) {})
.on("line", function(line) {
console.log(line);
});
}
Constant values as arguments
Sometimes we want to call a JavaScript function and make sure one of the
arguments is always constant. For this, the [@mel.as]
attribute can be
combined with the wildcard pattern _
:
external process_on_exit : (_[@mel.as "exit"]) -> (int -> unit) -> unit
= "process.on"
let () =
process_on_exit (fun exit_code ->
Js.log ("error code: " ^ string_of_int exit_code))
external process_on_exit: ([@mel.as "exit"] _, int => unit) => unit =
"process.on";
let () =
process_on_exit(exit_code =>
Js.log("error code: " ++ string_of_int(exit_code))
);
This generates:
process.on("exit", function (exitCode) {
console.log("error code: " + exitCode.toString());
});
The mel.as "exit"
and the wildcard _
pattern together will tell Melange to
compile the first argument of the JavaScript function to the string "exit"
.
You can also use any JSON literal by passing a quoted string to mel.as
:
mel.as {json|true|json}
or mel.as {json|{"name": "John"}|json}
.
Binding to callbacks
In OCaml, all functions have arity 1. This means that if you define a function like this:
let add x y = x + y
let add = (x, y) => x + y;
Its type will be int -> int -> int
. This means that one can partially apply
add
by calling add 1
, which will return another function expecting the
second argument of the addition. This kind of functions are called "curried"
functions, more information about currying in OCaml can be found in this
chapter of the
"OCaml Programming: Correct + Efficient + Beautiful" book.
This is incompatible with how function calling conventions work in JavaScript,
where all function calls always apply all the arguments. To continue the
example, let’s say we have an add
function implemented in JavaScript, similar
to the one above:
var add = function (a, b) {
return a + b;
};
If we call add(1)
, the function will be totally applied, with b
having
undefined
value. And as JavaScript will try to add 1
with undefined
, we
will get NaN
as a result.
To illustrate this difference and how it affects Melange bindings, let’s say we want to write bindings for a JavaScript function like this:
function map (a, b, f){
var i = Math.min(a.length, b.length);
var c = new Array(i);
for(var j = 0; j < i; ++j){
c[j] = f(a[i],b[i])
}
return c ;
}
A naive external function declaration could be as below:
external map : 'a array -> 'b array -> ('a -> 'b -> 'c) -> 'c array = "map"
external map: (array('a), array('b), ('a, 'b) => 'c) => array('c) = "map";
Unfortunately, this is not completely correct. The issue is in the callback
function, with type 'a -> 'b -> 'c
. This means that map
will expect a
function like add
described above. But as we said, in OCaml, having two
arguments means just to have two functions that take one argument.
Let’s rewrite add
to make the problem a bit more clear:
let add x = let partial y = x + y in partial
let add = x => {
let partial = y => x + y;
partial;
};
This will be compiled to:
function add(x) {
return (function (y) {
return x + y | 0;
});
}
Now if we ever used our external function map
with our add
function by
calling map arr1 arr2 add
it would not work as expected. JavaScript function
application does not work the same as in OCaml, so the function call in the
map
implementation, f(a[i],b[i])
, would be applied over the outer JavaScript
function add
, which only takes one argument x
, and b[i]
would be just
discarded. The value returned from the operation would not be the addition of
the two numbers, but rather the inner anonymous callback.
To solve this mismatch between OCaml and JavaScript functions and their
application, Melange provides a special attribute @u
that can be used to
annotate external functions that need to be "uncurried".
In Reason syntax, this attribute does not need to be
written explicitly, as it is deeply integrated with the Reason parser. To
specify some function type as "uncurried", one just needs to add the dot
character .
to the function type. For example, (. 'a, 'b) => 'c
instead of
('a, 'b) => 'c
.
In the example above:
external map : 'a array -> 'b array -> (('a -> 'b -> 'c)[@u]) -> 'c array
= "map"
external map: (array('a), array('b), (. 'a, 'b) => 'c) => array('c) = "map";
Here ('a -> 'b -> 'c [@u])
(. 'a, 'b) => 'c
will be interpreted as having
arity 2. In general, 'a0 -> 'a1 ... 'aN -> 'b0 [@u]
is the same as 'a0 -> 'a1 ... 'aN -> 'b0
.
'a0, 'a1, ... 'aN => 'b0
is the same as 'a0, 'a1, ... 'aN => 'b0
except the former’s arity is guaranteed to be N while the latter is unknown.
If we try now to call map
using add
:
let add x y = x + y
let _ = map [||] [||] add
let add = (x, y) => x + y;
let _ = map([||], [||], add);
We will get an error:
let _ = map [||] [||] add
^^^
This expression has type int -> int -> int
but an expression was expected of type ('a -> 'b -> 'c) Js.Fn.arity2
To solve this, we add @u
.
in the function definition as well:
let add = fun [@u] x y -> x + y
let add = (. x, y) => x + y;
Annotating function definitions can be quite cumbersome when writing a lot of externals.
To work around the verbosity, Melange offers another attribute called
mel.uncurry
.
Let’s see how we could use it in the previous example. We just need to replace
u
with mel.uncurry
:
external map :
'a array -> 'b array -> (('a -> 'b -> 'c)[@mel.uncurry]) -> 'c array = "map"
external map:
(array('a), array('b), [@mel.uncurry] (('a, 'b) => 'c)) => array('c) =
"map";
Now if we try to call map
with a regular add
function:
let add x y = x + y
let _ = map [||] [||] add
let add = (x, y) => x + y;
let _ = map([||], [||], add);
Everything works fine now, without having to attach any attributes to add
.
The main difference between u
and mel.uncurry
is that the latter only works
with externals. mel.uncurry
is the recommended option to use for bindings,
while u
remains useful for those use cases where performance is crucial and we
want the JavaScript functions generated from OCaml ones to not be applied
partially.
Modeling this
-based Callbacks
Many JavaScript libraries have callbacks which rely on the this
keyword,
for example:
x.onload = function(v) {
console.log(this.response + v)
}
Inside the x.onload
callback, this
would be pointing to x
. It would not be
correct to declare x.onload
of type unit -> unit
. Instead, Melange
introduces a special attribute, mel.this
, which allows to type x
as this:
type x
external x : x = "x"
external set_onload : x -> ((x -> int -> unit)[@mel.this]) -> unit = "onload"
[@@mel.set]
external resp : x -> int = "response" [@@mel.get]
let _ =
set_onload x
begin
fun [@mel.this] o v -> Js.log (resp o + v)
end
type x;
external x: x = "x";
[@mel.set]
external set_onload: (x, [@mel.this] ((x, int) => unit)) => unit = "onload";
[@mel.get] external resp: x => int = "response";
let _ = set_onload(x, [@mel.this] (o, v) => Js.log(resp(o) + v));
Which generates:
x.onload = function (v) {
var o = this;
console.log((o.response + v) | 0);
};
Note that the first argument will be reserved for this
.
Wrapping returned nullable values
JavaScript models null
and undefined
differently, whereas it can be useful
to treat both as 'a option
option('a)
in Melange.
Melange understands the mel.return
attribute in externals to model how
nullable return types should be wrapped at the bindings boundary. An external
value with mel.return
converts the return value to an option
type, avoiding
the need for extra wrapping / unwrapping with functions such as
Js.Nullable.toOption
.
type element
type document
external get_by_id : document -> string -> element option = "getElementById"
[@@mel.send] [@@mel.return nullable]
let test document =
let elem = get_by_id document "header" in
match elem with
| None -> 1
| Some _element -> 2
type element;
type document;
[@mel.send] [@mel.return nullable]
external get_by_id: (document, string) => option(element) = "getElementById";
let test = document => {
let elem = get_by_id(document, "header");
switch (elem) {
| None => 1
| Some(_element) => 2
};
};
Which generates:
function test($$document) {
var elem = $$document.getElementById("header");
if (elem == null) {
return 1;
} else {
return 2;
}
}
The mel.return
attribute takes an attribute payload, as seen with [@@mel.return nullable]
[@mel.return nullable]
above. Currently 4
directives are supported: null_to_opt
, undefined_to_opt
, nullable
and
identity
.
nullable
is encouraged, as it will convert from null
and undefined
to
option
type.
identity
will make sure that compiler will do nothing about the returned
value. It is rarely used, but introduced here for debugging purposes.
Generate getters, setters and constructors
As we saw in a previous section, there are some types
in Melange that compile to values that are not easy to manipulate from
JavaScript. To facilitate the communication from JavaScript code with values of
these types, Melange includes an attribute deriving
that helps generating
conversion functions, as well as functions to create values from these types. In
particular, for variants and polymorphic variants.
Additionally, deriving
can be used with record types, to generate setters and
getters as well as creation functions.
Variants
Creating values
Use @deriving accessors
on a variant type to create constructor values for
each branch.
type action =
| Click
| Submit of string
| Cancel
[@@deriving accessors]
[@deriving accessors]
type action =
| Click
| Submit(string)
| Cancel;
Melange will generate one let
definition for each variant tag, implemented as
follows:
- For variant tags with payloads, it will be a function that takes the payload value as a parameter.
- For variant tags without payloads, it will be a constant with the runtime value of the tag.
Given the action
type definition above, annotated with deriving
, Melange
will generate something similar to the following code:
type action =
| Click
| Submit of string
| Cancel
let click = (Click : action)
let submit param = (Submit param : action)
let cancel = (Cancel : action)
type action =
| Click
| Submit(string)
| Cancel;
let click: action = Click;
let submit = (param): action => Submit(param);
let cancel: action = Cancel;
Which will result in the following JavaScript code after compilation:
function submit(param_0) {
return /* Submit */{
_0: param_0
};
}
var click = /* Click */0;
var cancel = /* Cancel */1;
Note the generated definitions are lower-cased, and they can be safely used from
JavaScript code. For example, if the above JavaScript generated code was located
in a generators.js
file, the definitions can be used like this:
const generators = require('./generators.js');
const hello = generators.submit("Hello");
const click = generators.click;
Conversion functions
Use @deriving jsConverter
on a variant type to create converter functions that
allow to transform back and forth between JavaScript integers and Melange
variant values.
There are a few differences with @deriving accessors
:
jsConverter
works with themel.as
attribute, whileaccessors
does notjsConverter
does not support variant tags with payload, whileaccessors
doesjsConverter
generates functions to transform values back and forth, whileaccessors
generates functions to create values
Let’s see a version of the previous example, adapted to work with jsConverter
given the constraints above:
type action =
| Click
| Submit [@mel.as 3]
| Cancel
[@@deriving jsConverter]
[@deriving jsConverter]
type action =
| Click
| [@mel.as 3] Submit
| Cancel;
This will generate a couple of functions with the following types:
val actionToJs : action -> int
val actionFromJs : int -> action option
external actionToJs: action => int = ;
external actionFromJs: int => option(action) = ;
actionToJs
returns integers from values of action
type. It will start with 0
for Click
, 3 for Submit
(because it was annotated with mel.as
), and then 4
for Cancel
, in the same way that we described when using mel.int
with
polymorphic variants.
actionFromJs
returns a value of type option
, because not every integer can
be converted into a variant tag of the action
type.
Hide runtime types
For extra type safety, we can hide the runtime representation of variants
(int
) from the generated functions, by using jsConverter { newType }
payload
with @deriving
:
type action =
| Click
| Submit [@mel.as 3]
| Cancel
[@@deriving jsConverter { newType }]
[@deriving jsConverter({newType: newType})]
type action =
| Click
| [@mel.as 3] Submit
| Cancel;
This feature relies on abstract types to hide the JavaScript runtime representation. It will generate functions with the following types:
val actionToJs : action -> abs_action
val actionFromJs : abs_action -> action
external actionToJs: action => abs_action = ;
external actionFromJs: abs_action => action = ;
In the case of actionFromJs
, the return value, unlike the previous case, is
not an option type. This is an example of "correct by construction": the only
way to create an abs_action
is by calling the actionToJs
function.
Polymorphic variants
The @deriving jsConverter
attribute is applicable to polymorphic variants as
well.
NOTE: Similarly to variants, the
@deriving jsConverter
attribute cannot be used when the polymorphic variant tags have payloads. Refer to the section on runtime representation to learn more about how polymorphic variants are represented in JavaScript.
Let’s see an example:
type action =
[ `Click
| `Submit [@mel.as "submit"]
| `Cancel
]
[@@deriving jsConverter]
[@deriving jsConverter]
type action = [ | `Click | [@mel.as "submit"] `Submit | `Cancel];
Akin to the variant example, the following two functions will be generated:
val actionToJs : action -> string
val actionFromJs : string -> action option
external actionToJs: action => string = ;
external actionFromJs: string => option(action) = ;
The jsConverter { newType }
payload can also be used with polymorphic
variants.
Records
Accessing fields
Use @deriving accessors
on a record type to create accessor functions for its
record field names.
type pet = { name : string } [@@deriving accessors]
let pets = [| { name = "Brutus" }; { name = "Mochi" } |]
let () = pets |. Belt.Array.map name |. Js.Array2.joinWith "&" |. Js.log
[@deriving accessors]
type pet = {name: string};
let pets = [|{name: "Brutus"}, {name: "Mochi"}|];
let () = pets->(Belt.Array.map(name))->(Js.Array2.joinWith("&"))->Js.log;
Melange will generate a function for each field defined in the record. In this
case, a function name
that allows to get that field from any record of type
pet
:
let name (param : pet) = param.name
let name = (param: pet) => param.name;
Considering all the above, the produced JavaScript will be:
function name(param) {
return param.name;
}
var pets = [
{
name: "Brutus"
},
{
name: "Mochi"
}
];
console.log(Belt_Array.map(pets, name).join("&"));
Convert records into abstract types
When binding to JavaScript objects, it is generally recommended to use records since Melange precisely uses objects as their runtime representation. This approach was discussed in the section about binding to JavaScript objects.
But there’s a specific case where records may not be enough: when we want to emit a JavaScript object where some of the keys might be present or absent.
For instance, consider the following record:
type person = {
name : string;
age : int option;
}
type person = {
name: string,
age: option(int),
};
An example of this use-case would be expecting { name = "John"; age = None }
to generate a JavaScript such as {name: "Carl"}
, where the age
key doesn’t
appear.
The @deriving abstract
attribute exists to solve this problem. When present in
a record type, @deriving abstract
makes the record definition abstract and
generates the following functions instead:
- A constructor function for creating values of the type
- Getters and setters for accessing the record fields
@deriving abstract
effectively models a record-shaped JavaScript object
exclusively through a set of (generated) functions derived from attribute
annotations on the OCaml type definition.
Let’s see an example. Considering this Melange code:
type person = {
name : string;
age : int option; [@optional]
}
[@@deriving abstract]
[@deriving abstract]
type person = {
name: string,
[@optional]
age: option(int),
};
Melange will make the person
type abstract and generate constructor, getter
and setter functions. In our example, the OCaml signature would look like this
after preprocessing:
type person
val person : name:string -> ?age:int -> unit -> person
val nameGet : person -> string
val ageGet : person -> int option
type person;
external person: (~name: string, ~age: int=?, unit) => person = ;
external nameGet: person => string = ;
external ageGet: person => option(int) = ;
The person
function can be used to create values of person
. It is the only
possible way to create values of this type, since Melange makes it abstract.
Using literals like { name = "Alice"; age = None }
directly doesn’t type
check.
Here is an example of how we can use it:
let alice = person ~name:"Alice" ~age:20 ()
let bob = person ~name:"Bob" ()
let alice = person(~name="Alice", ~age=20, ());
let bob = person(~name="Bob", ());
This will generate the following JavaScript code. Note how there is no JavaScript runtime overhead:
var alice = {
name: "Alice",
age: 20
};
var bob = {
name: "Bob"
};
The person
function uses labeled arguments to represent record fields. Because
there is an optional argument age
, it takes a last argument of type unit
.
This non-labeled argument allows to omit the optional argument on function
application. Further details about optional labeled arguments can be found in
the corresponding section of the OCaml
manual.
The functions nameGet
and ageGet
are accessors for each record field:
let twenty = ageGet alice
let bob = nameGet bob
let twenty = ageGet(alice);
let bob = nameGet(bob);
This generates:
var twenty = alice.age;
var bob = bob.name;
The functions are named by appending Get
to the field names of the record to
prevent potential clashes with other values within the module. If shorter names
are preferred for the getter functions, there is an alternate { abstract =
light }
payload that can be passed to deriving
:
type person = {
name : string;
age : int;
}
[@@deriving abstract { light }]
let alice = person ~name:"Alice" ~age:20
let aliceName = name alice
[@deriving abstract({light: light})]
type person = {
name: string,
age: int,
};
let alice = person(~name="Alice", ~age=20);
let aliceName = name(alice);
Which generates:
var alice = {
name: "Alice",
age: 20
};
var aliceName = alice.name;
In this example, the getter functions share the same names as the object fields.
Another distinction from the previous example is that the person
constructor
function no longer requires the final unit
argument since we have excluded the
optional field in this case.
NOTE: The
mel.as
attribute can still be applied to record fields when the record type is annotated withderiving
, allowing for the renaming of fields in the resulting JavaScript objects, as demonstrated in the section about binding to objects with static shape. However, the option to pass indices to themel.as
decorator (like[@mel.as "0"]
) to change the runtime representation to an array is not available when usingderiving
.
Compatibility with OCaml features
The @deriving abstract
attribute and its lightweight variant can be used with
mutable
fields and
private types, which are
features inherited by Melange from OCaml.
When the record type has mutable fields, Melange will generate setter functions for them. For example:
type person = {
name : string;
mutable age : int;
}
[@@deriving abstract]
let alice = person ~name:"Alice" ~age:20
let () = ageSet alice 21
[@deriving abstract]
type person = {
name: string,
mutable age: int,
};
let alice = person(~name="Alice", ~age=20);
let () = ageSet(alice, 21);
This will generate:
var alice = {
name: "Alice",
age: 20
};
alice.age = 21;
If the mutable
keyword is omitted from the interface file, Melange will not
include the setter function in the module signature, preventing other modules
from mutating any values from the type.
Private types can be used to prevent Melange from creating the constructor
function. For example, if we define person
type as private:
type person = private {
name : string;
age : int;
}
[@@deriving abstract]
[@deriving abstract]
type person =
pri {
name: string,
age: int,
};
The accessors nameGet
and ageGet
will still be generated, but not the
constructor person
. This is useful when binding to JavaScript objects while
preventing any Melange code from creating values of such type.
Use Melange code from JavaScript
As mentioned in the build system section, Melange allows to produce both CommonJS and ES6 modules. In both cases, using Melange-generated JavaScript code from any hand-written JavaScript file works as expected.
The following definition:
let print name = "Hello" ^ name
let print = name => "Hello" ++ name;
Will generate this JavaScript code, when using CommonJS (the default):
function print(name) {
return "Hello" + name;
}
exports.print = print;
When using ES6 (through the (module_systems es6)
field in melange.emit
) this
code will be generated:
function print(name) {
return "Hello" + name;
}
export {
print ,
}
So one can use either require
or import
(depending on the module system of
choice) to import the print
value in a JavaScript file.
Default ES6 values
One special case occur when working with JavaScript imports in ES6 modules that look like this:
import ten from 'numbers.js';
This import expects numbers.js
to have a default export, like:
export default ten = 10;
To emulate this kind of exports from Melange, one just needs to define a
default
value.
For example, in a file named numbers.ml
numbers.re
:
let default = 10
let default = 10;
That way, Melange will set the value on the default
export so it can be
consumed as default import on the JavaScript side.