スキップしてメイン コンテンツに移動

OCaml で Web フロントエンドを書く

要旨

フロントエンド開発に Elm は堅くて速くてとても良いと思う。昨今の Flux 系アーキテクチャは代数的データ型と相性が良い。ところで工数を減らすためにはバックエンドも同じ言語で書いてあわよくば isomorphic にしてしまいたいところだが、Elm はバックエンドを書くには現状適していない。

OCaml なら js_of_ocaml でエコシステムを丸ごとブラウザに持って来れるのでフロントエンドもバックエンドも無理なく書けるはずである。まず The Elm Architecture を OCaml で実践できるようにするため Caelm というライブラリを書いている。俺の野望はまだまだこれからだ (未完)

Elm と TEA について

Elm というプログラミング言語がある。いわゆる AltJS の一つである。 ミニマリスティクな ML 系の関数言語で、型推論を持ち、型クラスを持たず、例外機構を持たず、変数の再代入を許さず、正格評価され、代数的データ型を持つ。 言語も小綺麗で良いのだが、何より付属のコアライブラリが体現する The Elm Architecture (TEA) が重要である。

TEA は端的に言えば Flux フロントエンド・アーキテクチャの変種である。同じく Flux の派生である Redux の README に TEA の影響を受けたと書いてあるので知っている人もいるだろう。 ビューなどから非同期に送信される Message (Redux だと Action) を受けて状態 (Model; Redux だと State) を更新すると、それに対応して Virtual DOM が再構築されビューがよしなに再描画され人生を書き換える者もいた——という一方向の流れはいずれにせよ同じである。 差異はオブジェクトではなく関数で構成されていることと、アプリケーション外部との入出力は非同期メッセージである Cmd / Sub を返す規約になっていることくらいだろうか。

後者は面白い特徴で、副作用のある処理はアプリケーションの外で起きて結果だけが Message として非同期に飛んでくるので、内部は純粋に保たれる。つまり Elm アプリケーションが相手にしないといけない入力は今現在のアプリケーションの完全な状態である Model と、時系列イベントである Message の二種類の不変データ型のみである。

もちろんこれは Redux も (ミドルウェアを使えば非同期処理も含めて) 同様だが、このアーキテクチャでアプリケーションが大規模化していくと Message の種類数は十から千程度のオーダに増加していく。状態更新関数 (update; Redux だと Reducer) はそのすべてに応じて Model を更新しなければならない。対応に抜けがあったり Message のペイロードに存在しない値を参照したりすれば即ち実行時エラーである。 JavaScript で書いた状態更新関数が実行時エラーを起こさないためには、プログラマの非常に精緻な注意力か網羅的なテストが必要になる。 一方 Elm には代数的データ型があるので Message を Union Type として定義することができる。コンパイラは状態更新関数の網羅性を静的に検証し、プログラマに決して足を撃たせない。

公平のために言うと、TypeScript であれば同じく Union Type を持っているので少々冗長だが Discriminated Unions として定義できる。確認していないが FlowType の型アノテーションでも恐らく同じことができるだろう。 じゃあ TypeScript で良いかというと、構文の親しみ易さや型推論の貧弱さ (ポジティヴに言い換えるとコードのドキュメント性の高さ)、言語的には可能だが Redux が規約で禁じている事項を守るコストといった要素のトレードオフである。逆に言うと Elm は TEA から逸脱できないように出来ている。

ともかく、代数的データ型と不変データ構造を持った言語は Flux 系のフロントエンド・アーキテクチャと相性が良いと言えるだろう。

Isomorphic

Isomorphic JavaScript というバズワードがある。要するにフロントエンドもバックエンドも同じアプリケーション・ソースコードを共有しましょうという提唱だと理解しているが、これはサーバサイドレンダリングの利点に加えて、フォームの検証器や API レスポンスのデータ構造など明らかに共用できる実装の二度手間や齟齬をなくすことができるから単純で良い考えである。 特に趣味やなんかで一人でアプリケーション全体を書くなら工数は減らしたいところだろう。

