最近の開発手法に GG

今開発している Web アプリケーションは、Yesod という Haskellフレームワークを使用している。これが非常に難しい。嫌な汗が出るぐらいヤバイ。でも、避けて通れない道なので頑張る。

さて、それ以外の部分は:

  • CoffeeScript で UI を書く → JavaScript なんて書かない
  • テンプレートエンジンで HTML を生成する → HTML なんて書かない
  • OR マッパーで DB に問い合わせる → SQL なんて書かない

という感じになっていて非常にモダンな感じ。超今どきっぽい。ヤバイ。

JavaScript は僕の3大得意言語のひとつなんだけど、JavaScript はクソ言語*1なので、今どきは AltJS が普通に使われる。なんか悲しい。
僕は AltJS は使ったことがないんだけど、使ってみたい順に TypeScript、JSX、ghcjs という感じで、CoffeeScript は圏外だったけど、見てみた感じ Ruby っぽく Python っぽく、Haskell っぽくもなくない感じの言語っぽい。ちなみに PureScript をちょっと触ってみた。これは Haskell と見間違うね!

テンプレートから HTML を生成するのは、JSPASPPHP なんかがほぼ HTML の拡張なのに対し、今使用しているテンプレートエンジン Slim は、箇条書きにしか見えない。

以下 速習テンプレートSlim(HTML作成編) - Qiita より引用。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Sample File</title>
  </head>
  <body class="sample">
    <div id="contents">
      <h1>Sample File</h1>
      <img alt="yterajima" src="http://www.e2esound.com/images/yterajima.jpg" />
      <p>テキストテキストテキスト</p>
      <p>テキストテキストテキスト
        テキストテキストテキスト</p>
    </div>
  </body>
</html>

これが Slim だとこうなる。

doctype html
html
  head
    meta charset="utf-8"
    title Sample File
  body.sample
    #contents
      h1 Sample File

      img src="http://www.e2esound.com/images/yterajima.jpg" alt="yterajima"

      p テキストテキストテキスト

      p
        | テキストテキストテキスト
          テキストテキストテキスト

ただこれ、ソースコードを修正してブラウザで確認するまでが面倒でしょ。

いえいえ、LiveReload という仕組みを使用すればソースコードの修正が監視されて、ブラウザが自動的にリロードします。ブラウザが!自動的にですよ、奥さん!

OR マッパーは仕組みがよくわかっていないけど、多分 Yesod-persistent の機能なんじゃないなぁ、と想像。ぶったまげたのは SQL を書かないことより、データベースにオブジェクトがそっくりそのまま格納できちゃうこと。おそらく 型を Show と Read のインスタンスにしておけば入れられるようになるんだと思う。

*1:異論は認める

Google スプレッドシートの小技

今日は Google スプレッドシートの技を習得したぞ!!

importrange 関数は、他のスプレッドシートの範囲を取り込む関数。
filter 関数は、同一のスプレッドシートの特定範囲を条件で絞り込む関数。

これを組み合わせれば、マスターデータから条件で絞り込んだものだけを他のスプレッドシートに抽出し、マスターは共有せず、抽出した別スプレッドシートだけを閲覧権限で共有するということをすれば、簡単で安全にデータ共有が可能になる。

僕はマスターだけ更新する、他者は別スプレッドシートだけ参照する。

必ず参照権限にしなければならない。その理由は、importrangeを書き換えると、別の領域が取り込めてしまうためだ。

VMware Fusion で共有フォルダがマウントできない

以下エントリで解決しました。

VMWareFusionでフォルダ共有ができなくなった場合の対処 - Qiita

ただし、いろいろといじったせいかもしれませんが、ホスト・ゲスト間のコピー・ペーストやファイルのドラッグアンドドロップなどが機能しなくなりました。

面談でググるな

2015 年 4 月 1 日から、新しい会社に所属します。

週休 4 日という大変面白い会社です。実際には、その休みに副業を行うので、前の会社よりも忙しいです。

忙しいですが、仕事を自分の裁量で選択できるというのはとても大きく、ストレスはないです。また、新しい会社では、業務として Haskell に携われるので、モチベーションが非常に高いです。

