先日、Emscripten & WebAssembly night !! #7というイベントにて、AsteriusというHaskellをWebAssemblyにコンパイルするツールについて紹介いたしました。
資料はこちら👇です。
AsteriusでHaskellの関数をJSから呼べるようにしてみた(けど失敗)
本日は、スライドの英語で書いていた箇所を和訳しつつ、いろいろ捕捉してブログ記事の形で共有します。
Link to
here🔍Asteriusとは何か
冒頭でも触れたとおり、AsteriusはHaskellのソースをWebAssemblyにコンパイルするコンパイラーです。
GHCのHEAD(開発中のバージョン)を都度フォークして、現在活発に開発中です。
Template Haskellと、GHC標準におけるIOを行う関数(の大半)を除いた、すべての機能が利用できるようになっています。
現状のWebAssemblyを実用する上で必要不可欠であろう、FFIもサポートされています。
つまり、JavaScriptからWebAssemblyにコンパイルされたHaskellの関数を呼んだり、HaskellからJavaScriptの関数を呼ぶことができます!
何かしらのIO処理を行う場合は、基本的にこのFFIを使ってJavaScriptの関数を呼ぶことになります。
加えて、ahc-cabal
という名前のコマンドで、cabalパッケージを利用することもできます。
こちらはcabal
コマンドの単純なラッパーです。ahc-cabal new-build
などと実行すれば、外部のパッケージに依存したアプリケーションも、まとめてWebAssemblyにコンパイルできます。
本格的に開発する上では欠かせないツールでしょう。
Link to
here👍Asteriusのいいところ
Asteriusは、“A linker which performs aggressive dead-code elimination, producing as small WebAssembly binary as possible.”と謳っているとおり、GHCのランタイムを抱えているにしては、比較的小さいWASMファイルを生成するそうです。
というわけで手元で試してみたところ、下記のような結果になりました。
- 空っぽのプログラム(
main = return ()
しかしないソース):- 36KB(
.wasm
ファイルのみ)。なかなかいい感じですね。 - 168KB(実行時に必要な
.mjs
ファイルを含めた合計)。未圧縮でこれなら確かに十分軽いでしょう。Webpackなどで結合・minifyするともっと軽くできますし。
- 36KB(
- 今回私が移植を試みたアプリ(詳細は後ほど):
- 1.9MB(
.wasm
ファイルのみ)。うーん、ちょっと苦しいような…😥。 - 2.1MB(実行時に必要な
.mjs
ファイルを含めた合計)。.mjs
ファイルの内容は特に変わりませんでした。
- 1.9MB(
ちなみに、移植前の元のソースを含むアプリを、Linux 64bit向けのELFファイルとしてビルドして比較してみたところ、.wasm
ファイルよりも少し小さいぐらいでした。
詳細な内訳が気にはなりますが、今のソースですと大体これぐらいが限界なのかも知れません(でもWASMは現状32bitバイナリー相当のはずだし、もう少し小さくならないものか…)。
加えて、Asteriusを利用して開発すると、ほぼ最新のGHCの開発版が使える、というところも、新しもの好きなHaskellerをわくわくさせるところですね!(今回はあいにく新しい機能について調べる余裕もなかったので、特に恩恵は受けてませんが…😅)
Asteriusは、GHCをフォークしていくつかの機能を追加して作られているものです。
しかし幸いオリジナルとの差分が十分に小さく、作者が定期的にrebaseすることができています。
詳細な違いはAbout the custom GHC forkにまとまっています。近い将来GHC本体に取り込まれそうな修正ばかりではないかと。
それからこれは、ブラウザーでHaskellを動かすことができるという点でAsteriusの競合に当たる、GHCJSと比較した場合の話ですが、FFIを利用して、JavaScriptから直接Haskellを呼ぶことができるようになっているのも、優れた点と言えるでしょう。
GHCJSはこちらのドキュメント曰く、JavaScriptからHaskellを呼ぶ機能は備えてはいるものの、簡単ではないためドキュメントも書かれておらず、推奨されていません。
これでは状況によってはかなり使いづらいでしょう。
今回私が試したように、コアとなる処理だけをHaskellの関数として書いて、それをJavaScriptから呼び出すということができないのです。
一方Asteriusでは、例えば👇のように書くことで、WASMがエクスポートする関数として、func
をJavaScriptから呼べるようにすることができます!
"func" func :: Int -> Int -> Int foreign export javascript
ただし、実際に今回試してみたところ、Asteriusではまだバグがあったので、この用途では依然使いにくいという状況ではありますが…(詳細は後で触れます)。
Link to
here👎Asteriusのイマイチなところ
Asteriusは、やっぱりまだまだ開発中で、バグが多いです。
今回の目的もバグのために果たせませんでした😢。
先ほども触れたとおり、特に未完成なのが、IOとTemplate Haskellです。
GHCなら使えるはずのIO
な関数の多くが使えませんし、Template Haskellに至っては一切利用できません。
IOについては、現状、(putStrLn
などのよく使われる)一部を除き、FFI(foreign import javascript
)を使ってJavaScriptの関数経由でよばなけれなりません。
これは、入出力関連のAPIを一切持たないという現状のWebAssemblyの事情を考えれば、致し方ない仕様だとも言えます。
WASIの策定によってこの辺の事情が変わるまでの間に、すべてforeign import javascript
で賄うというのも、なかなか面倒なことでしょうし。
Template Haskellに関しては、現在こちらのブランチで開発中です。…と、思ったらこのPull request、Closeされてますね…。
これに関して詳しい事情はわかりません。いずれにしても、Template Haskellを実装するには、コンパイル時にその場でHaskellを評価するためのインタープリターが別途必要だったりして、結構ハードルが高いのです。
加えて、RTS(この場合、コンパイルしたHaskellを動かすのに必要なWASMやJavaScriptファイル)がBigInt
に依存している関係で、V8やSpiderMonkeyでないと動かない点もまだまだ、という感じです。
ブラウザーで言うと、2019年5月3日時点でChromeか、FirefoxのBeta版以降でないと使用できません1。
Link to
here⚙️Asteriusの仕組み
Asteriusのドキュメント「IR types and transformation passes」をざっくり要約してみると、Asteriusは以下のような流れで動くそうです。
実際にはahc-link
というコマンドがこれらの手順をまとめて実行するので、ユーザーの皆さんはあまり意識する必要はないでしょう。
- フロントエンドプラグインという仕組みでラップしたGHC(のフォーク)を使い、GHCが生成したCmmという中間言語で書かれたコードを、
AsteriusModule
という独自のオブジェクトに変換します。 ahc-ld
という専用のリンカーで、WASM向けにリンクします。- 最後に、
ahc-dist
というコマンドで、リンクしたモジュールを実行できる状態にします。- binaryenか、wasm-toolkitというHaskellでWASMを書く言語内DSLを利用して、
ahc-ld
がリンクしたモジュールを検証し、.wasm
ファイルに変換して、 - 実行時に必要なJavaScriptファイルをコピーして、
- Haskellのソースにおける
main
関数を実行する、エントリーモジュールを作ります。
あとはこれをHTMLファイルから<script>
タグで参照すれば、ブラウザー上でHaskellが動きます。
- binaryenか、wasm-toolkitというHaskellでWASMを書く言語内DSLを利用して、
Link to
hereAsteriusでHaskell製の関数を実行してみた
ここからは、私が以前作ったアプリケーションのコアに当たる関数をAsteriusでコンパイルすることで、ブラウザー上で動かせるようチャレンジした時の体験談を紹介します。
今回試みたアプリケーションは、単純なコマンドラインアプリケーションです。
詳細は省きますが、行単位で書かれたファイルをパースして、項目ごとの合計を計算するだけの、ありふれたものです。
パーサーはmegaparsecを使って作り、整数の四則演算ができるようなっているのも特徴です。
そのアプリケーションの処理のほとんどすべてに当たる、ファイル名とその中身を受け取って、計算結果を文字列で返す関数(FilePath -> Text -> Text
)を、FFIでエクスポート(foreign export javascript
)し、JavaScriptから呼べるようにしてみました。
アプリケーション自体の書き換えはほとんど必要なかったものの、依存関係を減らしたり、依存するパッケージを書き換えたりするのが大変でした。
というのも、先ほど触れたとおり、Asteriusは現状「Template Haskellと、GHC標準におけるIOを行う関数(の大半)」が一切使用できないので、取り除かなければコンパイルエラーになってしまいます。
template-haskellパッケージに間接的に依存しているだけで依存関係の解決すらできないのはなかなかつらいものでした。
stack dot
コマンドを使って依存関係のツリーを作り、それを見てtemplate-haskellパッケージに間接的に依存しているパッケージを割り出し、そのパッケージの必要な関数のみを切り出すことでどうにか回避できました。
monoidal-containersパッケージとfoldlパッケージがそれでした。
幸い、どちらも依存しているのはごく一部だったで、必要な部分だけをコピペして使うことにしました。
それから、IO
への依存もなくすために、textパッケージから*.IO
なモジュールを取り除いたりもしました。
当然、元々のアプリケーションもtextパッケージの*.IO
なモジュールを使ってはいたので、それを使わないよう修正する必要がありました。
しかしそこはHaskell。そうしたIO
に依存した関数から純粋な関数を切り出すのは、型システムのおかげで大変楽ちんでした!😤
入出力をするのにJavaScriptのFFIを使わないといけない、という現状のWebAssemblyの制約が、偶然にもマッチしたわけですね!
純粋じゃない関数はときめかないので捨て去ってしまいましょう✨
Link to
here結果
ここまで頑張った結果、目的の関数をforeign export javascript
してコンパイルを通すことはできました🎉
しかし、実際にブラウザー上で動かしてみたところ、AsteriusのFFIのバグにハマってしまいました…😢
肝心のforeign export javascript
した関数が、返すべき値を返してくれないのです!
恐らくforeign export javascript
を使わずに、Haskell側からJavaScriptの関数を呼ぶようにしていれば、今回の問題は回避できたのではないかと思います。
しかし、それは今回のゴールではありませんし、あまり便利ではないのでひとまず移植は見送ることにしました。残念!
Link to
here✅おわりに
今回Asteriusを試したことで、ブラウザー上でHaskellを動かす、もう一つの可能性を知ることができました。
とは言え、バグが多かったり依存関係からIOやTemplate Haskellを抜き出さなければならなかったりで、まだまだ実用的とは言い難いでしょう。
しかし、今回報告したバグが直れば、ブラウザーによる処理のコアに当たる部分をHaskellで書く、という応用が利きそうです。
例えばPandocなどHaskell製アプリケーションをブラウザーから操作する、なんてアプリケーション作りが捗りそうですね!