Edited at

ReasonMLでJavaScriptをbindingするあれこれ

この記事は ACCESS Advent Calendar 2018、2日目の記事です。

どうも、仕事ではECMAScript3をよく書いている @soebosi です。

趣味でReasonMLを書いているので、今回はその話をします。


ReasonML

ReasonMLは、Facebook社が開発しているプログラミング言語です。

JavaScriptにトランスパイルできてReactを使うことを前提としているので、JSX的な構文も言語の機能として持っています。

JavaScriptとのbinding機構も用意されていて、構造に合わせていくつかの手段があります。

今回は、binding機構についてまとめたいと思います。

くわしくは、 https://bucklescript.github.io/en/ の公式ドキュメントを参照ください。

(ReasonMLといっておきながら、JavaScriptのbinding機構はBuckleScriptによってもたらされているので、BuckleScriptのドキュメントを見る必要あり)

動作確認したいときは、公式のplaygroundがおすすめです。

https://reasonml.github.io/en/try


%raw, %%raw

JavaScriptのプログラムを直接文字列で記述する方法です。

%rawは、そのままReasonML側の式に組み込むときに使用して、%%rawはトップレベルでの宣言で使用します。


raw1.re

let add = [%raw "(a, b) => a + b"];

[%%raw "console.log(\"hello world\")"];

Js.log(add(1, 2)); /* 3 */


これをJavaScriptにトランスパイルすると、こんな感じ。


raw1.bs.js

var add = ((a, b) => a + b);

console.log("hello world")
;

console.log(Curry._2(add, 1, 2));


ReasonMLは、ダブルクォートで囲む方法以外に{||}で囲むことでも文字列を作成できます。

%raw, %%rawでも使用できるので、ダブルクォートをJavaScript側で使いたいときには、エスケープしなくてもダブルクォートを埋め込むことができます。

さきほどの%%rawの例を書き換えると、以下みたいにできます。


raw2.re

[%%raw {|console.log("hello world")|}];



型はどうなってるの?

%%rawについては、ReasonMLの世界に進出してきていないのでよいのですが、%rawではReasonMLの値として使用できてしまいます。すなわち型があるはずです。

https://reasonml.github.io/docs/en/interop#dumping-in-some-javascript-and-making-it-accessible-from-reason

を見てみると、「magic type」なんて表現がされていますね。

ちょっと実験した感じ、どの型とも比較ができたので、anyのような型なのだと思います。

型を指定する方法も用意されているみたいで、以下の書き方ができます。


raw3.re

let add: (int, int) => int = [%raw (a, b) => "return a + b"];



@bs.val

グローバルに宣言されいてる値をbindingする時に使うのが、@bs.valです。

externalの後にReasonML側で使用する名前と型定義、=の後にJavaScript側の名前を記述します。ReasonML側で使用している名前がJavaScriptと同じ場合は、空文字として省略ができます。


val.re

[@bs.val] external setTimeout : (unit => unit, int) => float = "setTimeout";

/* or */
[@bs.val] external setTimeout : (unit => unit, int) => float = "";

setTimeout(() => Js.log("Hello World"), 100);


JavaScriptっぽくそのまま使えて素敵ですね。

出力されるJavaScriptも素直で読みやすいです。


val.bs.js

setTimeout((function () {

console.log("Hello World");
return /* () */0;
}), 100);


@bs.scope

setTimeoutのようなグローバルに定義された関数であれば、@bs.valだけでbindingできますが、グローバルなオブジェクトに紐づく関数をbindingしたい場合、@bs.scopeを使う必要があります。


scope1.re

[@bs.val] [@bs.scope "Math"] external floor : float => int = "";

Js.log(floor(5.2)); /* 5 */



scope1.bs.js

console.log(Math.floor(5.2));


nestされている場合は、カンマ区切りを括弧で囲むといけるようです。


scope2.re

[@bs.val] [@bs.scope ("window", "location")] external href: string = "";



@bs.new

newを使って生成されたインスタンスをbindingしたい場合は、@bs.newを使ってJavaScript側のコンストラクタをbindingします。


new.re

type date;

[@bs.new] external createDate : string => date = "Date";

Js.log(createDate("1995-12-17T03:24:00"));



new.bs.js

console.log(new Date("1995-12-17T03:24:00"));



@bs.send

生成されたオブジェクトのメソッドをbindingするときには、@bs.sendを使います。

ReasonML側の関数としては、第一引数に@bs.newで生成した型を渡します。


send.re

/* "new.re のつづき" */

[@bs.send] external getTime : date => int = "";

let d = createDate("1995-12-17T03:24:00");
Js.log(d->getTime); /* "「->」は実はパイプ演算子で、「Js.log(getTime(d));」と同じ意味になります。" */



send.bs.js

var d = new Date("1995-12-17T03:24:00");

console.log(d.getTime());


読める形でトランスパイルされるのは、本当に素晴らしいですね。


@bs.module

今までのbindingは組み込みのものでしたが、外部のモジュールをimport/exportしてbindingしたい場合もあります。

そのときは、@bs.moduleが利用できます。

読み込みたいモジュール名を@bs.moduleの後ろに書きます。


module.re

[@bs.module "path"] external basename : (string, string) => string = "";

Js.log(basename("/hoge/fuga/piyo.txt", "txt")); /* piyo */



module.bs.js

var Path = require("path");

console.log(Path.basename("/hoge/fuga/piyo.txt", "txt"));



@bs.variadic(bs.splite)

可変長引数のJavaScript関数を扱う場合は、@bs.variadicが使用できます。

(最近、@bs.spliteからリネームされたみたいで、playgroundでは、@bs.spliteしか使えませんでした)

@bs.variadicは、ReasonMLでの配列をJavaScriptの可変長引数として展開します。

使う場合は、ReasonML側の末尾の引数がarray('a)である必要があります。


variadic.re

[@bs.module "path"] [@bs.variadic]

external join : array(string) => string = "";

Js.log(join([|"hoge", "fuga", "piyo"|])); /* piyo */



variadic.bs.js

var Path = require("path");

console.log(Path.join("hoge", "fuga", "piyo"));



まとめ

ReasonMLのすばらしいところの一つに型による安全性があると思います。

究極%bs.rawを使えば、何でもbindingできるのですが、型の恩恵が受けられなくなってしまいます。

適切なbinding機構を選択しつつ、きちんとJavaScriptとのbindingに型を定義して、堅牢なアプリケーションを作りたいものです。

実は他にも@bs.set, @bs.meth, @bs.objみたいなbinding機構があるようですが、また別の機会にまとめることにします。

明日は、 @aKatsuhiroMihara のGo言語の話です!お楽しみに!