転職時に、いくつかの会社と面談を行い、面談時に質疑応答で技術的な受け答えをするのですが、面接官がいちいち僕の発言内容をググるのが最悪でした。

なにそれお前?
お前、先生がいないと面談もできないの?

面談に同席する技術者にこの傾向が多いようです。僕が面談を行うことがあるなら気をつけようと強く思った反面教師でした。

Haskell と SDL #2

Haskell と SDL - satosystemsの日記 の続きです。

マウスイベントをハンドリングして画像を描画するようにしてみました。

今までは main 関数内に直接記述していた描画コードを、以下のように関数として切り出しました。

drawImage :: SDL.Window -> Ptr SDL.Surface -> Int -> Int -> IO ()
drawImage window image x y = do
  surface <- SDL.getWindowSurface window
  width <- liftM SDL.surfaceW $ peek image
  height <- liftM SDL.surfaceH $ peek image
  format <- liftM SDL.surfaceFormat $ peek surface
  color <- SDL.mapRGB format 0xFF 0xFF 0xFF
  SDL.fillRect surface nullPtr color
  rect <- new $ SDL.Rect
   (fromIntegral (getCenter x width))
   (fromIntegral (getCenter y height))
   width
   height
  SDL.blitSurface image nullPtr surface rect
  free rect
  SDL.updateWindowSurface window
  return ()
  where
    getCenter xy wh = xy - ((fromIntegral wh) `div` 2)

これで、window の x y 座標に image を中央寄せして描画する汎用関数になりました。

繰り返し呼び出される関数なので、アロケートしたメモリは free で開放を忘れないようにしなければなりません。


イベント待ち関数は以下のようになりました。

waitEvent :: SDL.Window -> Ptr SDL.Surface -> IO ()
waitEvent window image = do
  (rc, event) <- sdlWaitEvent
  if rc == 1
    then case event of
      (SDL.QuitEvent _ _)                      -> return ()
      (SDL.MouseMotionEvent _ _ _ _ 1 x y _ _) -> drawImage window image (fromIntegral x) (fromIntegral y) >> waitEvent window image
      (SDL.MouseButtonEvent _ _ _ _ _ 1 _ x y) -> drawImage window image (fromIntegral x) (fromIntegral y) >> waitEvent window image
      _                                        -> waitEvent window image
    else print $ "SDL.waitEvent error:" ++ (show $ fromEnum rc)

MouseMotionEvent と MouseButtonEvent を待っています。不要な詳細は _ で無視しているのは良いとして、いきなり 1 と書かれたパターンマッチは何かというと、マウスボタンが押されている状態を表しています。つまり MouseMotionEvent ならドラッグ、MouseButtonEvent ならボタンプレスだけをハンドリングするようにしています。

後は、画像描画関数を呼び出して、再びイベント待ち関数を呼び出す、という流れです。


全体はこのような感じです。

module Main where

import qualified Graphics.UI.SDL as SDL
import Control.Monad
import Data.Either
import Foreign.C.String
import Foreign.C.Types
import Foreign.Marshal.Alloc
import Foreign.Marshal.Utils
import Foreign.Ptr
import Foreign.Storable

main :: IO ()
main = do
  sdlInit SDL.SDL_INIT_EVERYTHING >>= either sdlError return
  window <- sdlCreateWindow "hello" 640 480 >>= either sdlError return
  path <- newCString "hello.bmp"
  image <- SDL.loadBMP path
  free path
  drawImage window image 320 240
  waitEvent window image
  SDL.destroyWindow window
  SDL.quit

sdlInit :: SDL.InitFlag -> IO (Either String ())
sdlInit flag = do
  rc <- SDL.init flag
  return $ if rc == 0
    then Right()
    else Left $ "SDL.init error:" ++ (show $ fromEnum rc)

sdlCreateWindow :: String -> CInt -> CInt -> IO (Either String SDL.Window)
sdlCreateWindow windowTitle width height =
  withCAString windowTitle $ \title -> do
    window <- SDL.createWindow
      title
      SDL.SDL_WINDOWPOS_UNDEFINED
      SDL.SDL_WINDOWPOS_UNDEFINED
      width
      height SDL.SDL_WINDOW_SHOWN
    return $ if window /= nullPtr
      then Right window
      else Left "SDL.createWindow returns null pointer"

