Advanced JavaScript interoperability
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
let actionToJs: action => int;
let 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
let actionToJs: action => abs_action;
let 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
let actionToJs: action => string;
let 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.Array.join ~sep:"&" |. Js.log
[@deriving accessors]
type pet = {name: string};
let pets = [|{name: "Brutus"}, {name: "Mochi"}|];
let () = pets->(Belt.Array.map(name))->(Js.Array.join(~sep="&"))->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("&"));
Generate JavaScript objects with optional properties
In some occasions, we might want to emit a JavaScript object where some of the keys can be conditionally 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 object such as {name: "Carl"}
, where the age
key doesn’t appear.
The @deriving jsProperties
attribute exists to solve this problem. When present in a record type, @deriving jsProperties
generates a constructor function for creating values of the type, where the fields marked with [@mel.optional]
will be fully removed from the generated JavaScript object when their value is None
.
Let’s see an example. Considering this Melange code:
type person = {
name : string;
age : int option; [@mel.optional]
}
[@@deriving jsProperties]
[@deriving jsProperties]
type person = {
name: string,
[@mel.optional]
age: option(int),
};
Melange will generate a constructor to create values of this type. In our example, the OCaml signature would look like this after preprocessing:
type person
external person : name:string -> ?age:int -> unit -> person = "person"
type person;
external person: (~name: string, ~age: int=?, unit) => person = "person";
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.
Generating getters and setters
In case we need both getters and setters for a record, we can use deriving getSet
to have them generated for free.
If we take a record like this:
type person = {
name : string;
age : int option; [@mel.optional]
}
[@@deriving getSet]
[@deriving getSet]
type person = {
name: string,
[@mel.optional]
age: option(int),
};
The deriving
attribute can combine multiple derivers, for example we can combine jsProperties
with getSet
:
type person = {
name : string;
age : int option; [@mel.optional]
}
[@@deriving jsProperties, getSet]
[@deriving (jsProperties, getSet)]
type person = {
name: string,
[@mel.optional]
age: option(int),
};
When using getSet
, Melange will create functions nameGet
and ageGet
, as 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 getSet { light }
getSet({light: light})
payload that can be passed to deriving
:
type person = {
name : string;
age : int;
}
[@@deriving jsProperties, getSet { light }]
let alice = person ~name:"Alice" ~age:20
let aliceName = name alice
[@deriving (jsProperties, getSet({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 getSet
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 jsProperties, getSet]
let alice = person ~name:"Alice" ~age:20
let () = ageSet alice 21
[@deriving (jsProperties, getSet)]
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 getSet]
[@deriving getSet]
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.