slides
たのしい js_of_ocaml
Jun FURUSE/古瀬 淳
Technical memorandum, 2022-03-29

js_of_ocaml とは

OCaml から JavaScript を生成するコンパイラ。略称 JSOO。

OCaml バイトコードを逆アセンブルして JavaScript に変換

  • バイトコードにできれば JS になる
  • 言語上制約はない!
  • 型がついているのが嬉しい (TypeScript がお好きならそれでもよい)

ドキュメント

これ読めばいいです。例もあるよ:

https://ocsigen.org/js_of_ocaml/latest/manual/overview

このスライドは、突然マニュアルを読むのはしんどいという人のためのものです。 把握したらマニュアルに移ってくれて結構よ。

予備知識

  • OCaml, Lwt
  • HTML, JavaScript
  • イベントプログラミングモデル

Hello world

始め方

ocaml と opam を初期化し、

$ opam install js_of_ocaml js_of_ocaml-lwt \
    js_of_ocaml-ppx

とりあえずはこれで十分。 他にも js_of_ocaml 関連パッケージがあるが、ご自分のプロジェクトの応じてインストールして。

dune

dune-project

(lang dune 3.0) ; 古い dune の場合 2.0 くらいにしておくれ
(name hello)

dune

(executable
 (modes js)       ; JS にコンパイル
 (name hello)
 (libraries
    js_of_ocaml
    js_of_ocaml-lwt)
 (preprocess (pps js_of_ocaml-ppx))) ; プリプロセス(後述)

hello.ml

open Js_of_ocaml

