Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
3
Help us understand the problem. What is going on with this article?
@soebosi

ReasonMLでJavaScriptをbindingするあれこれ

More than 1 year has passed since last update.

この記事は 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言語の話です!お楽しみに!

3
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
soebosi

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
3
Help us understand the problem. What is going on with this article?