LoginSignup
12

More than 5 years have passed since last update.

BuckleScriptに入門しました

Last updated at Posted at 2017-07-18

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で書いていこうかな、と思える程度には使えるものになっていますので、ぜひ触ってみてください。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12