• Stars
    star
    115
  • Rank 305,916 (Top 7 %)
  • Language
  • License
    MIT License
  • Created over 4 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Task-oriented guide to writing JavaScript bindings for BuckleScript

BuckleScript Bindings Cookbook

WRITING BuckleScript bindings can be somewhere between an art and a science, taking some learning investment into both the JavaScript and OCaml/Reason type systems to get a proper feel for it.

This cookbook aims to be a quickstart, task-focused guide for writing bindings. The idea is that you have in mind some JavaScript that you want to write, and look up the binding that should (hopefully) produce that output JavaScript.

Along the way, I will try to introduce standard types for modelling various JavaScript data.

Contents

Globals

window // global variable

[@bs.val] external window: Dom.window = "window";

Ref https://reasonml.org/docs/reason-compiler/latest/bind-to-global-values

window? // does global variable exist

switch ([%external window]) {
| Some(_) => "window exists"
| None => "window does not exist"
};

[%external NAME] makes NAME available as an value of type option('a), meaning its wrapped value is compatible with any type. I recommend that, if you use the value, to cast it safely into a known type first.

Ref https://reasonml.org/docs/reason-compiler/latest/embed-raw-javascript#detect-global-variables

Math.PI // variable in global module

[@bs.val] [@bs.scope "Math"] external pi: float = "PI";

Ref https://reasonml.org/docs/reason-compiler/latest/bind-to-global-values#global-modules

console.log // function in global module

[@bs.val] [@bs.scope "console"] external log: 'a => unit = "log";

Note that in JavaScript, console.log()'s return value is undefined, which we can model with the unit type.

Modules

const path = require('path'); path.join('a', 'b') // function in CJS/ES module

[@bs.module "path"] external join: (string, string) => string = "join";
let dir = join("a", "b");

Ref: https://reasonml.org/docs/reason-compiler/latest/import-export#import

const foo = require('foo'); foo(1) // import entire module as a value

[@bs.module] external foo: int => unit = "foo";
let () = foo(1);

Ref: https://reasonml.org/docs/reason-compiler/latest/import-export#import-a-default-value

import foo from 'foo'; foo(1) // import ES6 module default export

[@bs.module "foo"] external foo: int => unit = "default";
let () = foo(1);

Ref: https://reasonml.org/docs/reason-compiler/latest/import-export#import-an-es6-default-value

const foo = require('foo'); foo.bar.baz() // function scoped inside an object in a module

module Foo = {
  module Bar = {
    [@bs.module "foo"] [@bs.scope "bar"] external baz: unit => unit = "baz";
  };
};

let () = Foo.Bar.baz();

It's not necessary to nest the binding inside Reason modules, but mirroring the structure of the JavaScript module layout does make the binding more discoverable.

Note that [@bs.scope] works not just with [@bs.module], but also with [@bs.val] (as shown earlier), and with combinations of [@bs.module], [@bs.new] (covered in the OOP section), etc.

Tip: the [@bs.scope ...] attribute supports an arbitrary level of scoping by passing the scope as a tuple argument, e.g. [@bs.scope ("a", "b", "c")].

Functions

const dir = path.join('a', 'b', ...) // function with rest args

[@bs.module "path"] [@bs.variadic] external join: array(string) => string = "join";
let dir = join([|"a", "b", ...|]);

Note that the rest args must all be of the same type for [@bs.variadic] to work. If they really have different types, then more advanced techniques are needed.

Ref: https://reasonml.org/docs/reason-compiler/latest/function#variadic-function-arguments

const nums = range(start, stop, step) // call a function with named arguments for readability

[@bs.val] 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

[@bs.val] external fooString: string => unit = "foo";
[@bs.val] external fooBool: bool => unit = "foo";

fooString("");
fooBool(true);

Because BuckleScript bindings allow specifying the name on the Reason side and the name on the JavaScript side (in quotes) separately, it's easy 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)

[@bs.val] external range: (~start: int, ~stop: int, ~step: int=?, unit) => array(int) = "range";
let nums = range(~start=1, ~stop=10, ());

If a Reason function or binding has 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 the function can actually execute. If this seems tedious, remember that no other language gives you out-of-the-box curried parameters and named parameters and optional parameters.

mkdir('src/main', {recursive: true}) // options object argument

type mkdirOptions;
[@bs.obj] external mkdirOptions: (~recursive: bool=?, unit) => mkdirOptions = "";

[@bs.val] external mkdir: (string, ~options: mkdirOptions=?, unit) => unit = "mkdir";

// Usage:

let () = mkdir("src", ());
let () = mkdir("src/main", ~options=mkdirOptions(~recursive=true, ()), ());

The [@bs.obj] attribute allows creating a function that will output a JavaScript object. There are simpler ways to create JavaScript objects (see OOP section), but this is the only way that allows omitting optional fields like recursive from the output object. By making the binding parameter optional (~recursive: bool=?), you indicate that the field is also optional in the object.

Alternative way

Calling a function like mkdir("src/main", ~options=..., ()) can be syntactically pretty heavy, for the benefit of allowing the optional argument. But there is another way: binding to the same underlying function twice and treating the different invocations as overloads.

[@bs.val] external mkdir: string => unit = "mkdir";
[@bs.val] external mkdirWith: (string, mkdirOptions) => unit = "mkdir";

// Usage:

let () = mkdir("src/main");
let () = mkdirWith("src/main", mkdirOptions(~recursive=true, ()));

This way you don't need optional arguments, and no final () argument for mkdirWith.

Ref: https://reasonml.org/docs/reason-compiler/latest/object-2#function

forEach(start, stop, item => console.log(item)) // model a callback

[@bs.val] external forEach: (~start: int, ~stop: int, [@bs.uncurry] int => unit) => unit = "forEach";
forEach(1, 10, Js.log);

When binding to functions with callbacks, you'll want to ensure that the callbacks are uncurried. [@bs.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 docs for details.

Ref: https://reasonml.org/docs/reason-compiler/latest/function#extra-solution

Objects

const person = {id: 1, name: 'Bob'} // create an object

let person = {"id": 1, "name": "Bob"};

Ref: https://reasonml.org/docs/reason-compiler/latest/object-2#literal

person.name // get a prop

person##name

Ref: https://reasonml.org/docs/reason-compiler/latest/object-2#read

person.id = 0 // set a prop

person##id #= 0

Ref: https://reasonml.org/docs/reason-compiler/latest/object-2#write

const {id, name} = person // object with destructuring

type person = {id: int, name: string};

let person = {id: 1, name: "Bob"};
let {id, name} = person;

Since BuckleScript version 7, Reason record types compile to simple JavaScript objects. But you get the added benefits of pattern matching and immutable update syntax on the Reason side. There are a couple of caveats though:

  • The object will contain all defined fields; none will be left out, even if they are optional types
  • If you are referring to record fields defined in other modules, you must prefix at least one field with the module name, e.g. let {Person.id, name} = person

Ref: https://reasonml.org/docs/reason-compiler/latest/object#records-as-objects

Classes and OOP

In BuckleScript it's 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

const foo = new Foo();
foo.bar();

You will write:

let foo = Foo.make();
let () = Foo.bar(foo);

Note that many of the techniques shown in the Functions section are applicable to the instance members shown below.

I don't see what I need here

Try looking in the Functions section; in BuckleScript functions and instance methods can share many of the same binding techniques.

const foo = new Foo() // call a class constructor

// Foo.re
// or,
// module Foo = {

type t;

// The `Foo` at the end must be the name of the class
[@bs.new] external make: unit => t = "Foo";

//}
...
let foo = Foo.make()

Note the abstract type t. In BuckleScript you will model any class that's not a shared data type as an abstract data type. This means you won't expose the internals of the definition of the class, only its interface (accessors, methods), using functions which include the type t in their signatures. This is shown in the next few sections.

A BuckleScript 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 BuckleScript modules.

Ref: https://reasonml.org/docs/reason-compiler/latest/class#new

const bar = foo.bar // get an instance property

// In module Foo:
[@bs.get] external bar: t => int = "bar";
...
let bar = Foo.bar(foo);

Ref: https://reasonml.org/docs/reason-compiler/latest/property-access#static-property-access

foo.bar = 1 // set an instance property

// In module Foo:
[@bs.set] external setBar: (t, int) => unit = "bar"; // note the name
...
let () = Foo.setBar(foo, 1);

foo.meth() // call a nullary instance method

// In module Foo:
[@bs.send] external meth: t => unit = "meth";
...
let () = Foo.meth(foo);

Ref: https://reasonml.org/docs/reason-compiler/latest/function#object-method

const newStr = str.replace(substr, newSubstr) // non-mutating instance method

[@bs.send.pipe: string] external replace: (~substr: string, ~newSubstr: string) => string = "replace";

let newStr = replace(~substr, ~newSubstr, str);

[@bs.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. This is handy for non-mutating functions as they traditionally take the instance as the final parameter.

It wasn't 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 you don't strictly need to use [@bs.send.pipe]; if you want you can use [@bs.send] everywhere.

arr.sort(compareFunction) // mutating instance method

[@bs.send] external sort: (array('a), [@bs.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

[@bs.get] external bar: t => option(int) = "bar";

switch (Foo.bar(foo)) {
| Some(value) => ...
| None => ...
}

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(...) type.

Ref: https://reasonml.org/docs/reason-compiler/latest/null-undefined-option

foo.bar == null // check for null or undefined

[@bs.get] [@bs.return nullable] external bar: t => option(t);

switch (Foo.bar(foo)) {
| Some(value) => ...
| None => ...
}

If you know the value is 'nullable' (i.e. could be null or undefined), or if the value could be polymorphic, then [@bs.return nullable] is appropriate to use.

Note that this attribute requires the return type of the binding to be an option(...) type as well.

More Repositories

1

re-web

Experimental web framework for ReasonML & OCaml
OCaml
263
star
2

fullstack-reason

A demo project that shows a fullstack ReasonML/OCaml app–native binary + webapp
Reason
166
star
3

scala-modules

Experimental implementations of ML-style modules in Scala
Scala
142
star
4

prometo

A type-safe JavaScript promise library for ReasonML
Reason
31
star
5

fsharp-typeclasses

F#
24
star
6

ggspec

A lightweight, functional-style unit testing framework for Guile/Scheme
Scheme
24
star
7

re-hyperapp

Almost zero-cost bindings for the https://github.com/hyperapp/hyperapp UI library.
OCaml
21
star
8

ocaml_sql_query

PoC of functional-style SQL query
OCaml
21
star
9

ocaml-decimal

Arbitrary-precision floating-point decimals
OCaml
21
star
10

lambdak

Full anonymous functions for Python
Python
19
star
11

dbc

Design-by-contract programming for JavaScript and ReasonML
OCaml
12
star
12

bs-webapi

(WIP) BuckleScript Web API Bindings
OCaml
10
star
13

stubbex

Stub and validate HTTP endpoints with ease.
Elixir
10
star
14

bs-hyperapp

DEPRECATED - see https://github.com/yawaramin/re-hyperapp for a more recent (experimental) attempt
OCaml
8
star
15

dream-html

Generate HTML markup from your Dream backend server
OCaml
6
star
16

bucklescript-cyclejs-test

OCaml
5
star
17

resilient-services

OCaml
5
star
18

learning-tydd-reason

Learning Type-Driven Development with ReasonML
OCaml
5
star
19

reason-workshop

HTML
4
star
20

aoc2020

Advent of Code 2020
OCaml
3
star
21

bs-pouchdb

OCaml
3
star
22

fake-io

A demonstration of controlling and testing the IO effect in Scala
Scala
3
star
23

fsharp-typed-json

JSON for F# using typeclasses.
F#
1
star
24

TDDMoney

My C# interpretation of the `Money' example from Kent Beck's Test-Driven Development by Example
C#
1
star
25

bs-doc-mgr

OCaml
1
star
26

fsharp-cond

Multi-Way Conditional Expression for F#
F#
1
star
27

lightweght-static-capabilities

Paper summary slides. Video:
OCaml
1
star
28

webagent

HTTP request builder using FreePascal/Lazarus
Pascal
1
star
29

letops

OCaml standard library let operators
OCaml
1
star
30

TDDUnit

My interpretation in C# of the unit-testing framework example from Kent Beck's Test-Driven Development
C#
1
star