AsteriusでHaskellの関数をJSから呼べるようにしてみた(けど失敗)(拡大版)

Posted by Yuji Yamamoto(@igrep) on May 4, 2019Tags: Asterius, WebAssembly

先日、Emscripten & WebAssembly night !! #7というイベントにて、AsteriusというHaskellWebAssemblyにコンパイルするツールについて紹介いたしました。
資料はこちら👇です。

AsteriusHaskellの関数をJSから呼べるようにしてみた(けど失敗)

本日は、スライドの英語で書いていた箇所を和訳しつつ、いろいろ捕捉してブログ記事の形で共有します。

Link to
here
🔍Asteriusとは何か

冒頭でも触れたとおり、AsteriusHaskellのソースをWebAssemblyにコンパイルするコンパイラーです。
GHCHEAD(開発中のバージョン)を都度フォークして、現在活発に開発中です。
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するともっと軽くできますし。
  • 今回私が移植を試みたアプリ(詳細は後ほど):
    • 1.9MB.wasmファイルのみ)。うーん、ちょっと苦しいような…😥。
    • 2.1MB(実行時に必要な.mjsファイルを含めた合計)。.mjsファイルの内容は特に変わりませんでした。

ちなみに、移植前の元のソースを含むアプリを、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がエクスポートする関数として、funcJavaScriptから呼べるようにすることができます!

foreign export javascript "func" func :: Int -> Int -> Int

ただし、実際に今回試してみたところ、Asteriusではまだバグがあったので、この用途では依然使いにくいという状況ではありますが…(詳細は後で触れます)。

Link to
here
👎Asteriusのイマイチなところ

Asteriusは、やっぱりまだまだ開発中で、バグが多いです。
今回の目的もバグのために果たせませんでした😢。

先ほども触れたとおり、特に未完成なのが、IOTemplate Haskellです。
GHCなら使えるはずのIOな関数の多くが使えませんし、Template Haskellに至っては一切利用できません。

IOについては、現状、putStrLnなどのよく使われる)一部を除き、FFIforeign import javascriptを使ってJavaScriptの関数経由でよばなけれなりません。
これは、入出力関連のAPIを一切持たないという現状のWebAssemblyの事情を考えれば、致し方ない仕様だとも言えます。
WASIの策定によってこの辺の事情が変わるまでの間に、すべてforeign import javascriptで賄うというのも、なかなか面倒なことでしょうし。

Template Haskellに関しては、現在こちらのブランチで開発中です。…と、思ったらこのPull requestCloseされてますね…。
これに関して詳しい事情はわかりません。いずれにしても、Template Haskellを実装するには、コンパイル時にその場でHaskellを評価するためのインタープリターが別途必要だったりして、結構ハードルが高いのです。

加えて、RTS(この場合、コンパイルしたHaskellを動かすのに必要なWASMJavaScriptファイル)BigIntに依存している関係で、V8SpiderMonkeyでないと動かない点もまだまだ、という感じです。
ブラウザーで言うと、201953日時点でChromeか、FirefoxBeta版以降でないと使用できません1

Link to
here
⚙️Asteriusの仕組み

Asteriusのドキュメント「IR types and transformation passes」をざっくり要約してみると、Asteriusは以下のような流れで動くそうです。
実際にはahc-linkというコマンドがこれらの手順をまとめて実行するので、ユーザーの皆さんはあまり意識する必要はないでしょう。

  1. フロントエンドプラグインという仕組みでラップしたGHC(のフォーク)を使い、GHCが生成したCmmという中間言語で書かれたコードを、AsteriusModuleという独自のオブジェクトに変換します。
  2. ahc-ldという専用のリンカーで、WASM向けにリンクします。
  3. 最後に、ahc-distというコマンドで、リンクしたモジュールを実行できる状態にします。
    • binaryenか、wasm-toolkitというHaskellWASMを書く言語内DSLを利用して、ahc-ldがリンクしたモジュールを検証し、.wasmファイルに変換して、
    • 実行時に必要なJavaScriptファイルをコピーして、
    • Haskellのソースにおけるmain関数を実行する、エントリーモジュールを作ります。
      あとはこれをHTMLファイルから<script>タグで参照すれば、ブラウザー上でHaskellが動きます。

Link to
here
AsteriusHaskell製の関数を実行してみた

ここからは、私が以前作ったアプリケーションのコアに当たる関数を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に依存した関数から純粋な関数を切り出すのは、型システムのおかげで大変楽ちんでした!😤
入出力をするのにJavaScriptFFIを使わないといけない、という現状のWebAssemblyの制約が、偶然にもマッチしたわけですね!
純粋じゃない関数はときめかないので捨て去ってしまいましょう✨

Link to
here
結果

ここまで頑張った結果、目的の関数をforeign export javascriptしてコンパイルを通すことはできました🎉
しかし、実際にブラウザー上で動かしてみたところ、AsteriusFFIのバグにハマってしまいました…😢 肝心のforeign export javascriptした関数が、返すべき値を返してくれないのです!
恐らくforeign export javascriptを使わずに、Haskell側からJavaScriptの関数を呼ぶようにしていれば、今回の問題は回避できたのではないかと思います。
しかし、それは今回のゴールではありませんし、あまり便利ではないのでひとまず移植は見送ることにしました。残念!

Link to
here
✅おわりに

今回Asteriusを試したことで、ブラウザー上でHaskellを動かす、もう一つの可能性を知ることができました。
とは言え、バグが多かったり依存関係からIOTemplate Haskellを抜き出さなければならなかったりで、まだまだ実用的とは言い難いでしょう。
しかし、今回報告したバグが直れば、ブラウザーによる処理のコアに当たる部分をHaskellで書く、という応用が利きそうです。
例えばPandocなどHaskell製アプリケーションをブラウザーから操作する、なんてアプリケーション作りが捗りそうですね!


  1. Can I use曰く安定版でもabout:configを書き換えればすでに使えるとのことなんですが、なぜか手元のFirefox 安定版ではうまくいきませんでした。確かにabout:configにそれらしき設定はあるものの、trueにしても何も変わらず…😰。
    ついでに細かいことを言うと、Firefox Nightlyabout:configを書き換えなくても使え、Beta版ではabout:configを書き換えると使えました。↩︎