問題は "Isomorphic" ではなく "JavaScript" の方で、それが Web ブラウザの Lingua Franca である以上 JavaScript あるいは AltJS でしか実践できないのは仕方がないのだが、サーバサイドでは JavaScript は選択肢の一つに過ぎないという点である。node.js のイベント駆動モデルが C10K 問題の解決策としてもてはやされた時期はあるが、これはプラットフォームの特性であって言語の強みとは言い難いし、今となってはイベントループのライブラリはどの言語にもある。

先述の通り、フロントエンドを今時のアーキテクチャで書くなら静的型付で代数的データ型を持つ関数言語を選びたい。これらの特徴はバックエンドでも品質と開発効率に寄与するだろう。

OCaml と js_of_ocaml について

OCaml は ML の方言である。Standard ML との差は日本語族における琉球語と吉里吉里語くらいの違いである。

なんだかんだ ML なのであまり小難しいこともなく、hello world するのに IO モナドは要らないし式の評価戦略は正格だし可変データ構造もあるし Pascal 式の for ループもある。怖くない。 関数言語の中では比較的メジャで、何故か Facebook とか金融業界で産業利用されているので開発環境も一通り整備されており、パッケージマネージャとか REPL とかライブラリ・ブラウザとか Emacs の編集モードとかいったものがある。 OPAM の中央レポジトリには約 1,700 個のパッケージが登録されている。Hackage は約 11,500 個、NPM は公称 475,000 個なのでパッケージの粒度はさておき数では大分見劣りするが、rizo/awesome-ocaml を見ると RDBMS や Redis へのバインディングやら Web アプリケーション・フレームワークやら JSON コーデックやら HTML ライタやら HTTP サーバ及びクライアントやら、Web 開発に欲しいものは案外揃っている。

Isomorphic という目論見にとって特に重要なのは js_of_ocaml (jsoo) というコンパイラの存在である。これはその名の通り JavaScript コードを出力するのだが、入力は OCaml ソースコードではない。コンパイル済の OCaml バイトコードである。 これは他の AltJS との大きな違いで、例えば BuckleScript (bs) は OCaml / Reason ソースコードから人間可読な JavaScript を生成するトランスパイラだが、NPM のエコシステムに乗っているので OPAM でインストールしたパッケージは使えない。もちろんソースコードを持ってきてトランスパイルすれば使えるが、普通の OCaml ライブラリは bs 用のビルド設定を用意していないので自分でビルドチェインを作る必要がある。 一方 jsoo なら OCaml のビルドチェインで作成したバイトコードをそのまま JavaScript にコンパイルできる。要するにビルドチェインの最後段に非侵襲的に追加することができるので、バックエンド用にビルドしたライブラリをそのままフロントエンドに流用できる。

念の為に言っておくと、OPAM ではなく NPM ライブラリの利用やビルドチェインとの統合なら bs の方がずっと簡単にできる。OPAM と NPM どちらのエコシステムに乗るかによるトレードオフである。

Caelm

というわけで本題。Caelm は名前の通り OCaml で TEA を実践するために開発中のライブラリである。

実際にこのライブラリで書かれたアプリケーションはデモを参照のこと (デモのリポジトリも。) UI の描画を含めて250行程度の規模である。

実際のところ OCaml で Virtual DOM の操作とか Flux 系のフレームワークを書こうという発想は新しくはない。 少なくとも janestreet/virtual_domLexiFi/ocaml-vdom の2つのライブラリがある。前者は janestreet/incr_dom というライブラリの Virtual DOM として使われており、後者はそれ自身が TEA の実装を持っている。

virtual_dom は異様に速いことで有名な Matt-Esch/virtual-dom のバインディングで、以前は Elm もこれを使っていた。 ocaml-vdom は Virtual DOM も自前で実装している。JavaScript バインディングの書き方に互換性がない js_of_ocaml と BuckleScript の両方で使えるようにしようという目論見があるようだ。

一方で Caelm は React.js を使っている。これの利点はそれが事実上の標準であるという一点である。開発が止まって久しい virtual-dom よりも圧倒的にライブラリやツールのサポートが充実している。 React Component として公開されているコンポーネントは無数にあるので、これらを OCaml で書いたアプリケーションで利用できれば素晴らしい (現時点ではまだできないが、そんなに手間はないと思う。) またイベントの処理が簡単なのも React.js の利点である。

