[PR]血液型生年月日で運命診断:無料お試しも本格鑑定!

top page

Pragmatic Haskell 〜おもちゃじゃないHaskell〜


About This

Haskellに関する実践的な話題を扱います。 (はてなダイアリーに書いてたやつをちょっと書き換えただけかも…)

Haskellでも辛くない!

お品書き


HaskellでGUIプログラミング編

もくじ

  1. 遭遇〜問題噴出
  2. 問題解決〜最初のアプリケーション
  3. 日本語の扱いの考察
  4. リアルタイムゲームに関する課題の解決
  5. テトリス完成〜総括

第一章 Haskellは一日にして成らず 〜関数型プログラミングのGUI事情〜

Haskellは良い言語だと思うのだが、 世間での認識を数学者のおもちゃレベルから汎用実用的言語へと改めるには やはりHaskellでゲームをばりばり書くとかの必要があるだろう(ほんまかいな?)

というわけでGUIである。 Windowが出せないと最近では馬鹿にされるてしまうのである。 HaskellでGUIとなると、とりあえず有名どころ(?)のwxHaskellを見てみることにする。

まず最初に用意するもの。

  1. GHC(Glasgow Haskell Compiler)
  2. Hugsでもいいかもしれないけど、GUIプログラムだとやはり バイナリを作りたいのでGHCを使いたい。 ここ から入手できる。Windowsな人ならmsiファイルがあるので インストールらくらく。

  3. wxHaskell
  4. 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!?

そんなわけで、この時点では問題山積みである。

でも大丈夫。最終的に全部解決するので。

第二章 完全なる関数の電卓 〜はじめてのGUIアプリ〜

wxHaskellを使えばHaskellでもGUIが出来るぜ、ということなのだが、 そのままでは色々と問題がありそうだったのは昨日書いたとおりである。 解決策を模索する。

一応解決したので、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 initState
varCreateというのは a -> Var a の型を持つ関数で、Varは
varGet :: Var a -> IO a
varSet :: Var a -> a -> IO ()
などの操作が行える。 これをみんなで共有して状態を読み書きしている。

しかし、これはうれしくない。 wxHaskellでは単純なIO()のコマンドのコールバックなのでそうせざるを得ないが、 なんとか隠蔽して関数的なレイヤーを構築すべきであろう。

上の電卓ではとりあえず最終的な計算部分だけは関数的になるように頑張って見た。 しかし、もっと何というか関数的に宣言的に記述したいのである。 コマンドの羅列で作ってると、これは手続き言語か、と思えてくる。 まぁ、もうちょっと設計を頑張らないと。

第三章 日本語は難しきものと知りたり 〜Haskell処理系の光と影〜

突然だが、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バイナリを。 (圧縮+サブシステム書き換えで割かしまともにしたやつ)

第四章 First step toward the game programming

Haskellでゲーム計画もいよいよ大詰めである。 これまでHaskellではGUIアプリを作ったことが無かったため (もっとも、日本でHaskellでGUIアプリを作ったという話を聞いたことが無いけど) その道程は困難を極めたが、これまでにひとつづつその障壁を取り除いてきた。 最初は私も本当にまともにゲームが組めるか不安でならなかったのだが、 ようやく行けそうだ、というような感覚が湧いてきたのである。

今回はゲームを実装する上で重要な入出力処理と リアルタイムキー入力及び画像出力、さらにスピードコントロールを考える。

というわけで このあたりで一通り雛形が完成した。説明が適当すぎたけど。 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に侵食されずに記述できた。 描画も適当にビットマップ描画するだけなので適当に並べ立て。 昨日のプログラム、あそこまで書けていれば、今日は特に困ることは無かった。

なんというか、すんなり過ぎてあまり話題が無いのだが… とりあえず、作ってて思ったことなど。

説明・考察はこの辺で。 成果物 を公開しておきます。

ブロックの絵とか、http://www.linkfever.net/game/tetris.html このページから ちょっと使わせてもらったんだけど…。まずかったら書き直します。 というか、このページほとんど誰も見てないから大丈夫だよね。

結局、ゲームのシステムの周りはコマンドの羅列になってしまったような 感じがするのだが、一つ特筆すべき点があった。 このプログラム、ほとんどバグが出なかったのである。 GHCにはソースレベルデバッガが無い(と思う)のだが、 そもそもデバッグの必要が無かった。 なんだか今まですっかり忘れていたが、 この点だけでもHaskellを使う大きな利点ではないかと思った。 (まぁ、コード自体が小さいんだけど…)



[PR]国仲涼子さんの美肌のヒミツ:蜂蜜配合の美肌スキンケア≪ハニーラボ≫