LunarMLとJavaScriptの連携について

久しぶりにLunarMLの話題です。LunarMLは、私が作っているStandard MLコンパイラーです。

最近は、JavaScriptとの連携に関する機能を追加しています。Webで使えるようにするのと、Promise/async/await周りの統合をいい感じにすることを目標にしています。

Web向けのコード出力とWeb API

従来のLunarMLはNode.js向けのJavaScriptコードを出力できましたが、Web向けのコードを出せるようにするのは難しいことではありません。単に標準ライブラリーを差し替えて、Node.js特有の機能を使わないようにするだけです。その過程で print などの基本的な機能も使えなくなりますが……(Webには標準出力がないので、print した文字列は虚無に消える。console.log は毎回改行するので使いづらい)。

問題はライブラリーの方で、Web向けのコードを出すからには、Web API(ブラウザーが提供してJavaScriptから利用できるAPI)と連携したいです。window, document などのオブジェクトを触ったりしたいです。Web APIにどのような型をつけるかが問題です。

Standard MLはオブジェクト指向ではなく、部分型付けはありません。プロパティーのような概念もありません。Web APIには例えば Node というインターフェースがあって多くのオブジェクトがそれを実装しているわけですが、Standard MLの型でそれをどうやって表現するかが問題です。

この辺はSMLToJsや、Js_of_ocamlを参考にすると良いのかもしれません。必要ならLunarMLの型システムを拡張することも選択肢に入りますが、それは最後の手段にしたいです。

根本的な問題として、単に windowdocument へのバインディングを用意するだけでは関数型という特性を活かせないのではないかという問題があります。関数型言語でWebフロントエンドを書くならば、React等の宣言的UIフレームワークに乗っかるべきではないでしょうか。あるいはThe Elm Architectureがいいのか。やるとしてもLunarML付属ライブラリーではなく、パッケージのような形で提供することになると思いますが。

まあしかし、TypeScriptがWebフロントエンド界の覇者となって久しく、私の限られた時間を投じてLunarMLでそれに挑戦することにどれほどの意味があるのだろうと思います。UIはTypeScriptに任せて、フロントエンドの一部の処理をLunarMLで書く、というような棲み分けが賢いのかもしれません。その場合、TypeScriptからLunarMLの資産に(できれば型付きで)アクセスする仕組みを整える必要がありますが。

LunarMLをWeb向けに使う場合、既存のバンドラー(webpackやvite)とどう統合するかという問題もあります。

Promiseの連携

最近のWeb APIには Promise を返すものがちょいちょいあります。fetch が代表例です。WebAssemblyやWebGPUを Promise を利用します。なので、LunarMLでも Promise をいい感じに扱えると良さそうです。

昔のJavaScriptでは、メソッドチェーンで .then を繋げて Promise を処理していました。LunarMLでも、パイプライン演算子を使えば似たような書き心地が実現できそうです。具体的には、以下のようなコードになります:

fetch (someUrl, options)
  |> Promise.andThen (fn x => ...)
  |> Promise.andThen (fn y => ...)
(* then は Standard ML のキーワードなので代わりに andThen を使う *)
(* Promise.andThen の型は ('a -> 'b promise) -> 'a promise -> 'b promise *)

さて、今のJavaScriptにはasync/await構文があります。async/awaitはそういう構文で、コンパイラーが頑張ってasync/awaitの入ったプログラムを変換します。しかし、LunarMLには限定継続があり、それを使うと文法を拡張しなくてもライブラリーレベルでasync/awaitを実現できます。具体的には、

val async : ('a -> 'b) -> 'a -> 'b Promise.promise
val await : ('a -> 'b Promise.promise) -> 'a -> 'b

という関数をライブラリーで実装できます。これを使うと

val x = await (fetch (someUrl, options))

という風に書けるようになります。awaitが後置じゃないと許せない人は

val x = fetch (someUrl, options) |> await

と書けます。

JavaScriptの Promisethen はコールバックが Promise(正確にはThenable)を返したら更にそれを待ち受ける挙動をします。モナドのbindみたいな感じです。この仕様があると ('a promise) promise みたいな型をいい感じに扱えないので、Standard MLの世界のコードが Promise の中身として Promise を返そうとした場合はラッパーを噛ませるようにします。

関数の連携をどうするか

現状のLunarMLでは 'a -> 'b という関数と、TypeScriptで言う (_: a) => b という関数は等価ではありません。LunarMLは末尾再帰を処理するためのトランポリンだとか、(JS-CPSバックエンドの場合は)CPS変換だとかを噛ませているので、同じように見える関数の型でも意味が違うのです。

したがって、JavaScriptの世界から呼び出すためのJavaScriptの関数をStandard MLの世界で作るには、単に fn x => ... と書くのではダメで、JavaScript.function : (value vector -> value) -> value という関数を使う必要があります。JavaScriptの関数の引数は可変長になれるので、引数を value vector で表現しています。

しかし、引数が value vector だと引数にアクセスするのにいちいち Vector.sub (args, 0) と書く必要があってだるいです。function2 : (value * value -> value) -> value のような便利関数を用意すれば引数へのアクセスの問題はマシになりますが、型が value なのでキャストが必要になります。型を function2 : ('a * 'b -> 'c) -> value という風にすればキャストは不要になりますが、キャストはunsafeなのでそういうのを便利関数として提供するのはよろしくなさそうです。

型クラスのようなものがあれば、

val function2 : (Marshal 'a, Marshal 'b, Marshal 'result) => ('a * 'b -> 'result) -> value

という風に「引数や返り値の型がJavaScriptでもいい感じになっている」ことを表現できそうですが、Standard MLには型クラスはありません。うまい設計を思いつければ型クラスを導入するのはアリなのですが、Standard MLと調和するうまい設計を思いつくかが問題です。

真面目な話、Standard MLでやるなら、型クラスの辞書渡しのノリで

val int : int rep
val string : string rep
val function2 : ('a rep * 'b rep * 'result rep) -> ('a * 'b -> 'result) -> value

という風にするのが良いのかもしれません。


そんな感じで、LunarMLの開発をちょっとずつ進めています。面白そうだと思った方はGitHubにスターをいただけると嬉しいです:

Spread the love