Melange 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
: translates optional fields in a record to omitted properties in the generated JavaScript object (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 "xxx"]
)the extension name without square brackets (%mel.raw "xxx"
) 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")
};
[%mel.external id]
makes id
available as a value of type 'a Option.t
Option.t('a)
, meaning its wrapped value is compatible with any type. If you use the value, it is recommended to annotate it into a known type first to avoid runtime issues.
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");
}