From d0247a277d4e9d0dd3b61347993e12be19955bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Ch=C3=A1varri?= Date: Wed, 16 Oct 2024 21:07:11 +0200 Subject: [PATCH] Bindings cookbook (#120) Co-authored-by: schinns --- docs/communicate-with-javascript.md | 642 +++++++++++++++++++++++++- scripts/communicate-with-javascript.t | 280 +++++++++++ 2 files changed, 916 insertions(+), 6 deletions(-) diff --git a/docs/communicate-with-javascript.md b/docs/communicate-with-javascript.md index 550cdedd1..37bb6ea03 100644 --- a/docs/communicate-with-javascript.md +++ b/docs/communicate-with-javascript.md @@ -1,11 +1,15 @@ # 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. +features to communicate with foreign JavaScript code. To learn about these +techniques (generically known as "bindings"), 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. + +If you already have in mind some JavaScript that you want to write, check the +last section ["Bindings cookbook"](#bindings-cookbook) to see how the same code +can be written with Melange. ## Language concepts @@ -961,6 +965,12 @@ let () = }; ``` +`[%mel.external id]` makes `id` available as a value of type 'a Option.tOption.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 @@ -1158,7 +1168,8 @@ var value = [ #### 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 +produce JavaScript objects. This type is 'a +Js.tJs.t('a), where `'a` is an [OCaml object](https://v2.ocaml.org/manual/objectexamples.html). The advantage of objects versus records is that no type declaration is needed in @@ -3190,3 +3201,622 @@ 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. + +## Bindings cookbook + +### Globals + +#### `window`: global variable + +```ocaml +external window : Dom.window = "window" +``` +```reasonml +external window: Dom.window = "window"; +``` + +See the [Using global functions or values](#using-global-functions-or-values) +section for more information. + +#### `window?`: does global variable exist + +```ocaml +let _ = match [%mel.external window] with +| Some _ -> "window exists" +| None -> "window does not exist" +``` +```reasonml +let _ = + switch ([%mel.external window]) { + | Some(_) => "window exists" + | None => "window does not exist" + }; +``` + +See the [Detect global variables](#detect-global-variables) section for more +information. + +#### `Math.PI`: variable in global module + +```ocaml +external pi : float = "PI" [@@mel.scope "Math"] +``` +```reasonml +[@mel.scope "Math"] external pi: float = "PI"; +``` + +See the [Binding to properties inside a module or +global](#binding-to-properties-inside-a-module-or-global) section for more +information. + +#### `console.log`: function in global module + +```ocaml +external log : 'a -> unit = "log" [@@mel.scope "console"] +``` +```reasonml +[@mel.scope "console"] external log: 'a => unit = "log"; +``` + +See the [Binding to properties inside a module or +global](#binding-to-properties-inside-a-module-or-global) section for more +information. + +### Modules + +#### `const path = require('path'); path.join('a', 'b')`: function in CommonJS/ES6 module + +```ocaml +external join : string -> string -> string = "join" [@@mel.module "path"] +let dir = join "a" "b" +``` +```reasonml +[@mel.module "path"] external join: (string, string) => string = "join"; +let dir = join("a", "b"); +``` + +See the [Using functions from other JavaScript +modules](#using-functions-from-other-javascript-modules) section for more +information. + +#### `const foo = require('foo'); foo(1)`: import entire module as a value + +```ocaml +external foo : int -> unit = "foo" [@@mel.module] +let () = foo 1 +``` +```reasonml +[@mel.module] external foo: int => unit = "foo"; +let () = foo(1); +``` + +See the [Using functions from other JavaScript +modules](#using-functions-from-other-javascript-modules) section for more +information. + +#### `import foo from 'foo'; foo(1)`: import ES6 module default export + +```ocaml +external foo : int -> unit = "default" [@@mel.module "foo"] +let () = foo 1 +``` +```reasonml +[@mel.module "foo"] external foo: int => unit = "default"; +let () = foo(1); +``` + +See the [Using functions from other JavaScript +modules](#using-functions-from-other-javascript-modules) section for more +information. + +#### `const foo = require('foo'); foo.bar.baz()`: function scoped inside an object in a module + +```ocaml +module Foo = struct + module Bar = struct + external baz : unit -> unit = "baz" [@@mel.module "foo"] [@@mel.scope "bar"] + end +end + +let () = Foo.Bar.baz () +``` +```reasonml +module Foo = { + module Bar = { + [@mel.module "foo"] [@mel.scope "bar"] external baz: unit => unit = "baz"; + }; +}; + +let () = Foo.Bar.baz(); +``` + +It is not necessary to nest the binding inside OCaml modules, but mirroring the +structure of the JavaScript module layout makes the binding more discoverable. + +See the [Binding to properties inside a module or +global](#binding-to-properties-inside-a-module-or-global) section for more +information. + +### Functions + +#### `const dir = path.join('a', 'b', ...)`: function with rest args + +```ocaml +external join : string array -> string = "join" [@@mel.module "path"] [@@mel.variadic] +let dir = join [| "a"; "b" |] +``` +```reasonml +[@mel.module "path"] [@mel.variadic] +external join: array(string) => string = "join"; +let dir = join([|"a", "b"|]); +``` + +See the [Variadic function arguments](#variadic-function-arguments) section for +more information. + +#### `const nums = range(start, stop, step)`: call a function with named arguments for readability + +```ocaml +external range : start:int -> stop:int -> step:int -> int array = "range" +let nums = range ~start:1 ~stop:10 ~step:2 +``` +```reasonml +external range: (~start: int, ~stop: int, ~step: int) => array(int) = "range"; +let nums = range(~start=1, ~stop=10, ~step=2); +``` + +#### `foo('hello'); foo(true)`: overloaded function + +```ocaml +external fooString : string -> unit = "foo" +external fooBool : bool -> unit = "foo" + +let () = fooString "" +let () = fooBool true +``` +```reasonml +external fooString: string => unit = "foo"; +external fooBool: bool => unit = "foo"; + +let () = fooString(""); +let () = fooBool(true); +``` + +Melange allows specifying the name on the OCaml side and the name on the +JavaScript side (in quotes) separately, so it's possible to bind multiple times +to the same function with different names and signatures. This allows binding to +complex JavaScript functions with polymorphic behaviour. + +#### `const nums = range(start, stop, [step])`: optional final argument(s) + +```ocaml +external range : start:int -> stop:int -> ?step:int -> unit -> int array + = "range" + +let nums = range ~start:1 ~stop:10 () +``` +```reasonml +external range: (~start: int, ~stop: int, ~step: int=?, unit) => array(int) = + "range"; + +let nums = range(~start=1, ~stop=10, ()); +``` + +When an OCaml function takes an optional parameter, it needs a positional +parameter at the end of the parameter list to help the compiler understand when +function application is finished and when the function can actually execute. This +might seem cumbersome, but it is necessary in order to have out-of-the-box curried +parameters, named parameters, and optional parameters available in the language. + +#### `mkdir('src/main', {recursive: true})`: options object argument + +```ocaml +type mkdirOptions + +external mkdirOptions : ?recursive:bool -> unit -> mkdirOptions = "" [@@mel.obj] +external mkdir : string -> ?options:mkdirOptions -> unit -> unit = "mkdir" + +let () = mkdir "src" () +let () = mkdir "src/main" ~options:(mkdirOptions ~recursive:true ()) () +``` +```reasonml +type mkdirOptions; + +[@mel.obj] external mkdirOptions: (~recursive: bool=?, unit) => mkdirOptions; +external mkdir: (string, ~options: mkdirOptions=?, unit) => unit = "mkdir"; + +let () = mkdir("src", ()); +let () = mkdir("src/main", ~options=mkdirOptions(~recursive=true, ()), ()); +``` + +See the [Objects with static shape (record-like): Using external +functions](#using-external-functions) section for more information. + +#### `forEach(start, stop, item => console.log(item))`: model a callback + +```ocaml +external forEach : + start:int -> stop:int -> ((int -> unit)[@mel.uncurry]) -> unit = "forEach" + +let () = forEach ~start:1 ~stop:10 Js.log +``` +```reasonml +external forEach: + (~start: int, ~stop: int, [@mel.uncurry] (int => unit)) => unit = + "forEach"; + +let () = forEach(~start=1, ~stop=10, Js.log); +``` + +When binding to functions with callbacks, you'll want to ensure that the +callbacks are uncurried. `[@mel.uncurry]` is the recommended way of doing that. +However, in some circumstances you may be forced to use the static uncurried +function syntax. See the [Binding to callbacks](#binding-to-callbacks) section +for more information. + +### Objects + +#### `const person = {id: 1, name: 'Alice'}`: create an object + +For quick creation of objects (e.g. prototyping), one can create a `Js.t` object +literal directly: + +```ocaml +let person = [%mel.obj { id = 1; name = "Alice" }] +``` +```reasonml +let person = {"id": 1, "name": "Alice"}; +``` + +See the [Using `Js.t` objects](#using-jst-objects) section for more information. + +Alternatively, for greater type accuracy, one can create a record type and a +value: + +```ocaml +type person = { id : int; name : string } +let person = { id = 1; name = "Alice" } +``` +```reasonml +type person = { + id: int, + name: string, +}; +let person = {id: 1, name: "Alice"}; +``` + +See the [Using OCaml records](#using-ocaml-records) section for more +information. + +#### `person.name`: get a prop + + +```ocaml +let name = person##name +``` +```reasonml +let name = person##name; +``` + +Alternatively, if `person` value is of record type as mentioned in the section +above: + + +```ocaml +let name = person.name +``` +```reasonml +let name = person.name; +``` + +#### `person.id = 0`: set a prop + + +```ocaml +external set_id : person -> int -> unit = "id" [@@mel.set] + +let () = set_id person 0 +``` +```reasonml +[@mel.set] external set_id: (person, int) => unit = "id"; + +let () = set_id(person, 0); +``` + +#### `const {id, name} = person`: object with destructuring + +```ocaml +type person = { id : int; name : string } + +let person = { id = 1; name = "Alice" } +let { id; name } = person +``` +```reasonml +type person = { + id: int, + name: string, +}; + +let person = {id: 1, name: "Alice"}; +let {id, name} = person; +``` + +### Classes and OOP + +In Melange it is idiomatic to bind to class properties and methods as functions +which take the instance as just a normal function argument. So e.g., instead of + +```javascript +const foo = new Foo(); +foo.bar(); +``` + +You will write: + + +```ocaml +let foo = Foo.make () +let () = Foo.bar foo +``` +```reasonml +let foo = Foo.make(); +let () = Foo.bar(foo); +``` + +Note that many of the techniques shown in the [Functions](#functions) section +are applicable to the instance members shown below. + +#### `const foo = new Foo()`: call a class constructor + +```ocaml +module Foo = struct + type t + external make : unit -> t = "Foo" [@@mel.new] +end + +let foo = Foo.make () +``` +```reasonml +module Foo = { + type t; + [@mel.new] external make: unit => t = "Foo"; +}; + +let foo = Foo.make(); +``` + +Note the abstract type `t`, which we have revisited already in [its +corresponding](#abstract-types) section. + +A Melange function binding doesn't have the context that it's binding to a +JavaScript class like `Foo`, so you will want to explicitly put it inside a +corresponding module `Foo` to denote the class it belongs to. In other words, +model JavaScript classes as OCaml modules. + +See the [JavaScript classes](#javascript-classes) section for more information. + +#### `const bar = foo.bar`: get an instance property + +```ocaml +module Foo = struct + type t + external make : unit -> t = "Foo" [@@mel.new] + external bar : t -> int = "bar" [@@mel.get] +end + +let foo = Foo.make () +let bar = Foo.bar foo +``` +```reasonml +module Foo = { + type t; + [@mel.new] external make: unit => t = "Foo"; + [@mel.get] external bar: t => int = "bar"; +}; + +let foo = Foo.make(); +let bar = Foo.bar(foo); +``` + +See the [Binding to object properties](#bind-to-object-properties) section for +more information. + +#### `foo.bar = 1`: set an instance property + +```ocaml +module Foo = struct + type t + external make : unit -> t = "Foo" [@@mel.new] + external setBar : t -> int -> unit = "bar" [@@mel.set] +end + +let foo = Foo.make () +let () = Foo.setBar foo 1 +``` +```reasonml +module Foo = { + type t; + [@mel.new] external make: unit => t = "Foo"; + [@mel.set] external setBar: (t, int) => unit = "bar"; +}; + +let foo = Foo.make(); +let () = Foo.setBar(foo, 1); +``` + +#### `foo.meth()`: call a nullary instance method + +```ocaml +module Foo = struct + type t + + external make : unit -> t = "Foo" [@@mel.new] + external meth : t -> unit = "meth" [@@mel.send] +end + +let foo = Foo.make () +let () = Foo.meth foo +``` +```reasonml +module Foo = { + type t; + + [@mel.new] external make: unit => t = "Foo"; + [@mel.send] external meth: t => unit = "meth"; +}; + +let foo = Foo.make(); +let () = Foo.meth(foo); +``` + +See the [Calling an object method](#calling-an-object-method) section for more +information. + +#### `const newStr = str.replace(substr, newSubstr)`: non-mutating instance method + +```ocaml +external replace : substr:string -> newSubstr:string -> string = "replace" +[@@mel.send.pipe: string] + +let str = "goodbye world" +let substr = "goodbye" +let newSubstr = "hello" +let newStr = replace ~substr ~newSubstr str +``` +```reasonml +[@mel.send.pipe: string] +external replace: (~substr: string, ~newSubstr: string) => string = "replace"; + +let str = "goodbye world"; +let substr = "goodbye"; +let newSubstr = "hello"; +let newStr = replace(~substr, ~newSubstr, str); +``` + +`mel.send.pipe` injects a parameter of the given type (in this case `string`) as +the final positional parameter of the binding. In other words, it creates the +binding with the real signature substr:string -\> +newSubstr:string -\> string -\> string(~substr: string, ~newSubstr: string, string) =\> +string. This is handy for non-mutating functions as they traditionally +take the instance as the final parameter. + +It is not strictly necessary to use named arguments in this binding, but it +helps readability with multiple arguments, especially if some have the same +type. + +Also note that it is not strictly need to use `mel.send.pipe`, one can use +`mel.send` everywhere. + +See the [Calling an object method](#calling-an-object-method) section for more +information. + +#### `arr.sort(compareFunction)`: mutating instance method + + +```ocaml +external sort : 'a array -> (('a -> 'a -> int)[@mel.uncurry]) -> 'a array + = "sort" +[@@mel.send] + +let _ = sort arr compare +``` +```reasonml +[@mel.send] +external sort: (array('a), [@mel.uncurry] (('a, 'a) => int)) => array('a) = + "sort"; + +let _ = sort(arr, compare); +``` + +For a mutating method, it's traditional to pass the instance argument first. + +Note: `compare` is a function provided by the standard library, which fits the +defined interface of JavaScript's comparator function. + +### Null and undefined + +#### `foo.bar === undefined`: check for undefined + +```ocaml +module Foo = struct + type t + external bar : t -> int option = "bar" [@@mel.get] +end + +external foo : Foo.t = "foo" + +let _result = match Foo.bar foo with Some _ -> 1 | None -> 0 +``` +```reasonml +module Foo = { + type t; + [@mel.get] external bar: t => option(int) = "bar"; +}; + +external foo: Foo.t = "foo"; + +let _result = + switch (Foo.bar(foo)) { + | Some(_) => 1 + | None => 0 + }; +``` + +If you know some value may be `undefined` (but not `null`, see next section), +and if you know its type is monomorphic (i.e. not generic), then you can model +it directly as an `Option.t` type. + +See the [Non-shared data types](#non-shared-data-types) section for more +information. + +#### `foo.bar == null`: check for null or undefined + +```ocaml +module Foo = struct + type t + external bar : t -> t option = "bar" [@@mel.get] [@@mel.return nullable] +end + +external foo : Foo.t = "foo" + +let _result = match Foo.bar foo with Some _ -> 1 | None -> 0 +``` +```reasonml +module Foo = { + type t; + [@mel.get] [@mel.return nullable] external bar: t => option(t) = "bar"; +}; + +external foo: Foo.t = "foo"; + +let _result = + switch (Foo.bar(foo)) { + | Some(_) => 1 + | None => 0 + }; +``` + +If you know the value is 'nullable' (i.e. could be `null` or `undefined`), or if +the value could be polymorphic, then `mel.return nullable` is appropriate to +use. + +Note that this attribute requires the return type of the binding to be an +`option` type as well. + +See the [Wrapping returned nullable values](#wrapping-returned-nullable-values) +section for more information. diff --git a/scripts/communicate-with-javascript.t b/scripts/communicate-with-javascript.t index 6fd031112..89db13df1 100644 --- a/scripts/communicate-with-javascript.t +++ b/scripts/communicate-with-javascript.t @@ -14,6 +14,286 @@ file. To update the tests, run `dune build @extract-code-blocks`. > (preprocess (pps melange.ppx))) > EOF + $ cat > input.ml <<\EOF + > module Foo = struct + > type t + > external bar : t -> t option = "bar" [@@mel.get] [@@mel.return nullable] + > end + > + > external foo : Foo.t = "foo" + > + > let _result = match Foo.bar foo with Some _ -> 1 | None -> 0 + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > module Foo = struct + > type t + > external bar : t -> int option = "bar" [@@mel.get] + > end + > + > external foo : Foo.t = "foo" + > + > let _result = match Foo.bar foo with Some _ -> 1 | None -> 0 + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > let arr = [|2|] + > external sort : 'a array -> (('a -> 'a -> int)[@mel.uncurry]) -> 'a array + > = "sort" + > [@@mel.send] + > + > let _ = sort arr compare + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > external replace : substr:string -> newSubstr:string -> string = "replace" + > [@@mel.send.pipe: string] + > + > let str = "goodbye world" + > let substr = "goodbye" + > let newSubstr = "hello" + > let newStr = replace ~substr ~newSubstr str + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > module Foo = struct + > type t + > + > external make : unit -> t = "Foo" [@@mel.new] + > external meth : t -> unit = "meth" [@@mel.send] + > end + > + > let foo = Foo.make () + > let () = Foo.meth foo + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > module Foo = struct + > type t + > external make : unit -> t = "Foo" [@@mel.new] + > external setBar : t -> int -> unit = "bar" [@@mel.set] + > end + > + > let foo = Foo.make () + > let () = Foo.setBar foo 1 + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > module Foo = struct + > type t + > external make : unit -> t = "Foo" [@@mel.new] + > external bar : t -> int = "bar" [@@mel.get] + > end + > + > let foo = Foo.make () + > let bar = Foo.bar foo + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > module Foo = struct + > type t + > external make : unit -> t = "Foo" [@@mel.new] + > end + > + > let foo = Foo.make () + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > + > module Foo = struct + > type t + > external make : unit -> t = "Foo" [@@mel.new] + > external bar : t -> unit = "bar" [@@mel.get] + > end + > + > let foo = Foo.make () + > let () = Foo.bar foo + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > type person = { id : int; name : string } + > + > let person = { id = 1; name = "Alice" } + > let { id; name } = person + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > + > type person = { id : int; name : string } + > let person = { id = 1; name = "Alice" } + > + > external set_id : person -> int -> unit = "id" [@@mel.set] + > + > let () = set_id person 0 + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > + > type person = { id : int; name : string } + > let person = { id = 1; name = "Alice" } + > + > let name = person.name + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > + > let person = [%mel.obj { name = "john"; age = 99 }] + > + > let name = person##name + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > type person = { id : int; name : string } + > let person = { id = 1; name = "Alice" } + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > let person = [%mel.obj { id = 1; name = "Alice" }] + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > external forEach : + > start:int -> stop:int -> ((int -> unit)[@mel.uncurry]) -> unit = "forEach" + > + > let () = forEach ~start:1 ~stop:10 Js.log + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > type mkdirOptions + > + > external mkdirOptions : ?recursive:bool -> unit -> mkdirOptions = "" [@@mel.obj] + > external mkdir : string -> ?options:mkdirOptions -> unit -> unit = "mkdir" + > + > let () = mkdir "src" () + > let () = mkdir "src/main" ~options:(mkdirOptions ~recursive:true ()) () + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > external range : start:int -> stop:int -> ?step:int -> unit -> int array + > = "range" + > + > let nums = range ~start:1 ~stop:10 () + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > external fooString : string -> unit = "foo" + > external fooBool : bool -> unit = "foo" + > + > let () = fooString "" + > let () = fooBool true + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > external range : start:int -> stop:int -> step:int -> int array = "range" + > let nums = range ~start:1 ~stop:10 ~step:2 + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > external join : string array -> string = "join" [@@mel.module "path"] [@@mel.variadic] + > let dir = join [| "a"; "b" |] + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > module Foo = struct + > module Bar = struct + > external baz : unit -> unit = "baz" [@@mel.module "foo"] [@@mel.scope "bar"] + > end + > end + > + > let () = Foo.Bar.baz () + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > external foo : int -> unit = "default" [@@mel.module "foo"] + > let () = foo 1 + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > external foo : int -> unit = "foo" [@@mel.module] + > let () = foo 1 + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > external join : string -> string -> string = "join" [@@mel.module "path"] + > let dir = join "a" "b" + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > external log : 'a -> unit = "log" [@@mel.scope "console"] + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > external pi : float = "PI" [@@mel.scope "Math"] + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > let _ = match [%mel.external window] with + > | Some _ -> "window exists" + > | None -> "window does not exist" + > EOF + + $ dune build @melange + + $ cat > input.ml <<\EOF + > external window : Dom.window = "window" + > EOF + + $ dune build @melange + $ cat > input.ml <<\EOF > let default = 10 > EOF