状態管理には FRP を使っている。余談だがこのための OCaml ライブラリの名前も React なので大変にややこしい。 Elm は FRP からアクタ・モデルに移行したというので、最初は協調スレッド (aka. ファイバ) である Lwt を使って同様の実装を試みたのだが、jsoo が bind による末尾再帰をループに展開できないので取り止めた。別に jsoo の最適化がタコなわけではない。ホスト環境が JavaScript なので実行時に解決される関数呼出をジャンプに置き換えるのはどうやっても難しい。実際 Elm の実装を見ても適当な回数処理したら残りをスタックに積んで一度再帰から抜けてやり直す、というような涙ぐましい努力をしている。 アプリケーションの立場からは TEA が破綻しないならランタイムの実装がどうなっていても構わないので、FRP でも問題はないだろう。

使い方

アーキテクチャはそのまんま TEA だが、OCaml なのでファンクタを使った API になっている。状態とビューをそれぞれモジュールとして定義し、ファンクタに作用させることでアプリケーションのモジュールが得られる。

例としてよくあるカウンターを作ってみよう。

まずは状態と可能な操作と更新関数を定義する。状態は現在のカウントを示す int で、可能な操作はカウンタの増加と減少で良いだろう:

module State = struct
  type t = int
  type message = Incr | Decr
  type command = message Lwt.t

  let initial = 0

  let update count = function
    | Incr -> count + 1, None
    | Decr -> count - 1, None
end

これだけ。command 型は使っていないが宣言は必要である。update の結果の型は t * command option なので、第二要素に None をつけている。 initial は初期状態である。必須ではないが初期値が決まっているならこのモジュールに定義しておくのが良い習慣である。

次に UI の描画を行うビューを定義する。このモジュールに必要なものは State.t を受けて描画結果の物理 DOM のルート要素を返す render 関数だけである:

module View = struct
  module Reactjs = Caelm.Reactjs.Make (
    struct
      let scope = Js.Unsafe.global
      let var_name = function
        | `React -> "React"
        | `ReactDOM -> "ReactDOM"
    end)
  module Tyxml = Caelm_reactjs_tyxml.Make (Reactjs)
  module Wrapper = Caelm.Reactjs_wrapper.Make (Reactjs)

  open Tyxml.Html
    
  let render ~send ~container count =
    div [ button ~a:[ a_onclick @@ fun _ -> send State.Incr ] [ pcdata "^" ]
        ; pcdata @@ string_of_int count
        ; button ~a:[ a_onclick @@ fun _ -> send State.Decr ] [ pcdata "v" ]
        ]
    |> to_react_element
    |> Wrapper.render ~container
end

必要なのは render 関数だけだと言ったな。あれは嘘だ。

いや嘘ではないが、JavaScript ライブラリである React.js とのバインディングなのでセットアップはそれなりに面倒である。

まず JavaScript の React / ReactDOM オブジェクトをどこから参照するか指定する。その設定をやっているのが Caelm.Reactjs.Make ファンクタで、この例の場合はグローバル変数 ReactReactDOM からオブジェクトを取得してラッパーを作成する。script 要素などで React / ReactDOM を事前にロードしておくのはプログラマの責任である。 面倒なら require 関数を使う Caelm.Reactjs.Make_with_require もあるのでこちらを使っても良い。(script 要素を書く手間が Browserify でバンドルする手間になるだけだが。)

こうして得た Reactjs モジュールは非常に低レベルで型制約が効かないので、もう一枚抽象を被せる。Caelm_reactjs_tyxml.Make は Virutal DOM を構築する型安全なコンビネータを持つモジュール Tyxml を作っている。render 関数内で使っている div とか button とかである。これらを使って得た値を to_react_element 関数に渡すと React の Virtual DOM に変換される。 2つのボタンには onClick イベントハンドラが設定されている。イベントハンドラは状態を更新するために send で Message を投げている。ちなみにイベント発火時に渡される引数は React.js の Synthetic Event だが、使っていないので _ で潰している。