sdlWaitEvent :: IO (CInt, SDL.Event)
sdlWaitEvent = alloca $ \ptr -> do
  rc <- SDL.waitEvent ptr
  event <- peek ptr
  return (rc, event)

waitEvent :: SDL.Window -> Ptr SDL.Surface -> IO ()
waitEvent window image = do
  (rc, event) <- sdlWaitEvent
  if rc == 1
    then case event of
      (SDL.QuitEvent _ _)                      -> return ()
      (SDL.MouseMotionEvent _ _ _ _ 1 x y _ _) -> drawImage window image (fromIntegral x) (fromIntegral y) >> waitEvent window image
      (SDL.MouseButtonEvent _ _ _ _ _ 1 _ x y) -> drawImage window image (fromIntegral x) (fromIntegral y) >> waitEvent window image
      _                                        -> waitEvent window image
    else print $ "SDL.waitEvent error:" ++ (show $ fromEnum rc)

sdlError :: String -> IO a
sdlError message = do
  errMsg <- SDL.getError >>= peekCString
  fail (message ++ " (" ++ errMsg ++ ")")

drawImage :: SDL.Window -> Ptr SDL.Surface -> Int -> Int -> IO ()
drawImage window image x y = do
  surface <- SDL.getWindowSurface window
  width <- liftM SDL.surfaceW $ peek image
  height <- liftM SDL.surfaceH $ peek image
  format <- liftM SDL.surfaceFormat $ peek surface
  color <- SDL.mapRGB format 0xFF 0xFF 0xFF
  SDL.fillRect surface nullPtr color
  rect <- new $ SDL.Rect
   (fromIntegral (getCenter x width))
   (fromIntegral (getCenter y height))
   width
   height
  SDL.blitSurface image nullPtr surface rect
  free rect
  SDL.updateWindowSurface window
  return ()
  where
    getCenter xy wh = xy - ((fromIntegral wh) `div` 2)

Unicode の正規化でハマった話

まずは以下のコードを見てください。

import java.io.File
import java.io.FileOutputStream

val file1 = new File("\u30C9ラえもん.txt")
val fos1 = new FileOutputStream(file1)
fos1.write("ぼく、ドラえもん。".getBytes("UTF-8"))
fos1.close()

val file2 = new File("\u30C8\u3099ラえもん.txt")
val fos2 = new FileOutputStream(file2)
fos2.write("ぼく、ドラえもん!".getBytes("UTF-8"))
fos2.close()

本エントリとは関係ないですが、こうしたちょっとしたコードを書くのに Scala は非常に良いですね。

さて、「ドラえもん.txt」というファイルを 2 回作成しようとしています。わかりづらいですが、ファイル内の最後が「。」なのか「!」なのかの違いがあります。

最初に作成しようとしているファイルの「ド」は、普通の「ド」、次に作成しようとしているファイルの「ド」は、「ト」+「゙」の結合文字列です。この場合、「ト」を基底文字、「゙」を結合文字と呼びます。

これを Mac で実行すると、「ドラえもん.txt」というファイルがひとつだけ作成されます。最初に作成されたファイルを、2 番目に作成したファイルで上書きした形です。

これを Windows で実行すると、「ドラえもん.txt」と「ドラえもん.txt」というファイルが作成されます(片方は単一のド、片方は合成文字列のドです)。

また、生成されたファイルを Mac なら:

ls > ls.txt

Windows なら:

chcp 65001
dir /b > dir.txt

とリダイレクトすると、Mac なら分解されて、Windows ならそれぞれ適切に結果に保存されます(Windows はchcp でコマンドプロンプト文字コードUTF-8 に変換する必要があります)。

MacWindows でこうした結果になるのは、それぞれのプラットフォームで Unicode の正規化の仕組みが異なるからです。

Mac は NFD (Normalization Form Canonical Decomposition) という正規化形式が使用され、これは合成文字列を分解して正規化するのに対し、WindowsNFC (Normalization Form Canonical Composition) という、分解した後結合して正規化するためです。

