Posted at

MacでHaskellを使ってJavaScriptをビルドする環境(GHCJS)を最短で整える(長い)


はじめに

先日、Elmを試したけどあまり合わなかった。

HTMLをプログラムで生成せずに昔ながらのやり方でファイルに書きたい。それでいて型指定も欲しい。

なのでGHCJSを試した。

GHCJSはElmのような新しい言語があるのではなく、Haskellのコンパイラ出力をJavaScriptに差し替えるというなかなか凄いやり方。Haskellの機能がほとんどそのまま使えて、更にDOMの機能も仕様の定義から自動生成されている。

自動生成なので ByteLengthQueuingStrategy のようなマイナー(?)なクラスにも対応するHaskellの関数があったりする。

つまり機能不足で困ることはほとんど無いと予想できる。

その代わりに提供されているのは素のJavaScriptのHaskell版というようなものなので、追加で miso のようなフレームワークを使った方が開発速度は速いと思う。


Haskellの環境を整える

brew install haskell-stack

StackはHaskell版brewみたいなもの。すぐにインストールできる。

取りあえず最低限のプログラムを実行してみる。

stack new haskell-sample --resolver=nightly-2018-05-10 

--resolver無しの場合は最新版LTSのコンパイラがインストールされる。取りあえずここはあとで使うのでバージョン指定で。

cd haskell-sample

stack build

.stack-work/install/x86_64-osx/nightly-2018-05-10/8.4.2/bin/haskell-sample-exe

someFuncと出れば成功。

これでHaskellの環境は終了。


Haskell版のコンパイル

続いてJavaScriptを書く環境を作る。

と言ってもサンプルプロジェクトをビルドするだけ。

https://github.com/nishimura/ghcjs-form-sample1.git

ここに置いた。

git clone https://github.com/nishimura/ghcjs-form-sample1.git

cd ghcjs-form-sample1
stack build

ghcjs-dom のコンパイルで固まったように見えるが落ち着いて待つ。メモリも大量に持って行かれるが、最初に書いたようにDOMで使う関数や型の大半をHaskell用に頑張って変換しているところなので。

ちなみに MacBook Pro で43分かかった。

コンパイルされたHaskell版プログラムを実行する。

.stack-work/install/x86_64-osx/nightly-2018-05-10/8.4.2/bin/ghcjs-form-sample1-exe

これでWebサーバーが立ち上がったので、 http://localhost:8000/ にアクセスして確認する。

これはHaskellで動いている状態。


JavaScript版のコンパイル

次はJavaScriptで動かす。

最初に~/.stack/config.yamlにバージョン差異をスルーする設定を書く。

echo 'allow-newer: true' >> ~/.stack/config.yaml

それからGHCJSのコンパイルに使うツールをインストールする。

stack install alex

stack install happy

export PATH="$HOME/.local/bin:$PATH"

PATHはbashrc等にも追加する。

node.jsも無ければインストール。

brew install node

そしてビルド。

stack build --stack-yaml=js.yaml

GHCJS自体のコンパイルに38分かかった。それから続いてghcjs-bootでGHCJSで使うライブラリのコンパイルが始まり、追加で44分かかった。

この時点で .stack-work/install/x86_64-osx/nightly-2018-05-10/ghcjs-8.4.0.1_ghc-8.4.2.20180505/bin/ghcjs-form-sample1-exe.jsexe/ にJavaScriptが生成されている。

open .stack-work/install/x86_64-osx/nightly-2018-05-10/ghcjs-8.4.0.1_ghc-8.4.2.20180505/bin/ghcjs-form-sample1-exe.jsexe/index.html

ブラウザで確認すると、コンソールに

id="area" not found

と出ていればOK。

htmlが自動生成されたものになっているので、まだ動かない。

今回利用するHTMLファイルはhtml/index.htmlにある。


JavaScriptのコンパイル(圧縮)

Closure Compilerの起動に必要なjavaのjreをここから手に入る。

https://www.java.com/ja/download/

完全にブラウザ用としてインストールされるっぽいけども気にせずこれを使う。

ln -s /Library/Internet\ Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java ~/bin/java

javaをPATHの通っているところに置いたあと、JavaScriptのコンパイルしてブラウザでアクセス。

./compile.sh

open html/index.html

これで終了。


Haskellのプログラムの説明

Main.hsAppMain.hsを起動するだけ。

本体は

application :: HTMLDocument -> HTMLElement -> JSM ()

application doc area = do
Just body <- getBody doc
releaseClick <- on area G.click $ do
(x, y) <- mouseClientXY
newParagraph <- createElement doc "p"
text <- createTextNode doc $ "Click " ++ show (x, y)
_ <- appendChild newParagraph text
_ <- appendChild body newParagraph
return ()

-- Make an exit button
exitMVar <- liftIO newEmptyMVar
exit <- createElement' doc TagSpan
text <- createTextNode doc "Click here to exit"
_ <- appendChild exit text
_ <- appendChild body exit
releaseExit <- on exit G.click $ liftIO $ putMVar exitMVar ()

-- Force all all the lazy evaluation to be executed
syncPoint

-- In GHC compiled version the WebSocket connection will end when this
-- thread ends. So we will wait until the user clicks exit.
_ <- liftIO $ takeMVar exitMVar
releaseClick
releaseExit
setInnerHTML body "<h1>Ka kite ano (See you later)</h1>"
return ()

こんな感じ。

https://github.com/ghcjs/jsaddle

ここのサンプルプログラムを写したんだけど、結構書き換えないと動かなかった。インターフェースをカジュアルに変えるのはHaskellあるあるっぽい。

Helper.hsElement => HTMLElement 変換用。

型が厳密すぎて、getElementById で取ってきた ElementHTMLElement ではないので GlobalEventHandlers のonclickイベントが付けられない。つらい。


まとめ

コンパイル待ち時間を抜いたら15分ぐらい。

コンパイル待ち時間を入れたら2時間ぐらいでセットアップできた。

(実は長時間待った後の最後にエラーが出てやり直したりして5時間ぐらいかかっている)