Virtual DOM が出来たら残るは実際の描画、つまり ReactDOM.render の呼び出しである。Caelm.Reactjs_wrapper.Make で得られたモジュール Wrapperrender 関数が文字通りラッパーになっている。

これで状態の更新の仕方と、状態を与えられたときの UI の描画方法が定義できた。 これらを使ってアプリケーションの状態管理を行うモジュールを生成する:

module App = Caelm_lwt.Make (State) (View)

App は渡された状態とビューを使うように設定された状態管理ランタイムである。

中身を気にする必要は特にない。描画するべき DOM 要素と初期状態を指定して App.run を呼ぶと実行が開始される:

let () =
  let container = Dom_html.getElementById "app-root" in
  let app = App.run ~container State.init in
  Js.export "terminateApp" @@ Js.wrap_callback (fun () -> App.terminate app)

これで "app-root" という id を持つ要素の中にアプリケーションの UI を描画して実行が始まる。 また JavaScript の関数としてエクスポートされた terminateApp をブラウザの開発者コンソールから呼び出すとアプリケーションは応答を止める。

課題

現在のところ大きな課題は二点ある。Cmd と Sub が抽象化し切れていないことと、Virtual DOM 再構築の非効率である。

Elm の場合 Cmd と Sub は Effect Manager という副作用を扱うシングルトンに処理を登録したときに貰えるトークンのようなもので、処理が終わって Message を投げたり失敗した後始末なんかも Effect Manager が行う。

Caelm の場合 Effect Manager の実装をサボっていて、現在のところ Cmd は (通常は Lwt の) スレッド、Sub は React の Event そのものである。 Cmd は有限時間内に何らかの Message を返す必要がある。これはまだ実装でなんとでもなるが、Sub は TEA 的に好ましくない。Event は状態を持つ値だからだ。 つまり解決するには API を変えないといけないので、現時点で Sub を使うのは止めておいた方が良い。

Virtual DOM の非効率については簡単で、再描画時に毎回 Virtual DOM 全体を再構築しているので無駄があるということである。 正直なところ素直に FRP していれば済む話で、実際前述の incr_dom などは (FRP ではなくて SAC 即ち自己適応計算だが) Incr.t でラップされた値を持ち回る API になっているのだが、Sub と同じで状態を持った値が API に露出するのは TEA の観点から良くないので悩ましい。

まとめ

Web フロントエンドに関数言語を適用する機運が見えてきた。これに乗じて Isomorphic OCaml の野望を達成すべく、その取っかかりとして作成している Caelm ライブラリを紹介した。 まだ API とパフォーマンスに課題はあるが動いているし、これを以ってフロントエンド・プログラマを名乗っても多分良かろうと思う。

コメント

このブログの人気の投稿

救急外来にかかったときの記録

子どもの頃にかかった記憶はあるが自分で行ったことはなかったのでメモしておく。 先日怪我をした。より具体的に云うとランニング中に転倒し顎を地面に叩きつけた。深夜の12時ごろの話である。 その時点ては両手の擦傷が痛いとか下顎の間接が痛いとか奥歯のセラミックが割れなくて幸いだったといった程度だが、マスクを外して見るとなにやら下部に血がついている。 顎にも擦傷があるのかとうんざりしながら歩いて帰り、血の滲んだマスクを捨てて傷口を洗おうとしたところで皮膚が割けて肉が見えているのに気付いた。 一瞬顔が青くなったが単身なので倒れるわけにはいかない。幸い血は固まっていてそれほど出血していないし、先程まで運動していたからかあまり痛みもない。 この時点で明白な選択肢は3つあった。即ち: 救急車を呼ぶ 自力で病院へ行き救急外来を受診する 応急処置して朝になったら近場の医院を受診する である。まず 3 は精神的に無理だと悟った。血も完全には止まっていないし、痛みだしたら冷静に行動できなくなるだろう。 1 はいつでも可能だったが、意識明瞭で移動にも支障がない状態では憚られた。救急車が受け入れ先病院を探すのにも時間がかかると聞く。 結局とりあえず 1 をバックアップ案とし、2 の自分で連絡して病院へ向かうことにした。まずは病院探しである。このときだいたい 00:30 AM。 最初に連絡したのは最寄りの都立病院の ER だった。ここならタクシーで10分もかからない、のだが、なんと ER が現在休止しているとの回答だった。そんなことがあるのかと驚愕したがどうしようもない。 近場に形成外科の救急外来の開いている病院はないか尋ねたところ 消防庁の相談センター の電話番号を案内された。 ここで4つの病院を紹介された。余談だが相談の対応は人間だが番号の案内は自動音声に切り替わるので録音の用意をした方が良い (一応2回くり返してくれる。) いずれも若干遠くタクシーで2、30分かかるが仕方がない。最初に連絡した最寄りの病院はその日形成外科の当直医師がいなかった。二件目でトリアージの質問をされ、受け入れ可能とのことだったので受診先が決定。このとき 00:45 AM。 診察時に脱ぎ易い服に着替え (このときまでランニングウェアだった)、健康保険証を持って病院へ向かう。ガーゼがないのでマス...

