BuckleScriptに入門しました
最近、フロントエンドを久しぶりに書く必要に駆られたのですが、その際に前から気になっていた BuckleScript を触ってみて入門してみました。
Bucklescriptとは
公式的には
A backend for the OCaml compiler which emits JavaScript.
とのことです。大元は、Bloomberg社(JaneStreetと同じく?OCamlの大口ユーザー)で作成されていたものだそうです。特徴として
- 型安全(OCamlだからね!)
- ハイクォリティなdead code elimination
- OCamlからの生成時のみならず、bundler(Google closure-compilerとか)でもdead code eliminationがやりやすいようなソースを出すそうです
- Offline optimization
- 大抵のJavaScript処理系が持つJITを頼らずに、高速なコードを生成するそうです
- JS/ネイティブ両方対応
- JSってどこでも動くよね、ということで。
- OCamlコンパイラを利用することでの(文字通りに)桁違いの速度のコンパイル
- TypeScriptの速度と比べると、文字通りに桁違いに速いです。
- 全部合わせて60ファイルくらいのコンパイルが、1.2秒(実測)で終わるくらい速いです。
ということが、公式ドキュメントで語られています。
ただ、いくつか注意があります。
- 元になっているOCamlのバージョンが4.02.3
- Bucklescriptが、いくつかの構文拡張などのために、OCamlコンパイラ自体にパッチを当てているためです。常に最新のバージョンを利用している人は注意です。
Bucklescriptのインストール
インストールは非常に簡単で、OCamlユーザーであれば使っているOPAMであれば、Bucklescriptのパッチが当たった版のOCamlをお手軽にインストールすることが出来ます。
opam switch 4.02.3+buckle-master
Bucklescriptを実際に利用する時は、 bs-platform
というnpmパッケージを利用します。
npm install --save bs-platform
Bucklescriptの環境は、bs-platformが提供するコマンドから作成できます。
$ $(npm bin)/bsb -init test
$ ls -la
合計 36
drwxr-xr-x 5 derui derui 4096 7月 18 22:40 .
drwxr-xr-x 3 derui derui 4096 7月 18 22:40 ..
-rw-r--r-- 1 derui derui 168 7月 18 22:40 .gitignore
drwxr-xr-x 2 derui derui 4096 7月 18 22:40 .vscode
-rw-r--r-- 1 derui derui 151 7月 18 22:40 README.md
-rw-r--r-- 1 derui derui 141 7月 18 22:40 bsconfig.json
drwxr-xr-x 3 derui derui 4096 7月 18 22:40 node_modules
-rw-r--r-- 1 derui derui 276 7月 18 22:40 package.json
drwxr-xr-x 2 derui derui 4096 7月 18 22:40 src
見慣れたnpmの構成に加えて、 bsconfig.json
というファイルがあります。これが、Bucklescriptでのビルド指示を行うファイルです。中身はこんな感じになっています。
{
"name": "main",
"version": "0.1.0",
"sources": [
{"dir": "src", "public": "all"}
],
"bs-dependencies" : [
"bs-fetch", "bs-dom-wrapper", "bs-lwt"
],
"generate-merlin": true,
"package-specs": ["commonjs"]
}
安心のmerlin対応です。また、 bs-dependencies
という見慣れないものがあります。Bucklescriptの大きな特徴は、パッケージ管理としてnpmをそのまま利用できる点で、npmでインストールしたパッケージをこっちにも書くことで、OCaml内からも利用できます。
Bucklescriptの例
実際に書いた、すごい薄いReactのバインディングはこんな感じです。前半はglueコードなので、bs.raw extensionが終わった後からが本番です。
[%%bs.raw{|
var _React = require('react');
var _createReactClass = require('create-react-class');
function _createElement (clazz, props, children) {
return _React.createElement(clazz, props, ...children);
}
function _createClass (fn, initialState, config) {
return _createReactClass({
getInitialState: function () {
return { state: initialState };
},
componentWillReceiveProps: function(newProps) {
if (config && config.willReceiveProps) {
config.willReceiveProps(this.props, this.state.state, newProps, state => this.setState({state}));
}
},
shouldComponentUpdate: function(props, state) {
if (config && config.shouldUpdate) {
return config.shouldUpdate(this.props, this.state.state, props, state.state);
}
return true;
},
componentDidUpdate: function() {
if (config && config.didUpdate) {
config.didUpdate(this.props, this.state.state, state => this.setState({state}));
}
},
componentDidMount: function() {
if (config && config.didMount) {
return config.didMount(this.props, this.state.state, state => this.setState({state}));
}
},
componentWillMount: function() {
if (config && config.willMount) {
return config.willMount(this.props, this.state.state, state => this.setState({state}));
}
},
componentWillUnmount: function() {
if (config && config.willUnmount) {
return config.willUnmount(this.props, this.state.state);
}
},
render: function () {
return fn(this.props, this.state.state, state => this.setState({ state }))
}
});
}
|}]
module D = Bs_dom_wrapper
type element
type ('props, 'state) component
type 'state set_state_fn = 'state -> unit
type ('prop, 'state) should_update =
'prop -> 'state -> 'prop -> 'state -> bool
type ('prop, 'state) mount = 'prop -> 'state -> 'state set_state_fn -> unit
type ('prop, 'state) unmount = 'prop -> 'state -> unit
type ('prop, 'state) receive_props = 'prop -> 'state -> 'prop -> 'state set_state_fn -> unit
(* make configuration object for component created from createComponent_ function *)
external make_class_config :
?shouldUpdate:('prop, 'state) should_update ->
?didUpdate:('prop, 'state) mount ->
?willReceiveProps:('prop, 'state) receive_props ->
?didMount:('prop, 'state) mount ->
?willMount:('prop, 'state) mount ->
?willUnmount:('prop, 'state) unmount ->
unit -> _ = "" [@@bs.obj]
type ('props, 'state) render_fn = 'props -> 'state -> 'state set_state_fn -> element
external createComponent_ : ('props, 'state) render_fn -> 'state -> 'a Js.t -> ('props, 'state) component = "_createClass" [@@bs.val]
external createComponentElement_ : ('props, 'state) component -> 'props -> element array -> element = "_createElement" [@@bs.val]
external createBasicElement_ : string -> 'a Js.t -> element array -> element = "_createElement" [@@bs.val]
(* Needed so that we include strings and elements as children *)
external text : string -> element = "%identity"
(*
* We have to do this indirection so that BS exports them and can re-import them
* as known symbols. This is less than ideal.
*)
let createComponent = createComponent_
let element = createBasicElement_
(* Event of React *)
module SyntheticEvent = struct
class type ['a, 'b] _t =
object
method preventDefault: unit -> unit
method stopPropagation: unit -> unit
method bubbles: bool
method cancelable: bool
method currentTarget: 'a Dom.htmlElement_like
method defaultPrevented: bool
method eventPhase: int
method isTrusted: bool
method nativeEvent: 'b Dom.event_like
method isDefaultPrevented: unit -> bool
method isPropagationStopped: unit -> bool
method target: 'a Dom.htmlElement_like
method timeStamp: int
method type_: string
(* properties when event belongs Mouse Events *)
method altKey: bool
method button: int
method buttons: int
method clientX: int
method clientY: int
method ctrlKey: bool
method getModifierState: int -> bool
method metaKey: bool
method pageX: int
method pageY: int
method relatedTarget: 'a Dom.htmlElement_like
method screenX: int
method screenY: int
method shiftKey: bool
end [@bs]
type ('a, 'b) t = ('a, 'b) _t Js.t
end
(* Define common prop object. *)
external props :
?className: string ->
?onClick:(('a, 'b) SyntheticEvent.t -> unit) ->
?onChange:(('a, 'b) SyntheticEvent.t -> unit) ->
?onSubmit:(('a, 'b) SyntheticEvent.t -> unit) ->
?href: string ->
?_type: string ->
?value: string ->
?defaultValue: string ->
unit -> _ =
"" [@@bs.obj]
(* Ignore function currying with external function *)
let div props children = createBasicElement_ "div" props children
let span props children = createBasicElement_ "span" props children
let a props children = createBasicElement_ "a" props children
let button props children = createBasicElement_ "button" props children
let input props children = createBasicElement_ "input" props children
let form props children = createBasicElement_ "form" props children
let label props children = createBasicElement_ "label" props children
let p props children = createBasicElement_ "p" props children
let canvas props children = createBasicElement_ "canvas" props children
let img props children = createBasicElement_ "img" props children
let component comp = createComponentElement_ comp
(* -- *)
external render : element -> 'a Dom.node_like -> unit = "" [@@bs.module "react-dom"]
JavaScript側の関数に対するバインディングは、OCaml公式のexternal(大抵はCとかのバインディングで使う)を利用するため、構文的に矛盾がありません。すでにCamlP4ではなく、PPXが推奨されている時に開発されたものなので、怪しげな構文拡張とかは基本的になく、attributeとextensionだけ(とちょっとだけ拡張されたclass構文)で構成されています。
上のバインディングを使うと、例えばinputはこんな感じに書けます。
module R = React
(* Property for file component *)
type prop = {
state: Reducer.state;
dispatcher: Dispatch.t;
}
external form_prop :
?className: string ->
?onSubmit: (('a, 'b) R.SyntheticEvent.t -> unit) ->
unit -> _ = "" [@@bs.obj]
type state = unit
let on_submit prop e =
e##preventDefault ();
let dispatch action = Dispatch.dispatch prop.dispatcher action in
Actions.upload_image dispatch prop.state.Reducer.stripped_image
let render props _ _ =
R.form (form_prop ~className:"tp-ImageUploader" ~onSubmit:(on_submit props) ()) [|
R.input (R.props ~className:"tp-ImageUploader_Input" ~onChange:(fun _ -> ()) ()) [||];
|]
let t = R.createComponent render () (React.make_class_config ())
TypeScriptとかで頑張ってGenericsを使って書いているReactのprops/stateとかも、OCamlのレコードとかで利用できますし、ReduxのAction/Reducerとかとの相性は抜群です。
特に、Actionが代数データ型で定義できるのが非常に楽、というか自然に対応できます。対処していないActionがあれば警告、削除漏れがあればエラーなど、強力なコンパイラにまかせて楽が出来ます。
また、 bsb -init
で作成した環境は、 npm run watch
でビルドのポーリングが可能になっており、超高速なコンパイルと相まって生産性も高いです。(人によって感じ方には差異があります)
Bucklescriptのtips
tipsがあるほど書いてませんが、それでもいくつかコツ的なものがあったので紹介します。
class typeとexternal
JSといえば object、というくらい頻出するObjectですが、Objectとのバインディングを行う方法もかなりの量があります。
そのうちでも頻出する(と思っている)のが、class typeによるObject型の定義と、externalによるObject型の作成です。
class typeによるObject型の定義は、公式からの引用ですが、以下のようになっています。
class type _rect = object
method height : int
method width : int
method draw : unit -> unit
end [@bs] (1)
type rect = _rect Js.t
この型がついた変数は、普通にmethodを利用するのと似た拡張構文である ##
を利用することで、JavaScriptのオブジェクトそのままに扱うことが出来ます。class typeを用いていることで、structure typingが可能となっています。
class typeを使ったObject型の定義は、 JavaScriptから返されるオブジェクト に対して利用するのがオススメです。頻繁に利用すると、OCaml側のobject型に慣れていないと??ってなる型エラーがそこかしこで出ます。(経験済み)
対して、 externalによるJavaScriptのObject作成は、 OCamlからJavaScriptの関数に渡すオブジェクト に対して利用するのがオススメです。これまた公式の例ですが、次のように標準のラベル変数を利用することで、さっくり作成できます。
external make_config : hi:int -> ?lo:int -> unit -> t = "" [@@bs.obj] (1)
let u = make_config ~hi:3 ()
let v = make_config ~lo:2 ~hi:3 ()
バインディングを書く対象のライブラリにも寄ると思いますが、JavaScript側のライブラリでなんやかややるタイプの場合はexternalが頻出して、API呼び出しとかでJSONが返ってくるとかそういった場合にはclass typeを使う、みたいな感じでした。
モジュールの分け方
OCamlと同じです。以上。
だとなんともならないんですが、OCamlを利用している以上、NodeJSでやっているような、非常に細かいモジュール分けや、Reactに見られるようなネストしたモジュール構造をOCamlで再現するのはすごーく辛いです。プロジェクト全体でファイル名自体がユニークでなければならないという制約があるので、実際の利用ではここが一番のネックではないでしょうか。
ですので、型に頼りたいロジック側の処理だけBucklescriptで、それ以外はJavaScriptで普通に書く、というのも手です。実際にReact部分も含めて全てBucklescriptで書いてみた限りでは、むしろReact側の方が型に頼りたい感じがするくらいでした。
props/stateが型に守られていると、あの値入れるの忘れた!とかなんでpropに入ってないんだ・・・的なケアレスミスとか単純でもめんどくさいミスが激減しました。
Bucklescriptはいいぞ
計一週間くらい書いてみましたが、バインディングさえ作成してしまえば、後はサクサク書いていくことが出来ました。今回はJavaScript側のライブラリはReactだけで、Redux的なものは自作してみましたが、moduleの作成とかfunctorのケアレスミスに悩んだくらいで、実装自体に苦労はしませんでした。
Bucklescriptもそうですが、Facebookの Reason とか、最近はフロントエンドでOCamlが熱いようです。これを機にユーザーが増え・・・ないかもしれませんが、OCamlの裾野が広がればいいなーと思います。
個人的に作成するJavaScriptは基本的にBucklescriptで書いていこうかな、と思える程度には使えるものになっていますので、ぜひ触ってみてください。