このような違いにより、どのような問題が発生するでしょうか。


Windows から合成文字列のファイルを Mac に持ってくると、Mac では扱えなくなります。

具体的には、該当ファイルは Finder で表示されません。ターミナルで cp などを行おうとしても、ファイルが存在しないと言われエラーが発生します。もし、ふたつのファイルが同一フォルダに存在する場合、見えるはずの合成文字列ではないファイルも Finder から見えなくなります単に合成文字列のファイルは見えなくなります。

上図は、Finder ではひとつしかないけど、ターミナルからはふたつ見えています。

Mac 上では合成文字列の「ドラえもん.txt」は適切に扱えないのに、何故かリダイレクト結果は合成文字列なので、リダイレクト結果を元にプログラミングをすると、期待通りに動作しない可能性があります。

Windows では、合成文字列ではないファイル名で検索すると、合成文字列ではないファイルだけがヒットし、合成文字列のファイル名で検索すると、検索結果にヒットしません。

合成文字列でなければ見つかります。

合成文字列だと見つかりません。

ファイル名は何かの拍子で合成文字列版に変わってしまうことがあり(おそらく私が WindowsMac で使用している ZIP ツールか、受け渡ししている相手の Windows の ZIP ツール)、ファイルはあるのに検索しても見つからない、ということが発生します。見つからなかったことで、お客様相手に、送った・受け取ってない、みたいな問題になると大変です。


MacWindows でのファイルの受け渡しに関するトラブルは、今に始まった話ではなく、過去は過去で Mac バイナリや文字コードに関する注意が必要でしたが、今どきは Unicode に統一されてハッピーという訳にはいかず、やはり気をつけていかなければならない、というお話でした。

補足:

Dropbox は、事前に Unicode エンコードの競合を検知してくれる機能があり、ファイル名を「ドラえもん (Unicode エンコードの競合).txt」などと変更してくれます。

LinuxWindows と同じ正規化形式なので、MacLinux でも同様の問題が発生する可能性があります。

最近のエディタは、合成文字列を一文字みたいに扱う傾向があります。Windows のメモ帳、Mac のテキストエディット、TextMateVim などなど。僕は秀丸のように、常に分解された状態で編集できるエディタが好みですね。

Haskell と SDL

この記事は Haskell Advent Calendar 2014 の 8 日目の記事です。

HaskellSDL を利用してゲーム的な何かを作成するための入門記事です。

環境構築(Haskell Platform)

Haskell の開発環境は Windows/Mac/Linux いずれでも構築可能です。僕は普段は Haskell のコーディングは Linux、特に Ubuntu 14.04 を使用することが多いですが、今回は Mac を選択しました。

Haskell Platform は最新を使用します。僕の Mac には少し古いバージョンが入っていたので:

$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.6.3

以下のコマンドで何がアンインストールされるかを確認し:

$ uninstall-hs only 7.6.3

以下のコマンドで実際にアンインストールします。

$ sudo uninstall-hs only 7.6.3 --remove

僕は加えて以下も行いました。

$ rm -rf ~/.cabal/ ~/.ghc/ ~/Library/Haskell/

その後 Haskell Platform の最新版をダウンロード・インストールして、バージョンを確認します。

$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.8.3

本当は Haskell Platform は複数のバージョンを混在させることも可能なのですが、ハマりそうなので使用するバージョンのみにしておきます。