js_of_ocaml の使い方

js_of_ocaml (jsoo) は Ocsigen が提供しているコンパイラである。その名の通り OCaml バイトコードから JavaScript コードを生成する。 これを使うことで OCaml で書いたプログラムを Web ブラウザや node.js で実行することができる。 インストール 単に OPAM を使えば良い: $ opam install js_of_ocaml js_of_ocaml-ocamlbuild js_of_ocaml-ppx バージョン 3.0 から OPAM パッケージが分割されたので、必要なライブラリやプリプロセッサは個別にインストールする必要がある。 とりあえず使うだけなら js_of_ocaml と js_of_ocaml-ppx の二つで十分。後述するように OCamlBuild でアプリケーションをビルドするなら js_of_ocaml-ocamlbuild も入れると良い。 これで js_of_ocaml コマンドがインストールされ、OCamlFind に js_of_ocaml 及びサブパッケージが登録される。 コンパイルの仕方 以下ソースファイル名は app.ml とし、ワーキングディレクトリにあるものとする。 手動でやる場合 一番安直な方法は、直接 js_of_ocaml コマンドを実行することである: $ # バイトコードにコンパイルする。js_of_ocaml.ppx は JavaScript オブジェクトの作成や操作の構文糖衣を使う場合に必要 $ ocamlfind ocamlc -package js_of_ocaml,js_of_ocaml.ppx -linkpkg -o app.byte app.ml $ # 得られたバイトコードを JavaScript にコンパイルする $ js_of_ocaml -o app.js app.byte OCamlBuild を使う場合 OCamlBuild を使う場合、.js 用のビルドルールを定義したディスパッチャが付属しているので myocamlbuild.ml でこれを使う: let () = Ocamlbuild_plugin . dispatch Ocamlbuild_js_of_ocaml . dispatcher $ # app.ml -...

Project Euler - Problem 35

問題 原文 How many circular primes are there below one million? 日本語訳 100万未満の巡回素数は何個か? 解答 回転させた数値がすべて素数ということは、すべての桁が奇数でなければいけません(ただし2を除く)。 追記 匿名氏にコメントでご指摘頂いたのでコードを一部修正しました。 いずれかの桁に5がある場合も、回転させると必ず5の倍数が現れるので除外できます。 もっと追記 前の修正に間違いが入っているのをご指摘頂いたので修正しました。 5自体は素数なので、巻き添えで除外してはいけません。 #!/usr/bin/env perl use strict; use warnings; use feature qw/say state/; use List::MoreUtils qw/all none/; sub is_prime($) { state %memos; my $n = shift; return 0 if $n < 2; return 1 if $n == 2; return 1 if $n == 3; return $memos{$n} if exists $memos{$n}; $memos{$n} = none { $n % $_ == 0 } 2 .. sqrt $n; } sub rotate($) { my $n = shift; substr($n, 1) . substr($n, 0, 1); } sub rotations($) { my $n = shift; my %seen = ($n => 1); $seen{$n} = 1 until exists $seen{$n = rotate $n}; keys %seen; } sub is_circular_prime($) { state %memos; my $n = shift; return 0 if $n =~ /[024568]/ and $n != 2 and $n != 5; return $memos{$n} if exists $memos{$n}; my ...

Project Euler - Problem 3