(* ## については後述 *)
let () = Firebug.console##log 1234

Firebug という名前、気にしない。(老人には懐かしい

index.html

<html>
  <head>
    <title>Hello</title>
    <!-- 場所に注意 -->
    <script src="_build/default/hello.bc.js"></script>
  </head>
  <body>
    Hello
  </body>
</html>

ビルド結果の JS は _build/default/hello.bc.js

ビルド

$ dune build

実行

ブラウザで index.html を開き、

コンソールログに 1234 が出ているのを確認

演習

写経して動作を確認してください。

  • dune, dune-project
  • hello.ml
  • dune build
  • _build/default/hello.bc.js
  • index.html

開発の流れ

  • .ml を編集
  • dune build
  • .html をリロード
  • コンソールを見る
  • 繰り返す

でかい

$ du -h _build/default/hello.bc.js 
4.0M    _build/default/hello.bc.js

ビルドは早いがでかい。Dead code elimination されてない。
ソースマッピング情報がふんだんに入っている

プロファイルを変える

dune ビルドプロファイルを dev(デフォルト) から変えると、
リンク時に時間がかかるが JS が小さくなる:

$ dune build --profile=release
$ du -h _build/default/hello.bc.js 
76K _build/default/hello.bc.js

デプロイ時には --profile を忘れずに。

JSOO の型

JS オブジェクトと型

JS のオブジェクト:

  • メソッド
  • プロパティ(読み専用/読み書き)

どのオブジェクトが何を持っているか覚えてられない。

JSOO では OCaml のオブジェクト型を使ってこれを把握する

JS オブジェクトの型: < ... > Js.t

< ... > というオブジェクト型がパラメータになっているが、
_ Js.t 自体は OCaml のオブジェクトではない。

< ... > のメンバ

メソッド
method mname : t1 -> t2 -> t3 meth
読み取り専用プロパティ
method pname : t1 readonly_prop
書き込み可能プロパティ
method pname : t1 prop

JS オブジェクトへのアクセス

OCaml オブジェクトに似たアクセス構文:

メソッド呼び出し
o##mname arg1 .. argn
プロパティ読み出し
o##.pname
プロパティ上書き
o##.pname := v

(js_of_ocaml-ppx プリプロセッサによる)

この JS オブジェクト何できる?

わからなくなったら Merlin で型を見る。
< ... > の部分に使えるメソッドとプロパティが書いてある。

js_string Js.t

文字列型。 OCaml の文字列はそのままでは JS には使えない。変換が必要:

val Js.string : string -> js_string Js.t
val Js.to_string : js_string Js.t -> string

メソッドなどは js.mliclass type js_string の定義を参照。

あまりによく使うので、個人的には (!$)(?$) という別名を定義している。

js_string 単体では使わないので type string_t = js_string Js.t としても便利かもしれない。

Js._true, Js._false

val Js.bool : bool -> bool Js.t
val Js.to_bool : bool Js.t -> bool

int

int は普通に使えるがオブジェクトではない。

cf. class type number

他の JS の型は

そろそろ飽きてきた。

js.mli 見たらわかります。
各メソッドの細かい意味は JS のリファレンスを。

Js.Unsafe については後述

Dom_html

Webページを操作する: Dom, Dom_html

見た目に面白いことをしよう。

Webページを操作したい場合は DomDom_html を使う。

dom.mli は基本的な DOM の操作が宣言されている。
例 Element を足す、消す

dom_html.mli は 3000行ありますが、怖くない。
各 HTML element のクラスにどんなメソッドやプロパティがあるか延々と列挙してあるだけ。

Dom_html: 単純な例

open Js_of_ocaml

module Html = Dom_html (* 慣習 *)

let () =
  Html.window##.onload := (* HTMLがロードし終わったら *) 
    Html.handler (fun _ev -> (* イベントハンドラ生成 *)
      let button = (* <button> を作る *)
        Html.document##createElement (Js.string "button") 
      in
      button##.innerText := Js.string "Hello"; (* 見た目 *)
      (* <body> の最後に足す *)
      Dom.appendChild Html.document##.body button;
      Js._false (* デフォルトハンドラは呼び出さない *) )

演習: 写経して動作確認

Dom_html.getElementById

id によるエレメント取得:

Html:

<div id="name">

JSOO:

Dom_html.getElementById "name" : element Js.t

イベントハンドラ

各エレメントのハンドラプロパティを上書きする

button##.onclick := Html.handler (fun ev ->
  ...; Js._false (* もしくは Js._true *) )

どんなハンドラプロパティがあるかは .mli か Merlin で型を見て。

イベント情報も型からわかります。

演習

  • button を押すと
  • textarea の現在の内容を読んで
  • その内容の label を追加する

アプリを書いてね

JS object

JS object

{ i:42, j:true } みたいなもの。書けます:

let o = 
  object%js
    val i = 42
    val mutable j = Js._true
  end

型は prop メソッドになり ##. でアクセスできる:

o : < i : int readonly_prop;  s : bool t prop >
o##.j := Js._false

JS class type

自分でも定義できる

class type my_class : object
  method i : int readonly_prop
  method j : bool t prop
end
let o : my_class Js.t = 
  object%js
    val i = 42
    val mutable j = Js._true
  end

JSON

JSON

class type json = object
  method parse : js_string t -> 'a meth
  method stringify : 'a -> js_string t meth
end

val _JSON : json t

OCaml プログラマにとっては多相型が奇妙だが、便利:

(* JS object を JSON 文字列に: "{\"i\":42,\"j\":true}" *)
Js._JSON.stringify
  (object%js val i = 42 val j = Js._true end)

(* 逆は実行時型検査は行われないので正しい型を与えること: *)
let x : my_class Js.t =
  Js._JSON.parse (Js.string "{\"i\":42,\"j\":true}")

HTTP

HTTP

通信は非同期で行われるので Lwt モナドを使います:

open Js_of_ocaml_lwt 

(* 詳しくは perform の型を見てね。 *)
XmlHttpRequest.perform url : http_frame Lwt.t

(* get の方が単純 *)
XmlHttpRequest.perform : string -> http_frame Lwt.t

Lwt

知らない人は勉強してください。

open Lwt.Syntax すると便利:

  • bind: let* x = e in e'
  • fmap: let+ x = e in e'

XmlHttpRequest の例

GitHub API の返答を表示する

open Js_of_ocaml
open Js_of_ocaml_lwt
open Lwt.Syntax

let () = Html.window##.onload := Html.handler (fun _ev ->
  ignore (
    let+ res = (* fmap, >|= *)
      XmlHttpRequest.perform "https://api.github.com" 
    in
    Html.document##.body##.innerText
      := Js.string res.content))

Lwt は作れば勝手に実行されます。
(Lwt_main.run で受ける必要はありません。)

演習

  • <button>を押すと、<input> からユーザー名を読みこみ、
  • GitHub REST API の https://api.github.com/users/ユーザー名 にアクセスし
  • 得られたJSONからユーザーID(id)を取得
  • 表示してください。

CORS に注意!

普通は XmlHttpRequest.get で外部サイトからデータを取ってこれません。

CORSの原理を知って正しく使おう by 徳丸浩さんの

再帰

末尾再帰

JS のスタックは浅い気がします。非末尾再帰は OCaml native より overflow しがち。10000も回せない。

JSOOは末尾再帰最適化(TCO)をします。末尾再帰で書いてね。

例外あり

ただ一部の末尾再帰はTCOしてくれません😭

詳しくはドキュメントを読んで。

