これは Haskell Advent Calendar 2012 の七日目の記事です1。
導入:JavaScript Problem
JavaScript Problem と呼ばれるウェブ開発上の問題があり、手短にまとめてそれは概ね次のようなパラドックスの形を取る。
- JavaScript はクソである。
- JavaScript は必要である。
何を云うのだ、JavaScript は決してクソなどではない、そんなことを云う Haskell の方こそがクズなのだと云う方も居られるだろう。しかし、これを云ったのは私ではないし、私はフレーム合戦を始めるためにこの文章を書こうという訳では勿論ない。
上の要約を存在命題の形に言い直せば、幾分わかりやすくなるかもしれない。
JavaScript の代わりに Haskell でプログラムを書けたら幸せになる人種は、確かにこの世に存在する。
そして、この記事はそのような人種に向けてかかれたものだ。
Fay とは?
JavaScript Problem に対する一つの解として、ここでは Fay を採り上げる。公式サイトによれば、Fay の特徴は概ね次の通りだ。
- Haskell の構文的・意味論的サブセット
- 静的型付き、遅延評価、純粋
- JavaScript にコンパイルされる
- JS と互換性のある基本的データ型のサポート
- 簡単な FFI 機構
- GHC を用いた型検査
つまりは、「JavaScript の代わりにほぼ Haskell を用いることが出来る」と云うことだ。このほぼと云うのがどの辺りにあるのかを明らかにするのが、本稿の目的の一つだ。
以下では、有限オートマトンシミュレータの例を使って、Fay についての簡単な紹介をしていく。GitHub にソースコードがある。一通り遊べるような段階まで作ったつもりであるので、以下の記事を読むのがかったるいと云うような場合には、こいつで日がな一日ひたすら遊び倒すと云うのも一興ではないだろうか。
余談:オートマトンとは
オートマトンと云うのは正規表現のことだと思って頂いて間違いはない。間違いはないのだが、まあそうは見えないだろう。噛み砕いて、菱形の頂点から出発して入力された文字に従って枝を辿っていったとき、最後に二重線で囲まれた頂点に居れば勝ち、そうでなければ負け、と云うようなゲームだと思えばよい。例えば、初期状態で “10010” を入力して “Run” を押せば、左下から出発して「0→1→2→4→3→0」と辿ることになり、最後に二重四角に辿り着いているので勝ち。“1011001” なら「0→1→2→5→5→4→2→5」となりただの丸で終わるので負け。シミュレータで実行した場合、緑色のマスが今いる頂点で、最終的に赤で止まれば負け、青で止まれば勝ちとなる。適当に文字列を入力したり、図を組替えたりして遊ぶ内にどういうものかうっすらとでもおわかり頂けるのではないかと思う。
閑話休題。そろそろオートマトンの記事なのか Fay の記事なのか解らなくなってきたところで、本編に移ろう。
Fay の概観
まずは
$ cabal install fay
として fay
をインストールしよう。
どんな感じでプログラムが書けるのかと云うところに関しては、ソースコードの方を見て貰うのが早いと思う。至って普通の
Haskell プログラムに見える。これを fay
コマンドに喰わせると、このような
JavaScript が生成されて、あとはこれを HTML で読み込めば良い。
Fay を使った開発のワークフローとしては、
- お気に入りのエディタを起動する
- 一見 Haskell っぽいプログラムを書く
- GHCi に読み込ませて型検査をする
$ fay Hoge.hs
などとして JavaScript に変換する- ブラウザや Node.js を使って動作を検証するre
と云う流れになる。GHCi は型検査にしか使わないところが要なのだが、詳細は後程ということにして、早速コードを見ていこう。まず大まかにコードの設計について紹介しよう。主なデータ型の意味は以下の通り。
Trans
-
状態遷移を表す。図で云う矢印。遷移前の状態(
transFrom
)、入力、遷移後の状態を持つ。 State
-
内部状態を表す。図で云う菱形や丸の部分。
Int
の別名。 Automaton
-
オートマトンを表す。状態遷移のリスト(
transs
)、図では菱形で表される初期状態(initial
)、二重線で囲まれた受理状態のリスト(accepts
)を持つ。 AutomatonState
-
オートマトンのレイアウトや、選択中の頂点・辺の情報、実行中の場合は現在の状態などを持つ。
では早速最初の方から見ていこう。
{-# LANGUAGE EmptyDataDecls, NamedFieldPuns, NoImplicitPrelude #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}
module Automaton (main) where
import Language.Fay.FFI
import Language.Fay.Prelude
import MyPrelude
はおまじないみたいなものだ。Fay では Prelude
の代わりに Language.Fay.Prelude
を使うので、NoImplicitPrelude
言語拡張を用いる必要がある。また、EmptyDataDecls
拡張はJavaScript 側のデータに対応する型を作るのに使われる2。どういうことか。例えば 680
行目以降を見てみよう。
data Element
instance Foreign Element
instance Show Element
ここで、Element は JavaScript の DOM (ないしは jQuery)API での HTML の要素を表わすデータ型だ。Haskell 側ではこの子の定義をしらないので、型の名前だけ作って、中身は空にしている。JavaScript では関数に型が付いていないので、存在しない関数を呼ぶと実行時エラーになるが、こうして Fay の側で型を分けておけばそういったエラーをある程度排除出来る。
Foreign
は
Haskell と JavaScript
とやり取りが出来る、と云う印のようなものと思っておけばよい。Show
インスタンスの関数定義がないように見えるが、これは通常の Show
クラスとは若干違うもので、JavaScript
の側で印字出来る、ということを表しているにすぎない。印字は JavaScript
で行われるので、一度JavaScript にコンパイルされてしまえば、全ては Object
なので自然に印字出来るから、show
関数などを実装する必要はない。そうそう、Show の結果は JavaScript
の方式に則ってなされる。そもそも Fay.Prelude
では Show
のメンバ関数はエクスポートされていない。GHC であるていどの Printf
デバッグがしたいと思ったら、deriving
節を使えばよい。
さて、Element
と云う型を定義したからには、それを弄る DOM 関数を定義したい。それには
Fay の ffi 機構を使えば簡単に出来る。例えば、イベントハンドラを登録する
bind
という jQuery
の関数があるが、これを呼び出す関数 bind
は次のように実装出来る(690行目)。
bind :: Element -> String -> (Event -> Fay Bool) -> Fay ()
= ffi "%1.bind(%2, %3)" bind
ffi
関数は、GHC
側から見れば実体としては undefined
と同じだ。これが意味を持つのは fay
に処理させた時で、型注釈から引数の数を推定して、%1
,
%2
, %3
などのプレースホルダに入れた状態の
JavaScript
を生成してくれる。関数や文字列なども自動的に変換してくれる。Fay
は IO
モナドの代わりのようなもので、副作用を伴うような操作はすべて Fay
モナドに包まれている。だから、main
関数の型も、ここでは Fay ()
になっている。bind
の使用例は例えば、run
関数を見てみよう(16行目)。
run :: Event -> Fay Bool
= do
run _ -- set up canvas
<- jQuery "canvas"
canvas <- flip getContext "2d" =<< getIndex canvas 0
cxt <- newRef defAutomatonState
mps
renderAutomaton cxt mps"mousedown" (onMouseDown mps cxt)
bind canvas "mousemove" (onMouseMove mps cxt) bind canvas
こんな感じに、本当に普通の関数のように呼べる。呼ばれている onMouseDown
関数を見てみると、
onMouseDown :: Ref AutomatonState -> Context -> Event -> Fay Bool
= do
onMouseDown rps cxt ev <- readRef rps
ps <- getMousePos ev
pos let state = getStateAt ps pos
= getTransAt ps pos
trans if not (null trans)
then
if mouseState ps == TransSelected trans
then setMouseIdle rps
else setTransSelected trans rps
else
case state of
Just q ->
if mouseState ps == StateSelected q
then setMouseIdle rps
else setMouseState rps (PointAtState q)
Nothing -> setMouseIdle rps
renderAutomaton cxt rpsreturn False
こんな感じになっていて、これも普通の Haskell
のコードそのものだ。Ref
と云うのが出て来ているが、これは可変参照を表す型で、さっき出て来た Foreign
のインスタンスの値を仕舞っておく事が出来る。Ref
は Fay
の側で提供されている訳ではなく、こちらでその API
を呼び出してやる必要がある。その辺りの実装は 647行目以降にある。
data Ref a
instance Foreign a => Foreign (Ref a)
-- | Make a new mutable reference.
newRef :: Foreign a => a -> Fay (Ref a)
= ffi "new Fay$$Ref(%1)"
newRef
-- | Replace the value in the mutable reference.
writeRef :: Foreign a => Ref a -> a -> Fay ()
= ffi "Fay$$writeRef(%1,%2)"
writeRef
-- | Get the referred value from the mutable value.
readRef :: Foreign a => Ref a -> Fay a
= ffi "Fay$$readRef(%1)" readRef
Fay では State
や Reader
は使えないので、この Ref
を使って、状態を引き回すことになる。他の関数を見ると、アプリの内部状態や
canvas の文脈を引数として引き回しているのが判ると思う。
Fay では、Haskell のデータ型と JavaScript
のデータ型をシームレスに連携させる事が出来る。文字列・配列・数値型などは
Haskell のをそのまま使える。例えば、矢印の描画位置を計算している getTransShape
関数を見てみよう(438行目)。
getTransShape :: AutomatonState -> Trans -> Maybe TransShape
AutomatonState{..} Trans{transFrom = src, transTo = targ} =
getTransShape case (lookup src stateMap, lookup targ stateMap) of
Just p0@(x, y), Just p1@(x', y')) ->
(if p0 == p1
then Just (Arc p0)
else let theta = if x <= x' then atan ((y-y') / (x-x')) else pi + atan ((y-y') / (x-x'))
in if any (\t -> transFrom t == targ && transTo t == src) (transs automaton)
then Just $ Line (p0 %+ stateRadius %* angle (theta + pi/8))
%- stateRadius %* angle (theta - pi/8))
(p1 else Just $ Line (p0 %+ stateRadius %* angle theta)
%- stateRadius %* angle theta)
(p1 -> Nothing _
ここでやっているのは、
- 自分自身への矢印は円形を描く。
- 反対向きの矢印がなければ、頂点の半径の分だけ間を開けて線を引く。
- もし反対向きの矢印があれば、更に15度ずつズラして線を引く。
という計算だ。atan
や pi
を使って幾何の計算をしている。これを GHCi
で読み込んで実行しようとすると、
> getTransShape defAutomatonState (Trans 0 '0' 1)
ghciJust (Line (*** Exception: Language.Fay.FFI.foreignFay: Used foreign function not in a JS engine context.
と叱られる。これは、内部的には atan
や pi
は ffi
関数(の仲間)を使って定義してあるためで、これらのデバッグをしようと思ったらブラウザや
Node.js を使うか、或いは一旦 import Language.Fay.Prelude
をコメントアウトして、Prelude
を読み込むようにする必要がある。必要な計算がわかっている場合は、最初は
GHC だけで開発して、最終的に Fay
に合うように書き換えると云う手段も取れるだろう。
データ型の変換で注意する必要があるのが、文字列の取り扱いだ。Haskell
のリストはリンクドリストに変換されていて、String
は文字列型にマップされている。この辺りの齟齬が出て来るのは、例えば文字列を
map
で書き換えたり、文字を集めてリストとして文字列を作ったときだ。例えば、setTransSelected
関数を見てみよう(248行目)。
setTransSelected :: [Trans] -> Ref AutomatonState -> Fay ()
= do
setTransSelected ts asRef <- readRef asRef
as = TransSelected ts }
writeRef asRef as { mouseState
hideInspectors=<< jQuery "#trans-inspector"
expose <- mapM jQuery ["#trans-info-from", "#trans-info-inputs", "#trans-info-to"]
[tFrom, tInps, tTo] show $ transFrom $ head ts)
setValue tFrom (show $ transTo $ head ts)
setValue tTo (=<< arrToStr (map transAlphabet ts) setValue tInps
これは、クリックされた所にある矢印のリストを受け取って、それらを選択状態にする関数だ。選択したらその情報をテキストフィールドに反映する必要がある。transAlphabet :: Trans -> Char
と云う型なので、矢印(Trans
)のリストに対して
map transAlphabet ts
としてやれば、文字のリストが得られて、Haskell
ではこれが文字列になる訳だが、JavaScript レベルではこれは
文字の配列 であって文字列ではない。なので、 arrToStr
と云う便利関数を定義して、これで変換している。Haskell
上では型があっているので arrToStr
がなくても検査は通るのだが、最後の行を
map transAlphabet ts) setValue tInps (
などとすると、abc
と表示されるべきところ、
[Object object]
のような形になってしまう。因みに、arrToStr
は
arrToStr :: [Char] -> Fay String
= ffi "%1.join('')" arrToStr
と云う風に ffi
を使って join
を呼び出す形で定義している。
あとのプログラムの詳細については、どちらかと云うと jQuery の使い方や幾何計算の説明になってしまうので、プログラムについてはこの辺りにしておく。
Fay を使ってみて
以上を踏まえて、Fay を使ってみた感想を書いてみたい。
Fay は基本的には Haskell のサブセットなので、普段 Haskell を書くように Fay を書けば問題はない。これは非常に嬉しい。しかし、サブセットである悲しさとして、以下のような違いがある。
Prelude
の代わりに Language.Fay.Prelude
を使う
Prelude
関数とほぼ同等か、それ以外にも Data.List
や
Data.Maybe
などから引っ張ってきた関数もエクスポートされているが、やはり足りないので、必要な関数はコピペしてやる必要がある。
上の例では、MyPrelude.hsにそのような欠けている関数のコピペ群がある。
また、Read
クラスもちゃんと動かないので、数値などをパーズしようと思ったら、parseInt
関数 のように、ffi でJS
の対応するパーズ関数を読んでやる必要がある。
ローカルのモジュールは読み込むことが出来るので、頻繁に使うような関数は、MyPrelude.hs
みたいな形で切り出して用意しておくと良いだろう。
外部パッケージに依存出来ない。
上でもちらっと触れたが、全てを JavaScript
にコンパイルする都合上、Language.Fay.*
のモジュールと、ローカルのモジュール以外の外部ライブラリのモジュールはインポート出来ない。外部ライブラリと云うのは、base
パッケージなど基本的なものも含めてインポート出来ない。これは割と不便だ。Monadic
だったり Applicative
でないものに関しては、上で云ったようにコピペすればいいが、では Monad
は……詳細後述。
モナドはほぼ Fay
限定
現時点では、Fay
以外のモナドは実質使えない。
Fay で提供されている (>>=)
,
return
などは全て Fay
モナドに特化した形に書き換えられている。do
構文はどのモナドに対しても使えるのだが、return
が使えないので余り意味を成さない。
この制限は正直かなしかった。特に、今回のように canvas
を引き回して描画命令を出す必要がある場合、Reader
モナドが使えないと正直書きづらくてしょうがないし、Maybe
も lookup
を
>>=
でチェーンしたり、(,) <$> lookup a hoge <*> lookup b fuga
のような書き方が出来ないのは割と辛いものがある。Reader
や
State
は諦めて Ref
を噛ませて引数として引き回すしかない。
GHC で型検査は出来るが、実行は出来ない関数がある。
前節でも触れたが、 GHC
では実行出来ない関数がある。主に数値計算や文字列処理の辺りだ。これらは
ffi を用いて実装されていて、GHC 内部では undefined
扱いなので、こういった操作を行おうとすると失敗する。したがって、ブラウザで実行しながらがんばって
printf デバッグをするか、或いは一旦 Language.Fay.Prelude
をコメントアウトして Prelude
を読み込むなどして動かしてみて試すしかない。間違っても吐き出された JS
のソーコードを読もうと思ってはいけない。というか読めない。$ fay Automaton.hs --pretty
と云う具合に --pretty
オプションを付ければちょっとは読み易くはなり、公式サイトのほうにシンボルの読み方等はあることにはある。
このような関数をデバッグするのは、ちょっと慣れないと大変だ。
使えない言語拡張がある。
サブセットなので、全ての言語拡張を使える訳ではない。 Template Haskell 系の拡張はまあ残念ながら全滅。PatternGuards も無理だった。また、NamedFieldPuns などは使えるようだが、レコード構文がネストすると上手くパーズ出来ないようだ。DoAndIfThenElse とかも上手く動かない。あと、
case hoge of
Fuga a | a `elem` target -> ....
のような構文もサポートされていないらしい。どの構文がサポートされていてどれがサポートされていないかは、fay
コマンドにソースを通すと判るので、適宜それを使わないように書き換えていく必要がある。
他の候補
ここでは「JavaScript の代わりに Haskell を使いたい」と云う欲望の形で JavaScript Porblem を紹介したが、ようは「リッチな型システムや柔軟性といったものを追加した Better JavaScript がほしい」と云う要望としても表現出来る。というかそうした立場のほうが一般的だ。そうした視点も加味して、以下のような選択肢もあるらしい3。
- Roy
-
型推論や函数型言語的な要素を含んだ言語。
- CoffeeScript
-
Ruby のような感じで JavaScript を書けるらしい。
- TypeScript
-
JavaScript のような構文に、型注釈や型推論を追加した言語。
- Haxe
-
型推論や subtyping などの強力な型システムやクロージャなどを備えた言語。JavaScript 以外にも swf や C++、PHP へのコンパイルも可能なもよう(2012/12/26 追記)。
- js_of_ocaml
-
Haskell と双璧をなす函数型言語、OCaml のバイトコードから JavaScript へのコンパイラ(2012/12/26 追記)。
もっと Haskell っぽい選択肢、となると以下のようなものがある。
- GHCJS
-
JavaScript を吐くように改造された GHC。やばい。まだ未完成ではあるが、完成すれば Fay よりも凄いだろう。GHC を本当に改造したバージョンと、GHC API を使うバージョンの二つがあるが、インストール作業がかなり時間が掛かる。残念ながら時間がなかったので筆者はインストールを断念した。今後に期待。
- UHC
-
GHC に次ぐ有力な Haskell Compiler、UHC の JavaScript バックエンド。
- Elm
-
Haskell にかなり近い構文を持った、函数型リアクティヴ言語。まだちゃんと試してみたことはないが、より JavaScript の機能を使い易いように注力しているようだ。意味論的には正格言語で、遅延評価はないので、厳密なサブセットではない。
あるいは、JavaScript を生成するようなライブラリも沢山ある。それを使うのも一つの手か。
結論
Fay を使えば、ほとんど JavaScript を書かずに Haskell
で済ませることが出来る。 ほとんど、と云うのは、まだ Fay 上に構築された
jQuery などのラッパーが十分に開発されていないため、自分で ffi
関数で JavaScript
のコード断片を書く必要があるということだ。コード断片を書く必要があると云うことは、JavaScript
について調べなくてはならず、そこが揃っていないと結局 JavaScript
を書いているような気分になってしまう。まあ、この辺りはライブラリが充実してくれば大丈夫だろう。というわけなので、表題の「さらば愛しき
JavaScript」と云うのは嘘だ。おさらばは出来ない。というかそこまで愛しくなかった。
[2012/12/26 追記] まだ Hackage に登録はされていないようだが、GitHub に fay-jquery というレポジトリーがあった。まだ設計を試行錯誤している最中のようだが、完成すればかなり便利だろう。GitHub には、他にも DOM API のラッパーもあった。また、今回の例では canvas のラッパーを頑張ってこちらで書いたが、GitHub の examples 以下にも canvas を有効活用している例があって、ここから持ってくるのもよいかもしれない。
ただ、Fay の FFI
インターフェースはシンプルだが使い勝手がよいと思う。きちんと引数の数が合っているかも
fay
コンパイラがチェックしてくれるし。ffi
でしっかり API
を組んでおけば、あとはそれを使うだけでよい。もっとも、ここで設計をミスると
JavaScript の方でエラーが起きるので注意が必要だ。また、特定の JavaScript
環境に依存しない形になっているので、お気に入りの JavaScript
ライブラリと組み合わせて使うことが出来る。Yesod Blog
では、 AngularJS と組み合わせる方法が紹介されていた。
あと、やはり Fay
以外のモナドも自由に扱えたほうが嬉しい。何度も云うようだが、ここで Reader
モナドが使えればすっきり書けるのに……と云ったような場面によく遭遇した。また、ffi
を使っている関数でも、基本的な
Prelude 関数とかは GHC
で読み込んで実行出来ると、手軽にデバッグ出来ると嬉しいなあと思う。JavaScript
コンソールや Node.js に読み込ませる方法もあるが、Haskell
で定義した関数が呼べたり呼べなかったりするのでちょっと難しい。
また、ここでは紹介出来なかったが、yesod-fay パッケージを使うと、Yesod アプリとブラウザ間で Ajax 通信を行う時に、どちらの処理も Haskell で書けて、しかもデータ型を共有出来るというスゴイ旨みがある。他の Yesod の要素と同じように継続ビルド環境もある。詳細はパッケージの説明を参照して貰いたい。Yesod の開発者である Michael も Fay と ghcjs に大きな期待を寄せているようだ。
私も同じ気持ちで、GHCJS が発展してくれたらこんなに嬉しいことはない。まずはインストールが簡単になってほしい。もちろん、GHCJS を新たに入れ直すのとかは面倒なので、簡単なものは Fay を使えばいいと思う。
という訳で、結論としては JavaScript とおさらばすることは出来ないが、Better JavaScript としての Haskell4 としては、そろそろ役者が揃って実用出来る段階に入ってきていると思う。今回のプログラムは900行弱だが、コンパイルした JS もあまりもっさりせず軽快に動く。Haskeller 諸氏におかれましては、何か JavaScript を書く必要に迫られた際に、是非 fay を試してみてほしい。
それでは。Happy Haskelling!