問題 原文 What is the largest prime factor of the number 600851475143 ? 日本語訳 600851475143 の素因数のうち最大のものを求めよ。 解答 いきなり難易度が上がりました。素因数分解の問題です。 結局のところ片っ端から割ってみるしかないのですが、探索範囲を狭めることはできます。 正の整数の範囲で考えると、l = m * nかつm >= nであれば、floor(sqrt(l)) >= nであることが直感的に分かります。ここでfloorは実数を0の方向に丸めて整数にする関数、sqrtは平方根を返す実数関数です。 nが分かれば、mは除算で求められます。つまりlが与えられた時、それを2つの因数に分解するのに探索する範囲は高々2 .. floor(sqrt(l))で十分ということです。 2つに分けられたならこっちのもの、得られたmとnを再帰的に分解して、その結果を合わせれば素因数分解の結果が得られます。 アルゴリズムを書き下すと次のようになります: n <- floor(sqrt(l))とする。 n = 1なら、これ以上は分解できない(lは素数である)のでlをそのまま返す。 nがlの因数なら、m <- l / nとし、mとnに対してこのアルゴリズムを再帰的に適用し、その結果を連結して返す。 因数でないなら、n <- n - 1とし、2.から繰り返す。 これで計算は可能です。しかしもう少し最適化を検討してみましょう。 偶数は一般に2mの形で表せます。同様に奇数は2m + 1と書けます。 偶数同士の積は2m * 2n = 4mnとなり、l = 2mnとおけば2lなのでこれも偶数です。 奇数同士の積は(2m + 1)(2n + 1) = 4mn + 2(m + n) + 1であり、l = 2mn + m + nとすると2l + 1なのでこれも奇数です。 偶数と奇数の積は2m(2n + 1) = 4mn + 2mなので、l = 2mn + mとおいて2lなので偶数です。 つまり、奇数の因数は必ず奇数のみから成るということが分かります。先ほどのアルゴリズムでは、4.のステップでn <- n - 1としていました。...

Perl の新 class 構文を使ってみる

Perl 5 のオブジェクト指向機能は基本的には Python の影響を受けたものだが、データを名前空間 (package) に bless する機構だけで Perl 4 以来の名前空間とサブルーチンをそのままクラスとメソッドに転換し第一級のオブジェクト指向システムとした言語設計は驚嘆に価する。 実際この言語のオブジェクトシステムは動的型付言語のオブジェクト指向プログラミングに要求されるおよそあらゆる機能を暗にサポートしており、CPAN には Moose を筆頭とした屋下屋オブジェクトシステムが複数存在しているがその多くは Pure Perl ライブラリである。つまり「やろうと思えば全部手書きで実現できる」わけである。 そういうわけで Perl のオブジェクト指向プログラミングサポートは機能面では (静的型検査の不在という現代的には極めて重大な欠如を除けば) 申し分ないのだが、しかし Moose その他の存在が示しているように一つ明らかな欠点がある。記述の冗長さだ。 コンストラクタを含むあらゆるメソッドは第一引数としてレシーバを受ける単なるサブルーチンとして明示的に書く必要があるし、オブジェクトのインスタンス変数 (a.k.a. プロパティ / データメンバ) は bless されたデータに直接的ないし間接的に プログラマ定義の方法 で格納されるためアクセス手段は実装依存である。これはカプセル化の観点からは望ましい性質だが、他者の書いたクラスを継承するときに問題となる。ある日データ表現を変更した親クラスがリリースされると突然自分の書いた子クラスが実行時エラーを起こすようになるわけだ。 そうならないためにはインスタンス変数へのアクセスに (protected な) アクセサを使う必要があるのだが、そのためには親クラスが明示的にそれらを提供している必要があるし、そもそも Perl にはメソッドのアクセス修飾子というものがないので完全な制御を与えるならばオブジェクトの内部状態がすべて public になってしまう。 そのような事情もあり、特にパフォーマンスが問題にならないようなアプリケーションコードでは Moose のようなリッチな語彙を提供するオブジェクトシステムを使うことが 公式のチュートリアルでも推奨 されてきた。Perl コアのオブジェクトシステムの改良は...