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
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.mli
の class 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ページを操作したい場合は Dom
と Dom_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
イベントハンドラ
各エレメントのハンドラプロパティを上書きする
どんなハンドラプロパティがあるかは .mli か Merlin で型を見て。
イベント情報も型からわかります。
演習
- button を押すと
- textarea の現在の内容を読んで
- その内容の label を追加する
アプリを書いてね
JS object
JS object
{ i:42, j:true }
みたいなもの。書けます:
型は prop
メソッドになり ##.
でアクセスできる:
o##.j := Js._false
JS class type
自分でも定義できる
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 プログラマにとっては多相型が奇妙だが、便利:
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
でメインからのメッセージを受ける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 を殺す
再開には再度 Worker.create
Web worker #3
自分が worker かどうか判定
メインと worker を同じ JS として書ける。
JSOO は結果が大きくなりがちなので
worker コードのロード時間削減できる。
演習
<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 の挙動があると仮定:
逆に 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 のため、オーバーフローしていたからです。
これを zarith
の Z.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
ではなくlist
やarray
を使ってハマる。
- 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
みたいにしてもよいが)
Google Chart API: パッケージのロード
考える:
google.charts
は直に Unsafe アクセスするかなload
というメソッドを生やそう"current"
はJs.js_string Js.t
であってstring
ではないよ{'packages':...}
にはobject%js .. end
を使うかな
Google Chart API: パッケージのロード
{'packages':..}
:
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 Chart API: コールバック
setOnLoadCallback
を生やそう:
class type google_charts = object
method load : js_string t -> packages t -> unit meth
method setOnLoadCallback: (unit -> unit) -> unit meth
end
使い方:
Google Chart API: DataTable
考える:
google.visualization
は Unsafe アクセスするかなDataTable
は プロパティっぽいnew DataTable ()
するので型は_ Js.constr readonly_prop
だろう
Google Chart API: DataTable
大文字の JS フィールドは 頭に _
を付けて OCaml のメソッドに:
google.visualization.*
は late binding されるので関数に:
let google_visualization () : google_visualization t =
Js.Unsafe.pure_js_expr "google.visualization"
使い方:
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
list
やarray
じゃないよjs_string t
とint
が混ざったヘテロ array。Js.Unsafe.any
を使う
Google Chart API: ヘテロ配列
Js.Unsafe.inject
で型を潰す:
Google Chart API: div の取得
ちゃんと HTML に <div id="chart_div">
を書いてね。
Google Chart API: PieChart
PieChart
を生やそう:
new
するからconstr
。引数は HTML要素Html.element t
っぽい。draw
というメソッドがある。DataTable
とJS オブジェクトを取る
Google Chart API: Chart オプション
Class type は宣言しなくてもいいけど:
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 の型を付けれる。
こっから先が たのしい かは人による。
おしまい。