Haskellに関する実践的な話題を扱います。 (はてなダイアリーに書いてたやつをちょっと書き換えただけかも…)
Haskellでも辛くない!
(自称)生粋の理論派HaskellプログラマがwxHaskellに出会い、 テトリスを完成させるまでの5日間。
Haskellは良い言語だと思うのだが、 世間での認識を数学者のおもちゃレベルから汎用実用的言語へと改めるには やはりHaskellでゲームをばりばり書くとかの必要があるだろう(ほんまかいな?)
というわけでGUIである。 Windowが出せないと最近では馬鹿にされるてしまうのである。 HaskellでGUIとなると、とりあえず有名どころ(?)のwxHaskellを見てみることにする。
まず最初に用意するもの。
Hugsでもいいかもしれないけど、GUIプログラムだとやはり バイナリを作りたいのでGHCを使いたい。 ここ から入手できる。Windowsな人ならmsiファイルがあるので インストールらくらく。
wxHaskellはGHCのライブラリに含まれていないので これもとってくる必要がある。 こちらから。 これまたWindowsな人なら解凍してバッチファイルを実行するだけで インストールが完了する。
インストールできたらまずはquick start あたりのサンプルを読むと良いと思う。 どんな感じのものなのかがある程度わかるはず。
まず最初に、適当に書いたウインドウを表示するだけのプログラム。
module Main where import Graphics.UI.WX main = start mainFrame mainFrame = do f <- frameFixed [text := "Hello World"] p <- panel f [] set f [layout := minsize (sz 300 300) $ widget p]
main = start mainFrame …というのが有るが、 startは IO () -> IO () の型を持つ関数で、 指定されたコマンドを実行した後にメッセージループに入るような動作をする。 つまり、渡すのは初期化コマンドである。 上のmainFrameでは300*300のサイズを持つウインドウを作成している。 引数はIO ()の型を持つので、もちろんウインドウの作成以外も出来る。
main = start $ putStrLn "Hell World!!"このようなコードを書くと、文字列を出力しただけで メッセージループに入るのでウインドウが作られない。 ウインドウがないとプログラムが終われなくなるので、 とりあえずウインドウを作るコマンドを渡すのが普通である。 で、とりあえず最初に張ったソースをコンパイル。
ごりごりごりごりごりごりごり…
300*300のウインドウ表示するだけのプログラムなんですが… 異様に長いコンパイルの後に7.4MBほどの実行ファイルが出来上がる。 ええっ?300*300のウインドウ表示するだけのプログラムが7.4MB!?
そんなわけで、この時点では問題山積みである。
最小が7MB強はちと辛い。
実行すればわかるのだが、起動時に一瞬大きいウインドウが表示される。 おそらく、ウインドウのパラメータの設定中にウインドウを表示しっぱなしのせい。
コンパイラが生成する実行ファイルが普通のコンソールアプリになってるので、 Windows環境下だとコンソールが出てしまう。かっこ悪い。
でも大丈夫。最終的に全部解決するので。
wxHaskellを使えばHaskellでもGUIが出来るぜ、ということなのだが、 そのままでは色々と問題がありそうだったのは昨日書いたとおりである。 解決策を模索する。
適当にZipで圧縮すると1.58MBに、 7Zipで圧縮すると970KBぐらいになった。 これでも相当でかいと思うのだが、結局実行するときは解凍しないといけないので 根本的には解決にならないだろう。 実行ファイルを圧縮できるUPXもつかってみた。 普通に圧縮したら6MB弱にしかならなかったのでこりゃ駄目だと 思ったら余分なデータの削除なるオプションで500KB弱になった。 ちょっと大きいような気もするけど7MBから考えると大幅にましだし、 まぁ、何とかなりそうである。
ウインドウが出来上がるまで不可視にすることが出来たので解決。
これはPEファイルのオプションなので、 バイナリエディタで書き換えれば表示されなくなる。 これで解決である。(これで良いのか…?) 後学の為に場所を記しておく。 (こんなとこだけ記されてもうれしくない方はちゃんとしたところで PEファイルのフォーマットを調べましょう)
ここ(アドレスDCh)の2バイトが0002hだとWindowsGUIアプリということのようである。
一応解決したので、wxHaskellの勉強である。 とりあえずなんか作らねば、というわけで電卓である。 関数型なので、関数電卓。作ったのは関数電卓じゃないけど。
module Main where
import Graphics.UI.WX
-- 電卓の状態
type CalcState = (Integer,Maybe Integer,Maybe String)
initState = (0,Nothing,Nothing)
main = start mainFrame
-- ウインドウの形成
mainFrame = do
f <- frameFixed [text := "Calculator"
,clientSize := sz winX winY
,visible := False]
p <- panel f []
file <- menuPane [text := "&File"]
mclose <- menuItem file [text := "&Close"]
disp <- textEntry p AlignRight
[text := "0",outerSize := sz (winX-5) textHeight]
var <- varCreate initState
set f [menuBar := [file]
,on (menu mclose) := close f]
makeButton p (disp,var)
set f [visible := True]
where
makeButton frame arg =
sequence [mak x y t | (y,ls) <- zip [0..] but
,(x, t) <- zip [0..] ls] where
mak x y t = button frame
[text := t
,position := pt (x*(bSizeX+bMergin)+bMergin)
(y*(bSizeY+bMergin)+textHeight+bMergin)
,outerSize := sz bSizeX bSizeY
,on command := pushButton arg t]
but = [ ["7","8","9","/","AC"]
,["4","5","6","*"]
,["1","2","3","-"]
,["0","+/-",".","+","="] ]
(bSizeX,bSizeY) = (30,20)
bMergin = 5
textHeight = 20
(winX,winY) = (182,145)
-- ボタン押下時の処理
pushButton :: (TextCtrl a,Var CalcState) -> String -> IO()
pushButton (disp,var) b
| b=="AC" = do
varSet var initState
dispNum disp initState
| any (==b) ["0","1","2","3","4","5","6","7","8","9"] =
upd $ pushNum $ read b
| any (==b) ["+","-","*","/"] = upd (pushOpr b)
| b=="=" = upd pushEqual
| b=="+/-" = upd pushMinus
where
upd f = do
dat <- varGet var
let new = f dat
varSet var new
dispNum disp new
-- 数字の表示
dispNum :: TextCtrl a -> CalcState -> IO ()
dispNum disp (n,Nothing,_) = set disp [text := show n]
dispNum disp (_,Just n ,_) = set disp [text := show n]
-- 各々の処理
pushNum :: Integer -> CalcState -> CalcState
pushNum d (n,Nothing,o) = (n,Just d,o)
pushNum d (m,Just n,o) = (m,Just (n*10+d),o)
pushOpr :: String -> CalcState -> CalcState
pushOpr o (n,Nothing, _) = (n,Nothing,Just o)
pushOpr o (m,Just n,Nothing) = (n,Nothing,Just o)
pushOpr o (m,Just n,Just s) = (exec s m n,Nothing,Just o)
pushEqual :: CalcState -> CalcState
pushEqual (m,Nothing, _) = (m,Nothing,Nothing)
pushEqual (m,Just n,Nothing) = (n,Nothing,Nothing)
pushEqual (m,Just n,Just o) = (exec o m n,Nothing,Nothing)
pushMinus :: CalcState -> CalcState
pushMinus (m,Nothing,o) = (-m,Nothing,o)
pushMinus (m,Just n,o) = (m,Just (-n),o)
exec opr m n = op m n where
op = case opr of
"+" -> (+)
"-" -> (-)
"*" -> (*)
"/" -> div
一応ソースとWindows用バイナリ。(↑と同じもんだけど)
スクリーンショット
割と普通である。 Windows電卓を参考にしたけど、結局面影なし。 機能も整数の演算のみ。小数点ボタンがあるけど、 押すとおちるので押さないように。
プログラムについて。
mainFrameがウインドウの構築を行う。 ここはまぁ普通。 この辺でウインドウに対してイベントハンドラを設定する。
このイベントハンドラというのが問題。 イベントハンドラはイベントが発生すると適当にコールバックされるのだ。 コールバック、コールバックと、WindowsのGUIプログラムとかに慣れていれば さも自然なことだが、関数が状態を持たないHaskellにとっては おなじコンテキストでコールバックされても困るのである。
実際にはコールバックされるのはIO()なコマンドなので実際には 何とかなるのだが計算の状態を文脈に隠蔽しておくことは(おそらく)不可能である。 書き換え可能な変数を使って計算状態を保持しておくほか無い。 上ではwxHaskellで定義されているvarCreateなどを使って そのような変数を生成しているが、Data.IORefのnewIORefなどでも 同じである。
var <- varCreate initStatevarCreateというのは a -> Var a の型を持つ関数で、Varは
varGet :: Var a -> IO a varSet :: Var a -> a -> IO ()などの操作が行える。 これをみんなで共有して状態を読み書きしている。
しかし、これはうれしくない。 wxHaskellでは単純なIO()のコマンドのコールバックなのでそうせざるを得ないが、 なんとか隠蔽して関数的なレイヤーを構築すべきであろう。
上の電卓ではとりあえず最終的な計算部分だけは関数的になるように頑張って見た。 しかし、もっと何というか関数的に宣言的に記述したいのである。 コマンドの羅列で作ってると、これは手続き言語か、と思えてくる。 まぁ、もうちょっと設計を頑張らないと。
突然だが、GHCは日本語を文字列リテラルとして含むコードをコンパイルできない。
プログラムの動作としては、Stringは[Char]だし、 Charは単なる0..255なデータだと思うので、日本語を扱うのは全く問題ない。 ところがどっこい、GHCは文字リテラルとして想定外のものを はじいているようなのである。(というかパーズできないのか) "lexical error in string/character literal" などというメッセージを出して コンパイルエラーとなる。
このエラーは文字エンコードをSJISにしてもUTF8にしても発生する。 しかし、EUC-JPにするとなぜか発生しない。 EUC-JPが80h〜A0hの領域を使っていないせいなのだろうか。
とにもかくにもEUC-JPなら問題なくコンパイルできる。 だが、Windowsが受け付けるのはSJISである。 Windowsが、というのはおかしいのであるが、 wxHaskellが受け取った文字列を何も変換していなくて、 実行環境がWindowsだとするとWindowsが使う文字コードが問題になってくるのである。 Unix系だとEUC-JPがデフォルトだと思うので、この章読み飛ばしちゃっても 問題ないかもしれない。
リテラルに格納されるのはGHCでコンパイルできるEUC-JPになる。 そこでtextとかの属性に文字列を渡す前に EUC-JP→SJIS変換を掛ければ良いのである。
[title,fileMenu,exitMenu] =
map eucToSjis ["電卓","ファイル (&X)","終了 (&X)"]
...
f <- frameFixed [text := title, ...
p <- panel f []
file <- menuPane [text := fileMenu]
mclose <- menuItem file [text := exitMenu]
...
このファイルを 「EUC-JPで保存」し、コンパイルして実行すると、 title,fileMenuなどには「SJIS」の文字列が入ることになる。 eucToSjisは次のように適当に作った。 あんまりちゃんと正しいか確かめてない。
module CCode where
import Data.Char
eucToSjis :: String -> String
eucToSjis =
eucToSjis (x:xs)
| ord x <= 0xA0 = x:eucToSjis xs
| otherwise = case xs of
y:ys -> (chr $ cvt1 (ord x)) :
(chr $ cvt2 (ord x) (ord y)) :
eucToSjis ys
_ -> error "invalid EUC-JP string."
where
cvt1 x | x < 0xdf = ((x+1) `div` 2) + 0x30
| otherwise = ((x+1) `div` 2) + 0x70
cvt2 x y | y <= 0xA0 = y
| mod x 2 == 0 = y-2
| y < 0xe0 = y-0x61
| otherwise = y-0x60
ちなみに上プログラムは
http://www.net.is.uec.ac.jp/~ueno/material/kanji/euc2sjis.html
このページを参考にさせていただいた。で、取り敢えず昨日のソースをこのように変更し、 コンパイル→実行したところ正しく日本語が表示された。 なんとかうまくいったようである。
しかし。 上記のソースはいくつかの点で相当いやな感じなのである。 いくつかの点というか、
何が問題って、原因はすべてGHCが多倍長文字を含むソースを コンパイルできないことにあるのだから、 もっと直接的な解決方法がある。バイナリ書き換えである。 生成された実行ファイルから文字列を検索し、しれしれっと SJISの文字列に書き換えればプログラムは何事も無かったかのように 日本語を表示してくれる。
だがしかし、これもどうなのだ、っちゅう感じである。 そもそもソースコード中で文字列について言及できてないのがなんともかんとも。 全然駄目である。
違う方法を考える。 発想は前と同じでソース中には多倍長文字を記述しない方式(…?)である。 国際化のことなんかなーんも考えてないGHCに多倍長文字食わすことが そもそもの間違い(?)だというわけで。 ソースに書けないものはどっかから引っ張ってこればよい。 なにか別のファイルにSJISで文字書いておけば実行時に そのファイルを読み込むことによりSJISの文字列が取得できる。
-- 文字列リソース
initResource :: String -> IO (String -> String)
initResource resfile = do
resDat <- readFile resfile
let dat = map (\(a:b:_) -> (a,b)) $
takeWhile (not.null) $
iterate (drop 2) $ lines resDat
return (\name -> fromMaybe name $ lookup name dat)
上関数は何かファイルを指定して、ID→文字列への写像を返すようなコマンドになる。
... res <- initResource "res.txt" f <- frameFixed [text := res "title", ... p <- panel f [] file <- menuPane [text := res "file"] mclose <- menuItem file [text := res "exit"] ...
使い方はこんな感じ。
title 超関数型電卓 file 超ファイル (&F) exit 超終了 (&X)
res.txtには上のようなことを書いておく。 それから、コンパイル&実行。
おお、やったぜ。 これでようやく枕を高くして眠れるのである。 ちなみにこれだと文字列をプラットフォームごとに用意することにより ポータビリティを持たせることが可能になる。 また、これはローカライズに普通に用いられる手法なので 他言語対応もできる。 問題点としてはファイルを別に用意する必要があるということか。 リソースに埋め込めれば理想的なのだが 私はいまのところHaskellでリソース扱う方法を知らないし、 ポータビリティも失われそうである。
まぁ、結局何が良いかよく分からないが、 個人的には最後のが比較的まともなのではないかと思う。 一応最後のやつのソース+Windowsバイナリを。 (圧縮+サブシステム書き換えで割かしまともにしたやつ)
Haskellでゲーム計画もいよいよ大詰めである。 これまでHaskellではGUIアプリを作ったことが無かったため (もっとも、日本でHaskellでGUIアプリを作ったという話を聞いたことが無いけど) その道程は困難を極めたが、これまでにひとつづつその障壁を取り除いてきた。 最初は私も本当にまともにゲームが組めるか不安でならなかったのだが、 ようやく行けそうだ、というような感覚が湧いてきたのである。
今回はゲームを実装する上で重要な入出力処理と リアルタイムキー入力及び画像出力、さらにスピードコントロールを考える。
wxHaskwllにはWin32APIで言うところのGetKeyState()みたいなものが無い。 どうやってキー入力を取るのかといえば、ウインドウにイベントハンドラを 追加して、それで処理するのである。 しかし、やはりリアルタイムなゲームを設計するにあたって、 イベントで処理はいやなのだ。適当にループまわして、そのループから キーの押下状態を知りたいのである。
wxHaskellのローレベルな部分へのアクセスであるWXCoreモジュールにも それに相当しそうなものは無い。 しかし、とりあえずこっちのほうにはKeyUpイベントと KeyDownイベントがあったので(Coreじゃないほうには無い) それらを使って何とかすることにした。
windowOnKeyDown :: Window w -> (EventKey -> IO ()) -> IO () windowOnKeyUp :: Window w -> (EventKey -> IO ()) -> IO ()
それぞれ↑の様な型を持っている。指定したハンドラが追加される。 ハンドラにはEventKey型のデータとして押された/放されたキーが渡される。
このそれぞれで現在押されているキーを管理することができるので、 とりあえずそのようなものを実装することにした。
-- キーボード処理 data GameKey = GKUp | GKDown | GKLeft | GKRight | GKRotate deriving (Eq,Show,Enum) data KeyState = Pushed | Pushing | Released | Releasing deriving (Eq,Show) isPressed Pushed = True isPressed Pushing = True isPressed _ = False type KeyProc = GameKey -> KeyState
使うデータを適当に定義する。 キー状態はゲーム中で用いやすいように
main :: IO () main = start $ do kd <- varCreate ([ ],[ ],[ ]) ... windowOnKeyDown f $ kbdDown kd windowOnKeyUp f $ kbdUp kd ...
データの共有のためにVarな変数を作る。
kbdDown kd ek = kbdUpdt kd $ \(e,s,l) ->
let k = keyCvt ek
ns = s `union` k
ne = e `union` k
in if ns==s then (e,s,l) else (ne,ns,l \\ k)
kbdUp kd ek = kbdUpdt kd $ \(e,s,l) ->
let k = keyCvt ek in (e \\ k,s \\ k,l `union` k)
keyCvt ek = case keyKey ek of
KeyUp -> [GKUp]
KeyDown -> [GKDown]
KeyLeft -> [GKLeft]
KeyRight -> [GKRight]
KeyChar 'Z' -> [GKRotate]
_ -> []
kbdUpdt kd f = varUpdate kd f >> return ()
kbdDownとkbdUpの処理は上のとおり。 (e,s,l) にて、eがちょうど押されたキーのリスト、 sが押されているキーのリスト、lがちょうど放されたキーのリストとしている。 上記コードだけではちょうど押されたキーのリストと ちょうど放されたキーのリストがずっとそのままになってしまう。 毎フレーム
updateKey (_,s,_) = ([ ],s,[ ])
により、クリアを行う。キーデータは
keyProc (e,s,l) k
| k `elem` e = Pushed
| k `elem` s = Pushing
| k `elem` l = Released
| otherwise = Releasing
などという関数をつくって取得することにする。 ゲームの処理にはこの辺が見えないようにこれに(e,s,l)なデータを部分適用 した関数を渡すことにする。要するに最初のほうで定義したKeyProcなものを 渡すということである。
...
f <- frameFixed [text := wndTitle
,clientSize := wndSize
,bgcolor := black
,visible := False]
dc <- clientDCCreate f
...
clientDCCreateによりDCが取得できる。 それをゲームの描画関数に渡してやることにする。
リアルタイムなゲームでは実行中FPSを維持する必要がある。 適当にウェイトを入れてやることにする。 というか、結局IOコマンドの羅列になるんだが…
elapseTime :: Integer -> IO (IO (Int,Bool))
elapseTime fps = do
let frametime = picosec `div` fps
tm <- getClockTime
st <- varCreate ( (0,0,noTimeDiff),(1,tm))
return $ do
( (bef,cur,fdt),(cnt,bt)) <- varGet st
ct <- getClockTime
let dt = diffClockTimes ct bt
ndt = diffClockTimes ct tm
adj = frametime*cnt - toPsec dt
nc = if cnt==fps then (1,ct) else (cnt+1,bt)
(nbef,ncur) = if tdSec fdt /= tdSec ndt then (cur,0) else (bef,cur)
if adj<0 then do
varSet st ( (nbef,ncur,ndt),nc)
return (bef,False)
else do
varSet st ( (nbef,ncur+1,ndt),nc)
threadDelay $ fromInteger $ min 16666 $ adj `div` 1000000
return (bef,True)
where
toPsec dt = toInteger (tdMin dt * 60 + tdSec dt) * picosec + tdPicosec dt
picosec = 1000000000000
こんな感じ。 FPSを指定するとFPSを安定させるコマンドを返すようなコマンドである。 (というかこれ、C++用に作ったやつをほとんど書き写しなんだがなぁ…) 実行はIdleループを用いて、これを延々回し続けることによって行う。
...
et <- elapseTime 60
windowOnIdle f $ do
k <- varGet kd
st <- varUpdate gs $ onProcess (keyProc k)
varSet kd $ updateKey k
(fps,draw) <- et
when draw $ dcBuffer dc (rectFromSize wndSize) $ \dc -> do
onDraw dc res st
drawText dc (show fps) (pt 600 10) [color := white]
return True
...
上記コードでetがFPSを安定させるコマンドになる。 ちなみに返す(Int,Bool)は現在FPS(描画した回数)と 次回描画処理を行うべきかどうかである。
上のコードのonProcessとonDrawがゲームの処理と描画処理になる。 それぞれ、
onProcess :: KeyProc -> GameState -> GameState onDraw :: DC a -> Resource -> GameState -> IO ()
の型を持つことにする。 ResourceとGameStateはゲームにあわせてお好みに定義する。
というわけで このあたりで一通り雛形が完成した。説明が適当すぎたけど。 onProcessは毎秒ちょうどFPS回呼び出される。 onDrawは毎秒高々FPS回呼び出される。 onProcessからはなんとかIOをはずすことができた。 IO処理は一切できないのだが、それはonDrawのほうに全部行うことにした。
まぁ、なんというか、これも書いててHaskellの意味あるんかなぁとか C++で作ってたころと何も変わらんやん? とかなんとかちょっと悲しくなってきたのであるが、 最初はともかく動くものを作るのが先決であろう。 一通りできてからもっとエレガントな定義的で遅延を生かした 方法を考えることにする。
一応今回もファイルをアップ。
前回のでゲームを作るための基盤はおおよそ固まっていたので、 とりあえず何かテトリスを作ることにした。 HaskellでGUIテトリス。半年ほど前、Haskellというものを知ったころからは 考えもつかぬことであった。参照透明なのにゲームなんか作れるのかと。 (その答えがvarCreateとかを使うことだったなんて。しくしく。)
結局のところHaskellでもIOモナドを羅列しだすと何でも出来るし、 また、IOモナドを羅列しだすとHaskellのHaskellらしさというものも あまり感じられなくなってくるのだが、 (たとえばIOが本質じゃないけどIOモナドになってるやつ、 たとえばvarCrate :: a -> IO (Var a) あたりならば、 IOなどという胡散臭い?ものを持ち出さずとも 見た目書き換え可能な変数は純関数的に構築できる。 (前回のとかでそうできないのはコールバックがIO()になってるからであって…)
詳しくはStateモナドあたりを調べていただくとして、 実際のところ、後ろに副作用が見え隠れする云々ではなくて 今現在の私にはモナディックなやり方がどうも 関数型っぽく感じられていないのかもしれない。 個人的にはStreamなやり方がまさしく純粋関数的に感じられる。 いやまぁ、モナドなやり方は便利だし、 高階プログラミングの技法としてすばらしいと思うけど、 そのたどり着く先が私の書いたようなコードだとするとちょっと悲しい) そのせいかどうかは知らないが、当初思っていたほどの達成感は無いような。
…で、なんかよくわからん余談が長くなったけど、 とりあえずテトリスである。 実装は前回のonProcessとonDrawを書き換えた感じ。 テトリスの処理自体は普通にIOに侵食されずに記述できた。 描画も適当にビットマップ描画するだけなので適当に並べ立て。 昨日のプログラム、あそこまで書けていれば、今日は特に困ることは無かった。
なんというか、すんなり過ぎてあまり話題が無いのだが… とりあえず、作ってて思ったことなど。
C++で作ってたころは適当にシーンクラスを作って、 実行すべきシーンクラスを差し替えながらいろいろやっていた。 Haskellでも、そのようなディスパッチャを作るのは簡単であるが、 もっとHaskellならではの良い方法があるのではないだろうか?
これもC++で作ったときと同様に ループごとに状態を書き換えという感じになるが、 (要するにFinite State Automaotnを実装するような感じに) 継続を用いればこのような気苦労がいらないのは周知の事実である。 (直接継続を扱えないC++などの言語では MicroThreadとかFiberなどと呼ばれるコルーチンという形で同様の処理を行える) Haskellで継続を扱うためにはContinuationモナドを使えるが、 継続を使うプログラムは難解になり易い。 それに、遅延言語はその遅延評価のために 同等の処理を継続を扱うことなしに可能である場合が多い。 要するに、遅延評価を生かして直接的に分断される計算のコンテキストを 管理することが出来るのでは無かろうかということである。
今使ってるマシン(PentiumM 1Ghz)でも今回作ったテトリス程度ならば とくに問題なく60フレーム出る。 (なんかちょっとだけ足りてないような気がしないでもない… しかし、なんだかCPUパワーを使い切っていない。 使い切っていないのにスキップしてしまっているということは 速度調節ルーチンがどこか間違えているのか、あるいはライブラリのバグか)
説明・考察はこの辺で。 成果物 を公開しておきます。
ブロックの絵とか、http://www.linkfever.net/game/tetris.html このページから ちょっと使わせてもらったんだけど…。まずかったら書き直します。 というか、このページほとんど誰も見てないから大丈夫だよね。
結局、ゲームのシステムの周りはコマンドの羅列になってしまったような 感じがするのだが、一つ特筆すべき点があった。 このプログラム、ほとんどバグが出なかったのである。 GHCにはソースレベルデバッガが無い(と思う)のだが、 そもそもデバッグの必要が無かった。 なんだか今まですっかり忘れていたが、 この点だけでもHaskellを使う大きな利点ではないかと思った。 (まぁ、コード自体が小さいんだけど…)