ただ OCaml コンパイラのような再帰の化け物も JS にコンパイルできます。安心してください。 踏んでも修正は容易です。

演習

スタックの浅さを実感してください

  • List.init でリスト [0; 1; ...; 10000] を作って
  • 各要素に 1 足したリストを作って console に表示してください
  • Native OCaml だと List.map でいけます。

Web worker

Web worker

JS はシングルスレッド。短時間で処理終了しないと怒られる。

そこで web worker に裏で仕事をさせる。
メッセージパッシングで通信。

Worker側

Worker.set_onmessage (Html.handler (function
  | .. ->  ..;  Worker.post_message x;  ..
      
  • Worker.set_onmessage でメインからのメッセージを受ける
  • Worker.post_message でメインにメッセージを送る

送受の型はちゃんと合わせてください。チェックはありません。

Web worker #2

メイン側

(* worker を作る *)
let w = Worker.create "./hoge_worker.bc.js" in
(* worker からのメッセージ処理 *)
w##.onmessage := Html.handler (fun ev -> ..; Js._false);
(* メッセージパッシングで起動 *)
w##postMessage m

送受の型は自分で合わせてね。

問答無用で worker を殺す

w##terminate

再開には再度 Worker.create

Web worker #3

自分が worker かどうか判定

let is_worker =
  try ignore (Js.Unsafe.eval_string "window"); false
  with _ -> true

メインと worker を同じ JS として書ける。

JSOO は結果が大きくなりがちなので
worker コードのロード時間削減できる。

演習

let rec fib = function
  | 1 | 2 -> 1
  | n -> fib (n-1) + fib (n-2)

<button> を押すと fib 47 を計算して表示するコードを書いてください。

押すとページが長時間反応しなくなります。これを web worker を使って修正してください。

  • 計算中は再度ボタンが押せなくするようにすべきですね。
  • 計算中断ボタンを足してみてもいいでしょう。

外部ライブラリ

Unix ライブラリは使えません

でもコンパイルはできてしまう。
バイトコード生成も JS 生成も問題ない。でも動かない。

例: Unix.sleep:

unix.ml:450 Uncaught TypeError: runtime.unix_sleep is not a function
    at sleep (unix.ml:450:45)
    at caml_call1 (unix.ml:0:0)
    at _c_ (hello.ml:40:13)
    at caml_call1 (jsoo_runtime.ml:0:0)
    at HTMLButtonElement.<anonymous> (dom.ml:320:2)

External 宣言関数は runtime に JS の挙動があると仮定:

external sleepf : float -> unit = "unix_sleep"

逆に runtime を用意すれば動く

例: 任意制度整数ライブラリ zarith を JS で使うためには、 zarith_stubs_js ライブラリをリンクする。

; dune
(executable
  (mode js)
  ..
  (libraries
    js_of_ocaml
    zarith_stubs_js))

ml_z_.. などの external の実装が runtime の下に生える。

演習

前の演習では fib 47 は負数を表示していたと思います。
これは JS の int が signed 32bit のため、オーバーフローしていたからです。

これを zarithZ.t を使ってちゃんと計算してください。

なければ自分で作れる

JSOO 未対応の JS の機能や外部 JS ライブラリを使いたい場合、

  • class type hoge でインターフェースを宣言
  • Js.Unsafe モジュールを使ってJS を直接評価
  • 結果の型を hoge Js.t
  • JS で new cls args するものは _ Js.constr を使う

したりして自分で作れます。ただ、

  • 外部 JS ライブラリを完全理解することになる
  • 作業量、メンテナンスが大変

たのしい js_of_ocaml ではなくなりつつある

頑張る場合の tips

  • 小さい例から始める
  • Console log を多用する
  • 型不安全世界
    • js_string t ではなく string を使ってハマる。
    • js_array t ではなく listarray を使ってハマる。
  • Late binding: ライブラリのロードが終わってから、 ライブラリのプロパティを eval_string で取り出す。先に参照すると undefined

Google Chart API

https://developers.google.com/chart/interactive/docs/quick_startを JSOO に。

  • Google chart がいいというわけではない
  • 昔やったことがあり、今も動いたので
  • Google map は API キーが必要
  • d3.js はメソッドチェーンの対応など複雑

Google Chart API: 外部ライブラリのロード

これは HTML に書けばよい。(zarith_stubs_js みたいにしてもよいが)

<script src="https://www.gstatic.com/charts/loader.js">
</script>

Google Chart API: パッケージのロード

google.charts.load('current', {'packages':['corechart']});

考える:

  • google.charts は直に Unsafe アクセスするかな
  • load というメソッドを生やそう
  • "current"Js.js_string Js.t であって string ではないよ
  • {'packages':...} には object%js .. end を使うかな

Google Chart API: パッケージのロード

{'packages':..}:

class type packages = object
  method packages : js_string t js_array t readonly_prop
end

google.charts:

class type google_charts = object
  method load : js_string t -> packages t -> unit meth
end

let google_charts () : google_charts Js.t =
  Js.Unsafe.pure_js_expr "google.charts"

使い方:

(google_charts ())##load 
  (Js.string "current")
  (object%js packages= Js.array [| ... |] end);;

Google Chart API: コールバック

google.charts.setOnLoadCallback(drawChart);

setOnLoadCallback を生やそう:

class type google_charts = object
  method load : js_string t -> packages t -> unit meth
  method setOnLoadCallback: (unit -> unit) -> unit meth
end

使い方:

google_charts##setOnLoadCallback drawChart;;

Google Chart API: DataTable

var data = new google.visualization.DataTable();

考える:

  • google.visualization は Unsafe アクセスするかな
  • DataTableは プロパティっぽい
    • new DataTable () するので型は _ Js.constr readonly_prop だろう

Google Chart API: DataTable

大文字の JS フィールドは 頭に _ を付けて OCaml のメソッドに:

class type google_visualization = object
  method _DataTable : dataTable t constr readonly_prop
end

google.visualization.* は late binding されるので関数に:

let google_visualization () : google_visualization t =
  Js.Unsafe.pure_js_expr "google.visualization"

使い方:

let datatable = (google_visualization ())##._DataTable in
let data = new%js datatable in ...

Google Chart API: DataTable

data.addColumn('string', 'Topping');
data.addColumn('number', 'Slices');
data.addRows([ ['Mushrooms', 3], ... ]);

使われ方からすると、DataTable の型は:

class type dataTable = object
  method addColumn 
    : js_string t -> js_string t -> unit meth
  method addRows 
    : Js.Unsafe.any js_array t js_array t -> unit meth
end
  • listarray じゃないよ
  • js_string tint が混ざったヘテロ array。Js.Unsafe.any を使う

Google Chart API: ヘテロ配列

[ [ 'Mushrooms', 3 ], [ 'Onions', 1 ], ... ]

Js.Unsafe.inject で型を潰す:

let row s n =
  Js.array [| Js.Unsafe.inject (Js.string s);
              Js.Unsafe.inject n |]
in
let rows = array [| row "Mushrooms" 3; 
                    row "Onions" 1;
                    ... 
                 |]
in
data##addRows rows

Google Chart API: div の取得

let chart_div = Html.getElementById "chart_div" in ...

ちゃんと HTML に <div id="chart_div"> を書いてね。

Google Chart API: PieChart

var chart = new google.visualization.PieChart(..);

PieChart を生やそう:

  • new するから constr。引数は HTML要素 Html.element t っぽい。
  • draw というメソッドがある。DataTable とJS オブジェクトを取る
class type pieChart = object
  method draw : dataTable t -> chart_option t -> unit meth
end

class type google_visualization = object
  method _DataTable : ...
  method _PieChart 
    : (Html.element t -> pieChart t) constr readonly_prop
end

Google Chart API: Chart オプション

var options = {'title':'How Much Pizza I Ate Last Night',
               'width':400,
               'height':300};

Class type は宣言しなくてもいいけど:

class type draw_option = object
  method title : string_t readonly_prop
  method width : int readonly_prop
  method height : int readonly_prop
end

let chart_options = object%js
    val title = Js.string "How Much Pizza I Ate..."
    val width = 400
    val height = 300
  end;;

Google Chart API: 仕上げ

let google_charts () = ...
let google_visualization () = ...

let drawChart () =
  ... 今までのコード ...
  chart##draw data chart_options

let () = Html.window##.onload :=
    Html.handler (fun _ev ->
        let charts = google_charts () in
        charts##load
          (Js.string "current")
          (object%js val packages = ... end);
        charts##setOnLoadCallback drawChart;
        Js._false)

課題

この Chart API の例を JSOO に移植してください。

えっ大変?大変なところはだいたい書いたよ。

たのしい js_of_ocaml

たのしい:

  • JS のオブジェクトが OCaml のサブタイピングに綺麗に乗る。
  • 両者シングルスレッド + async なので綺麗に Lwt に乗る。
  • 頑張れば JS のライブラリに OCaml の型を付けれる。

こっから先が たのしい かは人による。

おしまい。