Skip to content

Commit ac688cd

Browse files
authored
Merge pull request #161 from haskell-jp/asterius
記事の追加: AsteriusでHaskellの関数をJSから呼べるようにしてみた(けど失敗)(拡大版)
2 parents 13f3ba5 + c1605fe commit ac688cd

File tree

1 file changed

+134
-0
lines changed

1 file changed

+134
-0
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
---
2+
title: AsteriusでHaskellの関数をJSから呼べるようにしてみた(けど失敗)(拡大版)
3+
headingBackgroundImage: ../../img/background.png
4+
headingDivClass: post-heading
5+
author: Yuji Yamamoto
6+
postedBy: <a href="http://the.igreque.info/">Yuji Yamamoto(@igrep)</a>
7+
date: May 4, 2019
8+
tags: Asterius, WebAssembly
9+
...
10+
---
11+
12+
先日、[Emscripten & WebAssembly night !! #7](https://emsn.connpass.com/event/121028/)というイベントにて、[Asterius](https://tweag.github.io/asterius/)というHaskellをWebAssemblyにコンパイルするツールについて紹介いたしました。
13+
資料はこちら👇です。
14+
15+
[AsteriusでHaskellの関数をJSから呼べるようにしてみた(けど失敗)](https://the.igreque.info/slides/2019-04-19-asterius.html#(1))
16+
17+
本日は、スライドの英語で書いていた箇所を和訳しつつ、いろいろ捕捉してブログ記事の形で共有します。
18+
19+
# 🔍Asteriusとは何か
20+
21+
冒頭でも触れたとおり、[Asterius](https://tweag.github.io/asterius/)はHaskellのソースを[WebAssembly](https://developer.mozilla.org/ja/docs/WebAssembly)にコンパイルするコンパイラーです。
22+
GHCのHEAD<small>(開発中のバージョン)</small>を都度フォークして、現在活発に開発中です。
23+
Template Haskellと、GHC標準におけるIOを行う関数(の大半)を除いた、すべての機能が利用できるようになっています。
24+
現状のWebAssemblyを実用する上で必要不可欠であろう、FFIもサポートされています。
25+
つまり、JavaScriptからWebAssemblyにコンパイルされたHaskellの関数を呼んだり、HaskellからJavaScriptの関数を呼ぶことができます!
26+
何かしらのIO処理を行う場合は、基本的にこのFFIを使ってJavaScriptの関数を呼ぶことになります。
27+
28+
加えて、`ahc-cabal`という名前のコマンドで、cabalパッケージを利用することもできます。
29+
こちらは`cabal`コマンドの単純なラッパーです。`ahc-cabal new-build`などと実行すれば、外部のパッケージに依存したアプリケーションも、まとめてWebAssemblyにコンパイルできます。
30+
本格的に開発する上では欠かせないツールでしょう。
31+
32+
# 👍Asteriusのいいところ
33+
34+
Asteriusは、"A linker which performs aggressive dead-code elimination, producing as small WebAssembly binary as possible."と謳っているとおり、GHCのランタイムを抱えているにしては、比較的小さいWASMファイルを生成するそうです。
35+
というわけで手元で試してみたところ、下記のような結果になりました。
36+
37+
- 空っぽのプログラム(`main = return ()`しかしないソース):
38+
- 36KB(`.wasm`ファイルのみ)。なかなかいい感じですね。
39+
- 168KB(実行時に必要な`.mjs`ファイルを含めた合計)。未圧縮でこれなら確かに十分軽いでしょう。Webpackなどで結合・minifyするともっと軽くできますし。
40+
- 今回私が移植を試みたアプリ(詳細は後ほど):
41+
- 1.9MB(`.wasm`ファイルのみ)。うーん、ちょっと苦しいような...😥。
42+
- 2.1MB(実行時に必要な`.mjs`ファイルを含めた合計)。`.mjs`ファイルの内容は特に変わりませんでした。
43+
44+
ちなみに、移植前の元のソースを含むアプリを、Linux 64bit向けのELFファイルとしてビルドして比較してみたところ、`.wasm`ファイルよりも少し小さいぐらいでした。
45+
詳細な内訳が気にはなりますが、今のソースですと大体これぐらいが限界なのかも知れません<small>(でもWASMは現状32bitバイナリー相当のはずだし、もう少し小さくならないものか...)</small>。
46+
47+
加えて、Asteriusを利用して開発すると、ほぼ最新のGHCの開発版が使える、というところも、新しもの好きなHaskellerをわくわくさせるところですね!<small>(今回はあいにく新しい機能について調べる余裕もなかったので、特に恩恵は受けてませんが...😅)</small>
48+
Asteriusは、GHCをフォークしていくつかの機能を追加して作られているものです。
49+
しかし幸いオリジナルとの差分が十分に小さく、作者が定期的にrebaseすることができています。
50+
詳細な違いは[About the custom GHC fork](https://tweag.github.io/asterius/custom-ghc/)にまとまっています。近い将来GHC本体に取り込まれそうな修正ばかりではないかと。
51+
52+
それからこれは、ブラウザーでHaskellを動かすことができるという点でAsteriusの競合に当たる、GHCJSと比較した場合の話ですが、FFIを利用して、JavaScriptから**直接**Haskellを呼ぶことができるようになっているのも、優れた点と言えるでしょう。
53+
GHCJSは[こちらのドキュメント曰く](https://github.com/ghcjs/ghcjs/blob/3959a9321a2d3e2ad4b8d4c9cc436fcfece99237/doc/foreign-function-interface.md#calling-haskell-from-javascript)、JavaScriptからHaskellを呼ぶ機能は備えてはいるものの、簡単ではないためドキュメントも書かれておらず、推奨されていません。
54+
これでは状況によってはかなり使いづらいでしょう。
55+
今回私が試したように、コアとなる処理だけをHaskellの関数として書いて、それをJavaScriptから呼び出すということができないのです。
56+
57+
一方Asteriusでは、例えば👇のように書くことで、WASMがエクスポートする関数として、`func`をJavaScriptから呼べるようにすることができます!
58+
59+
```hs
60+
foreign export javascript "func" func :: Int -> Int -> Int
61+
```
62+
63+
ただし実際に今回試してみたところAsteriusではまだバグがあったのでこの用途では依然使いにくいという状況ではありますが...(詳細は後で触れます)。
64+
65+
# 👎Asteriusのイマイチなところ
66+
67+
Asteriusやっぱりまだまだ開発中でバグが多いです
68+
今回の目的もバグのために果たせませんでした😢。
69+
70+
先ほども触れたとおり特に未完成なのがIOTemplate Haskellです
71+
GHCなら使えるはずの`IO`な関数の多くが使えませんしTemplate Haskellに至っては一切利用できません
72+
73+
IOについては現状、<small>(`putStrLn`などのよく使われる)</small>一部を除きFFI<small>(`foreign import javascript`)</small>を使ってJavaScriptの関数経由でよばなけれなりません
74+
これは入出力関連のAPIを一切持たないという現状のWebAssemblyの事情を考えれば致し方ない仕様だとも言えます
75+
[WASI](https://github.com/WebAssembly/WASI)の策定によってこの辺の事情が変わるまでの間にすべて`foreign import javascript`で賄うというのもなかなか面倒なことでしょうし
76+
77+
Template Haskellに関しては現在[こちらのブランチ](https://github.com/tweag/asterius/pull/81)で開発中です。...思ったらこのPull requestCloseされてますね...。
78+
これに関して詳しい事情はわかりませんいずれにしてもTemplate Haskellを実装するにはコンパイル時にその場でHaskellを評価するためのインタープリターが別途必要だったりして結構ハードルが高いのです
79+
80+
加えてRTS<small>(この場合コンパイルしたHaskellを動かすのに必要なWASMJavaScriptファイル)</small>が[`BigInt`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/BigInt)に依存している関係でV8SpiderMonkeyでないと動かない点もまだまだという感じです
81+
ブラウザーで言うと201953日時点でChromeFirefoxBeta版以降でないと使用できません[^firefox-stable]
82+
83+
[^firefox-stable]: [Can I use](https://caniuse.com/#feat=bigint)曰く安定版でも`about:config`を書き換えればすでに使えるとのことなんですがなぜか手元のFirefox 安定版ではうまくいきませんでした確かに`about:config`にそれらしき設定はあるものの`true`にしても何も変わらず...😰。
84+
ついでに細かいことを言うとFirefox Nightlyは`about:config`を書き換えなくても使えBeta版では`about:config`を書き換えると使えました
85+
86+
# ⚙️Asteriusの仕組み
87+
88+
Asteriusのドキュメント[IR types and transformation passes](https://tweag.github.io/asterius/ir/)をざっくり要約してみるとAsteriusは以下のような流れで動くそうです
89+
実際には`ahc-link`というコマンドがこれらの手順をまとめて実行するのでユーザーの皆さんはあまり意識する必要はないでしょう
90+
91+
1. [フロントエンドプラグイン](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/extending_ghc.html#frontend-plugins)という仕組みでラップしたGHC<small>(のフォーク)</small>を使いGHCが生成したCmmという中間言語で書かれたコードを`AsteriusModule`という独自のオブジェクトに変換します
92+
1. `ahc-ld`という専用のリンカーでWASM向けにリンクします
93+
1. 最後に`ahc-dist`というコマンドでリンクしたモジュールを実行できる状態にします
94+
- [binaryen](https://github.com/WebAssembly/binaryen)か[wasm-toolkit](https://github.com/tweag/asterius/tree/master/wasm-toolkit)というHaskellWASMを書く言語内DSLを利用して`ahc-ld`がリンクしたモジュールを検証し`.wasm`ファイルに変換して
95+
- 実行時に必要なJavaScriptファイルをコピーして
96+
- Haskellのソースにおける`main`関数を実行するエントリーモジュールを作ります
97+
あとはこれをHTMLファイルから`<script>`タグで参照すればブラウザー上でHaskellが動きます
98+
99+
# AsteriusでHaskell製の関数を実行してみた
100+
101+
ここからは私が以前作った[アプリケーション](https://github.com/igrep/igrep-cashbook/tree/master/hs2)のコアに当たる関数をAsteriusでコンパイルすることでブラウザー上で動かせるようチャレンジした時の体験談を紹介します
102+
103+
今回試みたアプリケーションは単純なコマンドラインアプリケーションです
104+
詳細は省きますが行単位で書かれたファイルをパースして項目ごとの合計を計算するだけのありふれたものです
105+
パーサーは[megaparsec](http://hackage.haskell.org/package/megaparsec)を使って作り整数の四則演算ができるようなっているのも特徴です
106+
そのアプリケーションの処理のほとんどすべてに当たるファイル名とその中身を受け取って計算結果を文字列で返す関数`FilePath -> Text -> Text`FFIでエクスポート<small>(`foreign export javascript`)</small>JavaScriptから呼べるようにしてみました
107+
108+
アプリケーション自体の書き換えはほとんど必要なかったものの依存関係を減らしたり依存するパッケージを書き換えたりするのが大変でした
109+
というのも先ほど触れたとおりAsteriusは現状Template HaskellGHC標準におけるIOを行う関数の大半)」が一切使用できないので取り除かなければコンパイルエラーになってしまいます
110+
template-haskellパッケージに間接的に依存しているだけで依存関係の解決すらできないのはなかなかつらいものでした
111+
[`stack dot`](https://docs.haskellstack.org/en/stable/dependency_visualization/)コマンドを使って依存関係のツリーを作りそれを見てtemplate-haskellパッケージに間接的に依存しているパッケージを割り出しそのパッケージの必要な関数のみを切り出すことでどうにか回避できました
112+
[monoidal-containers](http://hackage.haskell.org/package/monoidal-containers)パッケージと[foldl](http://hackage.haskell.org/package/foldl)パッケージがそれでした
113+
幸いどちらも依存しているのはごく一部だったで必要な部分だけをコピペして使うことにしました
114+
それから`IO`への依存もなくすために[text](http://hackage.haskell.org/package/text)パッケージから`*.IO`なモジュールを取り除いたりもしました
115+
116+
当然元々のアプリケーションもtextパッケージの`*.IO`なモジュールを使ってはいたのでそれを使わないよう修正する必要がありました
117+
しかしそこはHaskellそうした`IO`に依存した関数から純粋な関数を切り出すのは型システムのおかげで大変楽ちんでした!😤
118+
入出力をするのにJavaScriptFFIを使わないといけないという現状のWebAssemblyの制約が偶然にもマッチしたわけですね
119+
純粋じゃない関数はときめかないので捨て去ってしまいましょう
120+
121+
## 結果
122+
123+
ここまで頑張った結果目的の関数を`foreign export javascript`してコンパイルを通すことはできました🎉
124+
しかし実際にブラウザー上で動かしてみたところ[AsteriusFFIのバグ](https://github.com/tweag/asterius/issues/105)にハマってしまいました...😢
125+
肝心の`foreign export javascript`した関数が返すべき値を返してくれないのです
126+
恐らく`foreign export javascript`を使わずにHaskell側からJavaScriptの関数を呼ぶようにしていれば今回の問題は回避できたのではないかと思います
127+
しかしそれは今回のゴールではありませんしあまり便利ではないのでひとまず移植は見送ることにしました残念
128+
129+
# ✅おわりに
130+
131+
今回Asteriusを試したことでブラウザー上でHaskellを動かすもう一つの可能性を知ることができました
132+
とは言えバグが多かったり依存関係からIOTemplate Haskellを抜き出さなければならなかったりでまだまだ実用的とは言い難いでしょう
133+
しかし今回報告したバグが直ればブラウザーによる処理のコアに当たる部分をHaskellで書くという応用が利きそうです
134+
例えばPandocなどHaskell製アプリケーションをブラウザーから操作するなんてアプリケーション作りが捗りそうですね

0 commit comments

Comments
 (0)