環境構築(Xcode

XcodeApp Store からインストールした後、Xcode Command Line Developer Tools を以下のようにインストールします。

$ xcode-select --install

これは MacPorts を使用するために必要な手続きなのですが、Mac OS X のバージョンや Xcode のバージョンによって、手続きが異なる可能性があるので注意してください。もしこの手順が抜けていても、MacPorts を使用する際に警告してくれるはずです。

環境構築(MacPorts

次は、SDL を使用する環境を構築します。

SDL は現在 SDL 1.2 と SDL 2.0 のふたつのバージョンがあり、どちらも利用可能です。僕は SDL には全く詳しくないのですが、Haskell のライブラリの対応度で言えば、SDL 1.2 の方が充実しているようで、またチュートリアルも豊富ですが、僕はゼロから始めるので SDL 2.0 を使用してみようと思います。

まずは MacSDL 2.0 のライブラリをインストールする必要があります。

僕は MacPorts を利用しているので*1、まず以下のように MacPorts 自身をアップデートして:

$ sudo port selfupdate

バージョンを確認します。

$ port version
Version: 2.3.3

libsdl がインストールされているか確認します。

$ port installed | grep libsdl

入っていないようなので以下のように確認した後:

$ port search libsdl2

以下のようにインストールします。

$ sudo port install libsdl2 libsdl2_image pkgconfig

絵を出すぐらいはしたいなぁ、ということで libsdl2_image も一緒にインストールしました。pkgconfig は Haskell のライブラリである sdl2 が依存しているので、同時に入れてしまいます。

環境構築(Cabal)

Cabal で Haskell のライブラリである sdl2 と sdl2-image をインストールします。

まずは以下のコマンドで cabal をアップデートします。

$ cabal update
Downloading the latest package list from hackage.haskell.org
Skipping download: Local and remote files match.
Note: there is a new version of cabal-install available.
To upgrade, run: cabal install cabal-install

cabal 自身のアップデートを行う必要がある場合、以下のように cabal をアップデートします。

$ cabal install cabal-install

ここで注意が必要なのは、上記はパスの通っている/usr/bin/cabal を更新したのではなく、~/Library/Haskell/bin/cabal に新しい cabal をインストールした、という点です。なので ~/Library/Haskell/bin/ にパスを通して、こちらが使用されるように .bash_profile に以下のようなコードを追加します。

if [ -d ${HOME}/Library/Haskell/bin ] ; then
  export PATH=${HOME}/Library/Haskell/bin:${PATH}
fi

読み込み直しを忘れずに。

$ source ~/.bash_profile

もう一度 cabal update します。

$ cabal update

それでは以下のコマンドで、sdl2 と sdl2-image をインストールします。

$ cabal install sdl2 sdl2-image

環境構築(まとめ)

cabal install sdl2 sdl2-image まで、うまく行ったでしょうか。

なぜ、環境構築をこんなにバカ丁寧に解説するかというと、ひとつは Haskeller 以外の人にも読んでもらいたいから、もうひとつは環境構築は結構つまづくポイントだからです。

前述したとおり、僕は HaskellUbuntu 14.04 で書くことが多いのですが、SDL2 の開発環境を Ubuntu 上に構築するのは諦めました。まず、Ubuntu では aptitude を使用して必要なライブラリ等をリポジトリからインストールするのですが、libsdl2 は 2.0.2 というバージョンがインストールされます。一方で Haskell の sdl2 は、最新の 1.3.0 では libsdl2 2.0.3 以上を要求します。libsdl2 2.0.2 で済まそうとすると、6 つ前のバージョンである sdl2 1.0.2 までさかのぼらなければなりません。

一旦はこのバージョンを使用して進めようと思ったのですが、・・・

SDL2 ことはじめ

Haskell で SDL2 を触り始めてまず思ったのが、ドキュメントに説明が一切ないので、どうするんだこれ、ということです。

そしてわかったのは、Haskell の SDL2 の API は C 言語の SDL2 の API と 1 対 1 の関係にあり、関数の名前もほとんど同じなので、関数の仕様が知りたければ https://wiki.libsdl.org/ を見よ、ということでした。

ということで、開発に必須なドキュメントは以下になります。

SDL2 自体のチュートリアルとして、以下が参考になると思います。

sdl2 第一歩

sdl2 は Graphics.UI.SDL を import する必要があります。

今回のサンプルでは、以下のようにしました。

import qualified Graphics.UI.SDL as SDL 

C/C++ の SDL2 は最初に SDL_Init を呼び出して初期化する必要があります。Haskell だとこんなかんじです。

  rc <- SDL.init SDL.SDL_INIT_EVERYTHING
  if rc == 0 then <次の処理> else <エラー処理>

そうです。Haskell の sdl2 の APIC/C++API のラッパーなので、完全に手続きを書く必要があります。

この処理はこのままだと使いにくいので関数にします。

sdlInit :: SDL.InitFlag -> IO (Either String ()) 
sdlInit flag = do
  rc <- SDL.init flag
  return $ if rc == 0
    then Right()
    else Left $ "SDL.init error:" ++ (show $ fromEnum rc) 

SDL.init に渡しているのは初期化フラグで、SDL_INIT_EVERYTHING は SDL_INIT_TIMER、SDL_INIT_AUDIO、SDL_INIT_VIDEO、SDL_INIT_JOYSTICK、SDL_INIT_HAPTIC、SDL_INIT_GAMECONTROLLER、SDL_INIT_EVENTS すべてを初期化するという意味です。本当は必要なサブシステムだけをビット演算の論理和で渡せば良いのですが、今回はサンプルをシンプルにするために全部初期化しちゃいます。

次にウィンドウを開きます。C/C++ だと SDL_CreateWindow、Haskell だと SDL.createWindow です。

これも以下の様な関数にまとめました。

sdlCreateWindow :: String -> CInt -> CInt -> IO (Either String SDL.Window)
sdlCreateWindow windowTitle width height =
  withCAString windowTitle $ \title -> do
    window <- SDL.createWindow
      title
      SDL.SDL_WINDOWPOS_UNDEFINED
      SDL.SDL_WINDOWPOS_UNDEFINED
      width
      height SDL.SDL_WINDOW_SHOWN
    return $ if window /= nullPtr
      then Right window
      else Left "SDL.createWindow returns null pointer"

ウィンドウタイトル、X 座標、Y 座標、幅、高さ、フラグを渡して Window オブジェクトを取得します。最後のフラグは、フルスクリーンモードや OpenGL を使用するモード、最大化状態や非表示状態など色々あって、同様にビット演算の論理和で複数同時に指定できるようになっています。今回はウィンドウが表示されるフラグだけを指定しています。

プログラムの終了処理は以下のようにしました。

sdlWaitEvent :: IO (CInt, SDL.Event)
sdlWaitEvent = alloca $ \ptr -> do
  rc <- SDL.waitEvent ptr 
  event <- peek ptr 
  return (rc, event)

waitQuitEvent :: IO ()
waitQuitEvent = do
  (rc, event) <- sdlWaitEvent
  if rc == 1
    then case event of
      (SDL.QuitEvent _ _) -> return ()
      _                   -> waitQuitEvent
    else print $ "SDL.waitEvent error:" ++ (show $ fromEnum rc) 

ウィンドウを閉じるとプログラムが終了するようにしてあります。

これを、main から呼び出すようにした、コード全体が以下です。

module Main where

import qualified Graphics.UI.SDL as SDL
import Data.Either
import Foreign.C.String
import Foreign.C.Types
import Foreign.Marshal.Alloc
import Foreign.Ptr
import Foreign.Storable

main :: IO ()
main = do
  sdlInit SDL.SDL_INIT_EVERYTHING >>= either sdlError return
  window <- sdlCreateWindow "hello" 640 480 >>= either sdlError return
  waitQuitEvent
  SDL.destroyWindow window
  SDL.quit

sdlInit :: SDL.InitFlag -> IO (Either String ())
sdlInit flag = do
  rc <- SDL.init flag
  return $ if rc == 0
    then Right()
    else Left $ "SDL.init error:" ++ (show $ fromEnum rc)

sdlCreateWindow :: String -> CInt -> CInt -> IO (Either String SDL.Window)
sdlCreateWindow windowTitle width height =
  withCAString windowTitle $ \title -> do
    window <- SDL.createWindow
      title
      SDL.SDL_WINDOWPOS_UNDEFINED
      SDL.SDL_WINDOWPOS_UNDEFINED
      width
      height SDL.SDL_WINDOW_SHOWN
    return $ if window /= nullPtr
      then Right window
      else Left "SDL.createWindow returns null pointer"

sdlWaitEvent :: IO (CInt, SDL.Event)
sdlWaitEvent = alloca $ \ptr -> do
  rc <- SDL.waitEvent ptr
  event <- peek ptr
  return (rc, event)

waitQuitEvent :: IO ()
waitQuitEvent = do
  (rc, event) <- sdlWaitEvent
  if rc == 1
    then case event of
      (SDL.QuitEvent _ _) -> return ()
      _                   -> waitQuitEvent
    else print $ "SDL.waitEvent error:" ++ (show $ fromEnum rc)

sdlError :: String -> IO a
sdlError message = do
  errMsg <- SDL.getError >>= peekCString
  fail (message ++ " (" ++ errMsg ++ ")")

エラー発生時に呼び出す関数として sdlError を定義しました。SDL_GetError という関数で、エラー発生時の可読可能なエラーメッセージが取得できるので、利用しています。

SDL.delay は指定ミリ秒だけ処理が止まる、いわゆるスリープ関数です。指定した期間は必ず補償されますが、OS のスケジューリングにより、多少長くなることもあります。

SDL.destroyWindow は Window オブジェクトの破棄、SDL.quit は終了処理です。

これを以下のようにコンパイルします。

$ ghc -L/usr/lib hello.hs

ポイントとなるのがライブラリディレクトリの指定 -L/usr/lib です。これを指定しないと以下の様なリンクエラーが発生します。

$ ghc hello.hs
[1 of 1] Compiling Main             ( hello.hs, hello.o )
Linking hello ...
Undefined symbols for architecture x86_64:
  "_iconv", referenced from:
      _hs_iconv in libHSbase-4.7.0.1.a(iconv.o)
     (maybe you meant: _hs_iconv, _base_GHCziIOziEncodingziIconv_iconvEncoding8_info , _base_GHCziIOziEncodingziIconv_iconvEncodingzuloc1_closure , _hs_iconv_close , _base_GHCziIOziEncodingziIconv_iconvEncoding3_info , _base_GHCziIOziEncodingziIconv_iconvEncoding10_info , _base_GHCziIOziEncodingziIconv_iconvEncoding6_closure , _base_GHCziIOziEncodingziIconv_iconvEncoding10_closure , _base_GHCziIOziEncodingziIconv_iconvEncoding7_closure , _base_GHCziIOziEncodingziIconv_iconvEncoding2_closure , _base_GHCziIOziEncodingziIconv_iconvEncoding2_info , _base_GHCziIOziEncodingziIconv_iconvEncoding8_closure , _base_GHCziIOziEncodingziIconv_iconvEncodingzuloc1_info , _base_GHCziIOziEncodingziIconv_iconvEncodingzuloc_info , _hs_iconv_open , _base_GHCziIOziEncodingziIconv_iconvEncodingzuloc_closure , _base_GHCziIOziEncodingziIconv_iconvEncoding7_info , _base_GHCziIOziEncodingziIconv_iconvEncoding3_closure , _base_GHCziIOziEncodingziIconv_iconvEncoding6_info , _base_GHCziIOziEncodingziIconv_iconvEncoding9_closure , _base_GHCziIOziEncodingziIconv_iconvEncoding4_closure , _base_GHCziIOziEncodingziIconv_iconvEncoding9_info )
  "_iconv_close", referenced from:
      _hs_iconv_close in libHSbase-4.7.0.1.a(iconv.o)
     (maybe you meant: _hs_iconv_close)
  "_iconv_open", referenced from:
      _hs_iconv_open in libHSbase-4.7.0.1.a(iconv.o)
     (maybe you meant: _hs_iconv_open)
  "_locale_charset", referenced from:
      _localeEncoding in libHSbase-4.7.0.1.a(PrelIOUtils.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

どうやら libiconv は /usr/lib と /opt/local/lib に存在し、/opt/local/lib の libiconv には _iconv や _iconv_close などのシンボルが定義されていないようです(代わりに _libiconv や _libiconv_close などが定義されています)。なので、リンクするライブラリを /usr/lib にしてやる必要があります。

こうしてできた実行ファイルを起動すると、以下の様な画面が表示されます。

ようやくスタート地点に立てました。

SDL2 で Hello World

SDL はどうやらデフォルトで BMP の描画ができるようです。そして、デフォルトでは BMP の描画しかできないようです。そのため、SDL2_Image で PNG などの他のフォーマットが扱えるように拡張されているようです。

せっかくライブラリをインストールしたのですが、今回は基本を抑えるということで、以下の BMP を表示しようと思います。

まずは、ウィンドウの背景を白に塗りつぶすコードを追加します。

  surface <- SDL.getWindowSurface window
  format <- liftM SDL.surfaceFormat $ peek surface
  color <- SDL.mapRGB format 0xFF 0xFF 0xFF
  SDL.fillRect surface nullPtr color
  SDL.updateWindowSurface window

次に BMP を描画するコードを追加します。

  path <- newCString "hello.bmp"
  image <- SDL.loadBMP path
  rect <- new $ SDL.Rect 170 200 300 80
  SDL.blitSurface image nullPtr surface rect

実行すると以下の様な画面が表示されます。

コード全体は以下のようになります。

module Main where

import qualified Graphics.UI.SDL as SDL
import Control.Monad
import Data.Either
import Foreign.C.String
import Foreign.C.Types
import Foreign.Marshal.Alloc
import Foreign.Marshal.Utils
import Foreign.Ptr
import Foreign.Storable

main :: IO ()
main = do
  sdlInit SDL.SDL_INIT_EVERYTHING >>= either sdlError return
  window <- sdlCreateWindow "hello" 640 480 >>= either sdlError return
  surface <- SDL.getWindowSurface window
  format <- liftM SDL.surfaceFormat $ peek surface
  color <- SDL.mapRGB format 0xFF 0xFF 0xFF
  SDL.fillRect surface nullPtr color
  path <- newCString "hello.bmp"
  image <- SDL.loadBMP path
  rect <- new $ SDL.Rect 170 200 300 80
  SDL.blitSurface image nullPtr surface rect
  SDL.updateWindowSurface window
  waitQuitEvent
  SDL.destroyWindow window
  SDL.quit

sdlInit :: SDL.InitFlag -> IO (Either String ())
sdlInit flag = do
  rc <- SDL.init flag
  return $ if rc == 0
    then Right()
    else Left $ "SDL.init error:" ++ (show $ fromEnum rc)

sdlCreateWindow :: String -> CInt -> CInt -> IO (Either String SDL.Window)
sdlCreateWindow windowTitle width height =
  withCAString windowTitle $ \title -> do
    window <- SDL.createWindow
      title
      SDL.SDL_WINDOWPOS_UNDEFINED
      SDL.SDL_WINDOWPOS_UNDEFINED
      width
      height SDL.SDL_WINDOW_SHOWN
    return $ if window /= nullPtr
      then Right window
      else Left "SDL.createWindow returns null pointer"

sdlWaitEvent :: IO (CInt, SDL.Event)
sdlWaitEvent = alloca $ \ptr -> do
  rc <- SDL.waitEvent ptr
  event <- peek ptr
  return (rc, event)

waitQuitEvent :: IO ()
waitQuitEvent = do
  (rc, event) <- sdlWaitEvent
  if rc == 1
    then case event of
      (SDL.QuitEvent _ _) -> return ()
      _                   -> waitQuitEvent
    else print $ "SDL.waitEvent error:" ++ (show $ fromEnum rc)

sdlError :: String -> IO a
sdlError message = do
  errMsg <- SDL.getError >>= peekCString
  fail (message ++ " (" ++ errMsg ++ ")")

まとめ

僕は SDL も SDL2 も初めて触れるので、あまり変わらないかなと思っていましたが、Web 上には圧倒的に SDL の解説やサンプルが多く、特にこだわりがなければ SDL を使用するという選択の方が良いのではないかと思いました。

Haskell の sdl2 ライブラリは C/C++ の SDL2 ライブラリのバインディングなので、完全に手続きを書かなければなりません。また、要所要所で Ptr や CString などが要求されるため、煩わしさもあります。エラー発生時のリソース開放なども、かなり気をつけて設計しなければリークが発生してしまうでしょう。

そのため、sdl2 を使用した更に簡単にゲーム開発ができるライブラリがいくつも存在するようです。

今後はそうしたライブラリをちら見しつつ、sdl2 は基本なのでしっかりおさえて、作りかけのポーカーゲームを完成させようと思います。

*1:MacPorts は右記よりインストールしてください:https://www.macports.org/