OCaml でウェブフロントエンドを書きたいことがありますよね? 皆さんありますよね?
そういう時、どうしましょう。
以前、私は ReScript や ReasonML を使っていたのですが、今回は Js_of_ocaml を使って見ようと思います。
なお、 ReScript というのはその昔 BuckleScript と呼ばれていたものです。
今回の練習は以下のレポジトリで行いました。記事内で使ったコードは全てレポジトリ内にあります。
https://github.com/cedretaber/exercise_jsoo
Js_of_ocaml
Js_of_ocaml は OCaml を JS に変換してくれるツールです。
ReScript も同じく OCaml を JS にしてくれるのですが、以下のような違いがあります。
- ReScript は OCaml の中間コードから JS に変換するのに対して、 Js_of_ocaml はバイトコードを JS に変換する
- ReScript には JS 風の表層構文も用意されている
- ReScript は出力された JS の読みやすさに気を遣っているのに対し、 Js_of_ocaml は JS コードの可読性がかなり低い(印象論)
- ReScript は JS との連携が取りやすいのに対し、 Js_of_ocaml は OCaml との連携が取りやすい(個人の感想です)
標準で推奨されているパッケージマネージャを見ても、 ReScript が npm であり Js_of_ocaml は opam であるように、 ReScript が JS 方面から作られた AltJS なのに対して、 Js_of_ocaml は OCaml コードをウェブフロントエンドで動かすためのツールといった位置付けでしょう。
例えば Coq という有名な定理証明支援系がありますが、これは OCaml で書かれており、これをウェブフロントエンドで動かすことができる jsCoq には Js_of_ocaml が使われています。
要するに、これを使ってウェブフロントエンドをガリガリ書くためというよりは、既存の OCaml 資産をウェブでも利用しよう、みたいな場合に活用できるツールなのですね。多分。
とはいえ、単に OCaml を JS に変換するだけではなく、 JS 側の機能やウェブフロントエンドの機能を取り扱う方法も提供されています。例えば DOM を弄ったり、 JS のオブジェクトを扱ったり、 WebSocket や Web Workers を操作したり、です。
最近は wasm 用の機能が取り込まれたりしています。
https://github.com/ocsigen/js_of_ocaml/pull/1724
そんな Js_of_ocaml なのですが、ちょっと使ってみようとしたら以下のような面倒な問題がありました。
- fetch のインターフェイスが提供されていない
- Promise のインターフェイスが提供されていない
まず、当たり前ですが、 Js_of_ocaml には JS の関数やクラスをラップする汎用的な手段が提供されています。
なので、 fetch だろうが Promise だろうが自分でラップしてやれば( OCaml 部分に限れば)安全に取り扱うことができますし、 JS の機能にもアクセスすることができます。
とはいえ、ちょっとオープンな API を使ってゴニョゴニョやってみるかー、という時に fetch や Promise の定義から始めなければならないのは中々面倒なものがあります。
あと、外部機能の呼び出しはどうしても Unsafe な領域になるので、そこは気を付けて繋ぎ込む必要があるのでしょう。
で、ちょっと試してみようとしたら意外と面倒だったので、作業記録をメモしておきたいと思います。
調べながら試した形なので、他にもっと良いやり方があったり、そもそも方法が間違っていたりするかもしれません。ご容赦ください。
dune を使ったプロジェクトの準備、依存ライブラリの追加などは省略します。
公式ドキュメントを参照ください。
Console モジュール
まず、 JS の関数の呼び出し方法を確認しつつ、デバッグのために console.log を使えるようにしましょう。
と言っても、難しいことはありません。
open Js_of_ocaml
let log any : unit =
Js.Unsafe.fun_call (Js.Unsafe.js_expr "console.log") [|Js.Unsafe.inject any|]
JS の関数を呼び出すには Js.Unsafe.js_expr
関数を利用します。第一引数に渡すのは「関数名」ではなくて関数そのものなので、 Js.Unsafe.js_expr "console.log"
で作ってやる必要があります。思い切り Unsafe と書いてあることからも分かる通り、例えば console
の値が変わっていたら大変なことが起こりそうですね(他人事)。
Js.Unsafe.inject
は型を合わせるための関数ですが(全てを any 型にしてくれます)、これも Unsafe なので、型が間違っていてもコンパイル時に気付くことができずランタイムに例外が発生します。まぁこの辺りは、 interop という都合上やむを得ないでしょう。
これだけでも、 JS の関数の呼び出しはそこまで難しくないことと、型が静的に決まっている世界から決まっていない世界へ移動する際に(またはその逆の場合に)安全性が失われることが何となく感じられるかと思います。
Promise モジュール
fetch を作る前に Promise を作ります。 fetch の返り値は Promise だからですね。
今回は必要な機能だけを取り込むこととします。
参考: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
open Js_of_ocaml
type 'a t
let then_ (f : 'a -> 'b) (promise : 'a t) : 'b t =
Js.Unsafe.meth_call promise "then" [|Js.Unsafe.inject f|]
let bind (f : 'a -> 'b t) (promise : 'a t) : 'b t =
Js.Unsafe.meth_call promise "then" [|Js.Unsafe.inject f|]
Promise の型を type 'a t
で作っています。中身は JS の Promise そのままなので、 OCaml では実体を定義しません。
Js.Unsafe.meth_call
はメソッド呼び出しを行うための関数です。第一引数に JS のオブジェクトを、そして第二引数にメソッド名を OCaml の文字列で渡し、第三引数がそのメソッドの引数です。
さて、2つの関数ですが、呼び出しているメソッドは同じ then
ですね。しかしどちらも関数名は then
ではありません。これについて説明します。
名前ですが、 then
は OCaml のキーワードなので関数名として使うことができません。よって、末尾にアンダースコアを付けています。 map
とかの名前に変えてあげても良いかもしれませんが、そのあたりは好みの問題でしょうか。
同じメソッドに対して関数が2つある理由ですが、 JS の then
の性質の都合です。
Promise.prototype.then
メソッドは、渡されたコールバック関数の返値によって挙動が変わります。返り値が Promise 以外の場合は、その値を Promise で包んだ値を返すのですが、返り値が Promise の場合は、その Promise の解決を待ってから履行・拒否の結果をそのままこの Promise の値とする、みたいな動きとなります。
分かりやすくいうと、 map/bind あるいは map/flat_map の両方の機能を併せ持つのですね。
OCaml には関数オーバーロードの機能が無いので、こういう場合は関数を分けなければなりません。よって、別名で bind/flat_map の方の then
を定義しています。
さて、今回は Promise をこのように OCaml で表現したのですが、 Js_of_ocaml では JS のオブジェクトをラップする方法も提供されています。
しかし、 Promise を Js_of_ocaml における JS のオブジェクトで表現してみたところ、型が複雑になりすぎて私の腕前ではコンパイルを通せなかったので、今回は上のようなやり方で連携しています。
また、 Promise.prototype.then
には第二引数があったり、また関数以外を渡せたりもするのですが、今回は省略しています。
エラーハンドリングなどもやっていません。たとえば Promise.prototype.catch
メソッドでエラーを処理するのは JS の Promise を使う上では基本ですが、今回は完全に無視しています。
きちんと処理したい方は、関数定義を追加してみましょう。
Fetch モジュール
fetch を呼び出すためのモジュールを作ります。
参考: https://developer.mozilla.org/ja/docs/Web/API/Window/fetch
open Js_of_ocaml
let fetch (url : string) : _ Js.t Promise.t =
Js.Unsafe.fun_call (Js.Unsafe.js_expr "fetch") [|Js.Unsafe.inject url|]
let fetch_json (url : string) : _ Js.t Promise.t =
fetch url |> Promise.bind (fun res -> res##json)
上記ドキュメントを読んで分かる通り、この fetch
は Window インターフェイスのメソッドなのですが、ここでは Js.Unsafe.fun_call
を使い、レシーバ無しでメソッドを呼び出しています。
MDN Web Docs のサンプルでもそういう使い方しているし、雑に作るならこれでも良いかな、と思いまして……。
本来であれば、 Response 型を定義してあげて fetch の返り値をそれにすれば良いのですが、ちょっと型が上手く合わず、全て型推論に任せてしまっています。
これだと存在しないメソッドなども呼べてしまって良くないのですが、ここの型パズルが難しくて手に負えませんでした……。
というか正直、この辺りのやり方は探り探りというか、型が滅茶苦茶複雑になってしまってコンパイルが通らなくて泣いてしまったので、あまり仕組みを理解せず書いている部分があります……。
お気づきでしょうが、 fetch
にも本当は第二引数以降があります。多彩なオプションを取ることができます。 URL を文字列で渡すのではなく Request オブジェクトを渡すこともできます。
今回はそういう多様性をバッサリと切り捨てて、必要な機能だけを絞って実装しています。
是非とも他の引数も取れるように改良してみてください。
JS のオブジェクトを OCaml の値に変換する
これで fetch
を使ってオンラインからリソースを取得できるようになりました。使ってみましょう。
いい感じの JSON を返してくれる JSONPlaceholder というサービスがあるので、ありがたく使わせてもらいます。 TODO アイテムみたいなものを返すエンドポイントを利用します。
さて、一つ気を付けなければならないのは、ブラウザで動かす以上「 wait して非同期処理の終了を待つ」はできない、ということです。 Promise.prototype.then
メソッドにコールバックを渡して非同期処理を行っていくのが基本です。まぁ継続渡しスタイルみたいなものですね。
天下り的な説明になってしまって恐縮なのですが、まずは JSON を JS のオブジェクトにパースしてから OCaml の値に変換する方法を見てみましょう。
let fetch_todo_as_json _ =
Fetch.fetch_json todo_url
|> Promise.then_ (fun json ->
Console.log json;
parse_todo json
)
動作確認のために、 Console.log を使ってログを出しています。
fetch
の使い方は簡単ですね。
返り値の Promise を then_
関数に渡して、その中で値を処理しています。
JSON から組み立てられた JS のオブジェクトを変換する処理は以下のような感じです。
module Todo = struct
type t = {
user_id : int;
id : int;
title : string;
completed : bool;
}
let make ~user_id ~id ~title ~completed = { user_id; id; title; completed }
let to_string = function
| { user_id; id; title; completed } ->
Printf.sprintf "userId: %d, id: %d, title: %s, completed: %b" user_id id title completed
end
let parse_todo json =
match
Js.Optdef.to_option json##.userId,
Js.Optdef.to_option json##.id,
Js.Optdef.to_option json##.title,
Js.Optdef.to_option json##.completed
with
| Some user_id, Some id, Some title, Some completed ->
Result.Ok (
Todo.make
~user_id:(Js.to_int32 user_id |> Int32.to_int)
~id:(Js.to_int32 id |> Int32.to_int)
~title:(Js.to_string title)
~completed:(Js.to_bool completed)
)
| _ ->
Result.Error "Invalid JSON"
小さなレコードを作って、それに変換するようにしています。
JS のオブジェクト( <..> Js.t
型)のフィールドにアクセスするには obj##.fieldName
のように書きます。ちなみにメソッド呼び出しは obj##methodName
です。
いくらなんでもオブジェクトのフィールドを一つ一つ確認して変換するのは面倒……と思ってしまうと思いますが、流石にこのあたりは自動化してくれるライブラリがあるようです。
標準の Json モジュールでも Unsafe な変換が行えるほか、 Deriving_Json モジュールを使えば安全な変換も行えるようです(試してないのですが……)。
また、 JS のオブジェクトを経由する以外に、 JSON 文字列から OCaml の JSON ライブラリを利用してパースする方法もあります。
どちらが良いのかはケースバイケースでしょうか。
そちらの方法についてはこの記事では解説しませんが、レポジトリにはコードがあるので気になる方は見てみてください。 Yojson ライブラリを使っています。
OCaml の値として取り扱えるようになったので、この後はどう使っても自由です。
処理した結果をブラウザに表示する方法ですが、それに関してもこの記事では省略します。公式サンプルなどに沢山例があるのでご参照ください。
感想など
こんな感じで、 JS の関数やメソッドを使うだけなら、それ程難しいことをしなくても大丈夫です。
安全性についてはちょっと気を付けないといけませんが、それも例外を考慮して丁寧に型を付けてあげればある程度何とかなるんじゃないですかね。 TypeScript の型などが大いに参考になるかと思います。
「その TypeScript の型から自動で型定義を生成できないの?」とも思うのですが、そういうツールもあるのですかね……? ReScript にはその手のツールがあるのを知っているのですが。
さて、使ってみた感想なのですが、最初に書いた通り「 Js_of_ocaml は OCaml で書かれたコードを JS で動かすためのライブラリ」という印象を強くしました。
JS との interop や JS のオブジェクトを取り扱うのは、難しくはないのですが、あまり複雑なことをしようという気分にはならないですね……。
この点に関しては、やはり ReScript や ReasonML が圧倒的にやりやすいと感じました。 JS の関数やメソッドに型を付けるのも分かりやすいですし、 JS のオブジェクトの取り扱いも自然です。なにより、出力される JS が読みやすいので、出力結果を見ながら調整できます。
なので、もしフロントエンドをガッツリと OCaml で書きたいのであれば、「ブラウザに表示する部分や JS の関数を利用する部分は ReScript/ReasonML で書く」「既存の OCaml コードを利用した処理は Js_of_ocaml で書く」みたいな使い分けをすれば、ストレス少なく開発できたりするんじゃないでしょうか。
メイン処理部分を Js_of_ocaml で書いて Web Workers で動かし、 ReScript/ReasonML で書いたフロントから適宜それを呼ぶ、みたいな使い方するとカッコいいかも?
(同じレポジトリの中に ReScript/ReasonML と Js_of_ocaml を混在させるとビルドが大変なことになりそうではありますが。)
まぁ複雑で実用的(?)な使い方はさて置き、ちょっと API 叩いて何か処理してブラウザに表示する、程度であれば Js_of_ocaml でざくざく書けるので、ウェブフロントエンドを OCaml で記述することに使命感を燃やしている皆さんは是非とも試してみてください。