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

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 とパフォーマンスに課題はあるが動いているし、これを以ってフロントエンド・プログラマを名乗っても多分良かろうと思う。

コメント

このブログの人気の投稿

Perl 7 より先に Perl 5.34 が出るぞという話

Perl 5 の次期バージョンとして一部後方互換でない変更 (主に間接オブジェクト記法の削除とベストプラクティスのデフォルトでの有効化) を含んだメジャーバージョンアップである Perl 7 がアナウンスされたのは昨年の 6 月 のことだったが、その前に Perl 5 の次期周期リリースである Perl 5.34 が 5 月にリリース予定 である。 現在開発版は Perl 5.33.8 がリリースされておりユーザから見える変更は凍結、4 月下旬の 5.33.9 で全コードが凍結され 5 月下旬に 5.34.0 としてリリース予定とのこと。 そういうわけで事前に新機能の予習をしておく。 8進数数値リテラルの新構文 見た瞬間「マジかよ」と口に出た。これまで Perl はプレフィクス 0 がついた数値リテラルを8進数と見做してきたが、プレフィクスに 0o (zero, small o) も使えるようになる。 もちろんこれは2進数リテラルの 0b や 16進数リテラルの 0x との一貫性のためである。リテラルと同じ解釈で文字列を数値に変換する組み込み関数 oct も` 新構文を解するようになる。 昨今無数の言語に取り入れられているリテラル記法ではあるが、この記法の問題は o (small o) と 0 (zero) の区別が難しいことで、より悪いことに大文字も合法である: 0O755 Try / Catch 構文 Perl 5 のリリース以来 30 年ほど待たれた実験的「新機能」である。 Perl 5 における例外処理が特別な構文でなかったのは予約語を増やさない配慮だったはずだが、TryCatch とか Try::Tiny のようなモジュールが氾濫して当初の意図が無意味になったというのもあるかも知れない。 use feature qw/ try / ; no warnings qw/ experimental::try / ; try { failable_operation(); } catch ( $e ) { recover_from_error( $e ); } Raku (former Perl 6) だと CATCH (大文字なことに注意) ブロックが自分の宣言されたスコープ内で投げられた例外を捕らえる

BuckleScript が ReScript に改称し独自言語を導入した

Via: BuckleScript Good and Bad News - Psellos OCaml / ReasonML 文法と標準ライブラリを採用した JavaScript トランスパイラである BuckleScript が ReScript に改称した。 公式サイトによると改称の理由は、 Unifying the tools in one coherent platform and core team allows us to build features that wouldn’t be possible in the original BuckleScript + Reason setup. (単一のプラットフォームとコアチームにツールを統合することで従来の BuckleScript + Reason 体制では不可能であった機能開発が可能になる) とのこと。要は Facebook が主導する外部プロジェクトである ReasonML に依存せずに開発を進めていくためにフォークするという話で、Chromium のレンダリングエンジンが Apple の WebKit から Google 主導の Blink に切り替わったのと似た動機である (プログラミング言語の分野でも Object Pascal が Pascal を逸脱して Delphi Language になったとか PLT Scheme (の第一言語) が RnRS とは別路線に舵を切って Racket になったとか、割とよくある話である。) 公式ブログの Q&A によると OCaml / ReasonML 文法のサポートは継続され、既存の BuckleScript プロジェクトは問題なくビルドできるとのこと。ただし現時点で公式ドキュメントは ReScript 文法のみに言及しているなど、サポート水準のティアを分けて ReScript 文法を優遇することで移行を推進していく方針である。 上流である OCaml の更新は取り込み、AST の互換性も維持される。将来 ReScript から言語機能が削除されることは有り得るが、OCaml / ReasonML からは今日の BuckleScript が提供する機能すべてにアクセスできる。 現時点における ReScript の

多分週刊チラシの裏 (Mar 23, 2021 - Mar 27, 2021)

Intel プロセッサのマイクロコードを変更する非公開命令が発見される 今日のプロセッサは複雑な命令を単純な回路で実装したりバグの修正を容易にするため、1 個の命令でも実際にはプロセッサ内部に格納されたマイクロプログラムを実行するようになっていることが多い。 かつて浮動小数点演算器のバグで Pentium をリコールする羽目になった Intel も例外ではないのだが、Intel が電子署名したマイクロコードでなくとも適用できる非公開命令が発見されたという報告。 ただし無条件ではなく、命令自体はユーザモードでもデコードされるがプロセッサが特定の “Unlocked State” にないときは未定義命令として処理されるらしい。 MSKK の週休三日トライアルで生産性が四割向上 2019 年の記事。週休三日制を導入する事業所は中小を中心に増加しているが、Microsoft の日本法人であるマイクロソフト株式会社 (MSKK) が “Work Life Choice Challenge” と称して 2019 年夏に実施した金曜日を休日とする試験的措置において、前年同月比 40 % の生産性向上が見られたとのこと。 同施策は休日の追加に加えて会議時間短縮の奨励、またメッセージングアプリによる会議自体の代替なども含んでいる。 2015 年の電通における過労自殺事件に国際的な耳目が集まって以降、日本は「過労死」に象徴される長時間労働の是正に取り組んでおり、MSKK のこの措置は将に時節を得たものであったと言える。 r/WallStreetBets からゴリラ保護基金へ多額の寄付 「みんなで株価吊り上げて食い付いたヘッジファンド釣ろうぜ」という一種の祭で GameStop の株価が高騰したのは今年の 1 月だが、その震源地であった Reddit の WallStreetBets (WSB) コミュニティからマウンテンゴリラ保護のための基金である The Dian Fossey Gorilla Fund International に 350,000 USD の寄付があったとのこと。 ところで何故ゴリラかというと「猿の惑星」に倣って WSB コミュニティ内で同志を猿 (ape) と称していたからとか。異説に「猿みたいに株を買うのにキーボードを連打しているから」とも。

多分週刊チラシの裏 (Feb 28, 2021 - Mar 22, 2021)

JavaScript 開発者が如何にして TypeScript 嫌いから TypeScript ファンになったか 気軽な読み物。型宣言の冗長さとジェネリクスなどの複雑性を嫌って (選択肢にあれば) JavaScript の方を選んできた筆者が TypeScript しか選べない職場に移って数ヶ月後にはすっかりファンになっていたという話。 理由は月並で「『不可能な状態を不可能にする』Union Type と網羅性チェック」「コンパイル時型検査によるエラーの早期検出」「リッチな IDE 支援」の 3 本。理由がそれだけなら個人的には Flow か Elm を進めたいところではある。 NASA の最新火星ローバーが搭載するプロセッサは 1998 年の iMac と同じ NASA が Mars 2020 ミッションのために送り出し、先月火星表面に着陸した最新かつ過去最大のローバーである Perseverance の話。 2021 年に活動を開始したこのハイテク・ガジェットのメインプロセッサは PowerPC 750 であるとのこと。1998 年発売の初代 iMac が搭載していた “G3” プロセッサといえば分かり易いだろう。 もちろん民生品そのものではなく、-55 - 125 ℃ の気温と 200,000 - 1,000,000 Rad の放射線に耐える特別仕様の BAE Systems RAD750 である。ちなみに「火星で自撮り」という快挙を成し遂げたのち現在も活動中の先代 Curiosity も同じものを搭載している。動作周波数 110 - 200 MHz、価格は $200,000 程度とのこと。 Internet Archive Infrastructure 過去の Web サイト、書籍、ビデオに音楽からクラシックソフトウェアまでインターネットに公開されたあらゆるデータを収集・保存する Internet Archive のインフラ紹介ビデオ。 クラウドは一切使っておらず、自前のベアメタルサーバ 750 台に接続されたストレージはシステム全体で 200PB とのこと。保存されるデータは現在のところ年 25 % 以上増大しており、四半期で 5 - 6 PB 規模だという。 Semantic Versioning はお前を救わない 「ある API