HaskellでGUI #2 FliptItの改良:FFIの利用
(今回は GUI というより FFI な気がする.でも目的は GUI だからいいか.)
前回作成した GUI の FlipIt を改良した.
改良は以下の2点
- 周期を増加.
- 解を表示.
周期の増加
前回は
マスの変化が
白→黒→白→黒→…
だったが,今回はこの周期を変更できるようにした.
白→灰→黒→白→灰→黒→…
内部的には Bool の2次元配列を Int の2次元配列に変更しただけ.
描画の色を少し調整した程度.
ただ,解を求める際にガウスの消去法を使用するので,周期は素数でないと問題が生じる.
また,周期が大きすぎても,わけわかめになるだけなので,最大周期は7に設定している.
解を表示
ガウスの消去法をやるだけといえば,それだけ.
ただ,以前にC++で書いたことがある.
そこで,今回は Haskell の FFI(Foreign Function Interface) を使って外部コードを利用してみた.
しかし,C++ の関数を利用するのは C に比べて難しそうなので, 少しコードを書き換えて C の関数を利用することにした.
C の関数の用意
flipItSolver.h
#include "finiteGauss.h" int solve(int *x, int *b, int n, int q);
上の関数 solveで
int *x,[出力]解を記録するための配列
int *b, [入力]盤面の状態が記録された配列
int n, [入力]盤面のサイズ
int q, [入力]mod q で考える
という仕様.
上の関数 solve を Haskellで呼び出すには
{-# LANGUAGE ForeignFunctionInterface #-} import Foreign.C.Types (CInt) import Foreign.Ptr (Ptr) foreign import ccall "flipItSolver.h solve" c_solve :: Ptr CInt -> Ptr CInt -> CInt -> CInt -> IO CInt
とすれば良い.
Int -> CInt, CInt -> Int の変換は fromIntegral で可能.
しかし,ここで疑問が
(1) Ptr CIntってどうやって生成するのさ?
(2) Ptr CInt からどうやって値を読む?
(3) CInt にどうやって値を書く?
(4) 配列の場合は?
Haskell の Ptr の扱い
(1) Ptr a の作成
Foreign.Marshal.Alloc を使いましょう.
http://haskell.org/ghc/docs/latest/html/libraries/base-4.2.0.0/Foreign-Marshal-Alloc.html
malloc と free があるのでそれを使う.C と同じ感覚だと思う.
malloc :: Storable a => IO (Ptr a) free :: Ptr a => IO ()
(2)(3) Ptr a の読み書き
Foreign.Storable を使いましょう.
http://haskell.org/ghc/docs/latest/html/libraries/base-4.2.0.0/Foreign-Storable.html
peek :: Ptr a -> IO a poke :: Ptr a -> a -> IO ()
ちなみに,
peek は チラ見,のぞき見
poke は 突っ込む
という意味らしい. 関数の動作とも一致する.
(4) 配列の場合
Ptr a が 配列の場合には便利なインターフェースが用意されている.
http://haskell.org/ghc/docs/latest/html/libraries/base-4.2.0.0/Foreign-Marshal-Array.html
mallocArray :: Storable a => Int -> IO (Ptr a) newArray :: Storable a => [a] -> IO (Ptr a) peekArray :: Storable a => Int -> Ptr a -> IO [a] pokeArray :: Storable a => Ptr a -> [a] -> IO ()
これを使えば,[a] で書き込んだり,読んだりできる.
実際に,C の関数を Haskell で利用する
updateAns :: Board -> Board -> Int -> IO () updateAns board ans m = do n <- size board b <- newArray.map fromIntegral =<< getElems board x <- mallocArray $ n * n c_solve x b (fromIntegral n) (fromIntegral m) xs <- peekArray (n * n) x bounds <- getBounds ans zipWithM_ (writeArray ans) (range bounds) (map fromIntegral xs) free b free x
newArray と mallocArray で Ptr CInt を作成.
c_solve で C の関数 solve を呼びだして,解を x に記録.
x から [CInt] を読みだして,それを xs に格納.
xs の値を ans に zipWithM_ つかって書き込み.
free で確保したポインタを開放.
コンパイル
コンパイルが若干面倒.
今までは,ghc --make で特に何も考えずに楽々コンパイルできたが,Cの関数を使っている
それに対応しなくていはいけない.
gcc -c finiteGauss.c gcc -c flipItSolver.c ghc --make -O2 GFlipIt.hs flipItSolver.o finiteGauss.o -o flipIt
(finiteGauss は flipItSolverで利用している.)
ソースコード
http://dl.dropbox.com/u/662567/flipIt.tar.xz
FlipIt.hs
{-# LANGUAGE ForeignFunctionInterface #-} module FlipIt (Board, size, inBoard, flipPanel, reset, shuffle) where import Control.Monad (liftM, when, forM_, replicateM, zipWithM_) import Data.Array.IO (IOUArray, readArray, writeArray, getBounds, range, getElems) import System.Random (randomRIO) import Foreign.C.Types (CInt) import Foreign.Ptr (Ptr) import Foreign.Marshal.Alloc (free) import Foreign.Marshal.Array (newArray, mallocArray, peekArray) -- Monadic when and if whenM :: Monad m => m Bool -> m () -> m () whenM b f = b >>= flip when f type Board = IOUArray (Int, Int) Int size :: Board -> IO Int size = liftM (succ.fst.snd).getBounds inBoard :: Board -> (Int, Int) -> IO Bool inBoard board (x, y) = do n <- size board return $ 0 <= x && x < n && 0 <= y && y < n flipPanel :: Board -> Board -> Int -> (Int, Int) -> IO () flipPanel board ans m (x, y) = flipPanel_ board m (x, y) >> updateAns board ans m flipPanel_ :: Board -> Int -> (Int, Int) -> IO () flipPanel_ board m (x, y) = do whenM (inBoard board (x, y)) $ forM_ [(x, y), (x+1, y), (x, y+1), (x-1, y), (x, y-1)] $ \ix -> whenM (inBoard board ix) $ writeArray board ix.(`mod` m).succ =<< readArray board ix reset :: Board -> Board -> IO () reset board ans = do bounds <- getBounds board forM_ (range bounds) $ \ix -> writeArray board ix 0 >> writeArray ans ix 0 shuffle :: Board -> Board -> Int -> IO () shuffle board ans m = do n <- size board r <- randomRIO (n, n*n) [xs, ys] <- replicateM 2.replicateM r $ randomRIO (0, n-1) mapM_ (flipPanel_ board m) $ zip xs ys updateAns board ans m foreign import ccall "flipItSolver.h solve" c_solve :: Ptr CInt -> Ptr CInt -> CInt -> CInt -> IO CInt updateAns :: Board -> Board -> Int -> IO () updateAns board ans m = do n <- size board b <- newArray.map fromIntegral =<< getElems board x <- mallocArray $ n * n c_solve x b (fromIntegral n) (fromIntegral m) xs <- peekArray (n * n) x bounds <- getBounds ans zipWithM_ (writeArray ans) (range bounds) (map fromIntegral xs) free b free x
GFlipIt.hs
import Graphics.UI.Gtk hiding (fill) import Graphics.UI.Gtk.Gdk.EventM (EventM, EButton) import Graphics.Rendering.Cairo import Control.Monad (liftM, when, forM_) import Data.IORef import Data.Array.IO (newArray, readArray) import System.Environment (getArgs) import FlipIt (Board, size, flipPanel, reset, shuffle) updateCanvas :: DrawingArea -> Board -> Board -> Int -> IORef Bool -> EventM any Bool updateCanvas area board ans m cheat = do liftIO $ do win <- widgetGetDrawWindow area renderWithDrawable win $ drawBoard area board ans m cheat return True updateBoard :: DrawingArea -> Board -> Board -> Int -> IORef Bool -> EventM EButton Bool updateBoard area board ans m cheat = do liftIO $ do (x, y) <- widgetGetPointer area (sw, sh, pw, ph) <- boardGetLength area board flipPanel board ans m (div x (floor $ sw + pw), div y (floor $ sh + ph)) updateCanvas area board ans m cheat drawBoard :: DrawingArea -> Board -> Board -> Int -> IORef Bool -> Render () drawBoard area board ans m cheat = do setSourceRGB 0.5 0.5 0.5 paint (sw, sh, pw, ph) <- liftIO $ boardGetLength area board n <- liftIO $ size board c <- liftIO.readIORef $ cheat forM_ (sequence $ replicate 2 [0..n-1]) $ \[i', j'] -> do let [i, j] = map fromIntegral [i', j'] x <- liftIO.readRatio board $ (i', j') if x == 1 then setSourceRGB 1 1 1 else setSourceRGB (0.7 * x) (0.7 * x) x rectangle (sw * (i+1) + pw * i) (sh * (j+1) + ph * j) pw ph fill when c $ do y <- liftIO.readRatio ans $ (i', j') when (y < 1) $ do setSourceRGB (1 - y) 0.9 (0.6 - 0.6 * y) rectangle (sw * (i+6) + pw * i) (sh * (j+6) + ph * j) (pw - 10*sw) (ph - 10* sh) fill where ratio x = fromIntegral (m - 1 - x) / fromIntegral (m - 1) readRatio array = liftM ratio.readArray array boardGetLength :: DrawingArea -> Board -> IO (Double, Double, Double, Double) boardGetLength area board = do (_w, _h) <- widgetGetSize area n <- liftM fromIntegral $ size board let w = fromIntegral _w h = fromIntegral _h space = 0.05 / (n + 1) panel = 0.95 / n return (space * w, space * h, panel * w, panel * h) run :: Int -> Int -> IO () run _m n = do let m | notElem _m [2,3,5,7] = 2 | otherwise = _m initGUI -- win (Window) -- +--- vbox (VBox) -- +--- can (DrawingArea) -- +--- hbox (HBox) -- +-- cls (Button) -- +-- rst (Button) -- +-- shf (Button) -- +-- cht (Button) win <- windowNew win `set` [windowTitle := "FlipIt!", windowDefaultWidth := n * 50, windowDefaultHeight := n * 50 + 40, containerBorderWidth := 0] win `onDestroy` mainQuit vbox <- vBoxNew False 0 can <- drawingAreaNew hbox <- hBoxNew False 0 cls <- buttonNewWithLabel "Close" rst <- buttonNewWithLabel "Reset" shf <- buttonNewWithLabel "Shuffle" cht <- buttonNewWithLabel "Cheat" containerAdd win vbox boxPackStart vbox can PackGrow 0 boxPackStart vbox hbox PackNatural 5 boxPackStart hbox cls PackGrow 0 boxPackStart hbox rst PackGrow 0 boxPackStart hbox shf PackGrow 0 boxPackStart hbox cht PackGrow 0 widgetShowAll win board <- newArray ((0, 0), (n-1, n-1)) 0 ans <- newArray ((0, 0), (n-1, n-1)) 0 cheat <- newIORef False on can exposeEvent $ updateCanvas can board ans m cheat on can buttonPressEvent $ updateBoard can board ans m cheat on cls buttonPressEvent $ liftIO mainQuit >> return True on rst buttonPressEvent $ liftIO (reset board ans) >> updateBoard can board ans m cheat on shf buttonPressEvent $ liftIO (shuffle board ans m) >> updateBoard can board ans m cheat on cht buttonPressEvent $ liftIO (modifyIORef cheat not) >> updateBoard can board ans m cheat mainGUI main :: IO () main = do args <- getArgs if length args < 2 then run 2 5 else run (read $ args!!0) (read $ args!!1)
flipItSolver
fliptItSolver.h
#include "finiteGauss.h" int solve(int *x, int *b, int n, int q);
flipItSolver.c
#include "flipItSolver.h" int di[5] = {0, 1, 0, -1, 0}; int dj[5] = {0, 0, 1, 0, -1}; // Ax = b (mod q) を x について解く // ガウスの消去法を使用する // A は FlipIt の隣接行列 // 0 = 解無し, 1 = 解有り int solve(int *x, int *b, int n, int q) { int i, j, k, **a = (int**)malloc(sizeof(int*) * n * n); for (i = 0; i < n * n; ++i) { a[i] = (int*)malloc(sizeof(int) * n * n + 1); } for (i = 0; i < n * n; ++i) for (j = 0; j < n * n + 1; ++j) a[i][j] = 0; for (i = 0; i < n; ++i) for (j = 0; j < n; ++j) for (k = 0; k < 5; ++k) if (0 <= i + di[k] && i + di[k] < n && 0 <= j + dj[k] && j + dj[k] < n) { a[n * i + j][n * (i + di[k]) + (j + dj[k])] = 1; } for (i = 0; i < n * n; ++i) a[i][n * n] = b[i]; return gauss(a, x, n * n, n * n + 1, q); }
finiteGauss
finiteGauss.h
#include <stdio.h> #include <stdlib.h> int gauss(int **a, int *x, int m, int n, int q);
finiteGauss.c
#include "finiteGauss.h" // input : a, b // output : x, y s.t. ax + by = (符号付き)gcd(a, b) int extGcd(int a, int b, int *x, int *y) { if (b == 0) { *x = 1; *y = 0; return a; } int g = extGcd(b, a % b, y, x); (*y) -= (a / b) * (*x); return g; } // xn = 1 (mod p) int invMod(int n, int p) { int x, y, g = extGcd (n, p, &x, &y); if (g == 1) return x; else if (g == -1) return -x; else return 0; // gcd(n, p) != 1,解なし } // 有限体上の線型方程式系 Ax = b (mod q)を解く // a = [A | b]: m × n の係数行列 // x: 解を記録するベクトル // 計算量: O(min(m, n) * m * n) int gauss(int **a, int *x, int m, int n, int q) { int rank = 0, i, j, k, l, *pivot = (int*)malloc(sizeof(int) * n); // 前進消去 for (i = 0, j = 0; i < m && j < n-1; ++j) { int p = -1, tmp = 0; // ピボットを探す for (k = i; p < 0 && k < m; ++k) { if (a[k][j] != 0) p = k; // 有限体上なので非零で十分 } // ランク落ち対策 if (p == -1) continue; // 第i行と第p行を入れ替える for (k = j; k < n; ++k) tmp = a[i][k], a[i][k] = a[p][k], a[p][k] = tmp; // 第i行を使って掃き出す for (k = i+1; k < m; ++k) { tmp = - a[k][j] * invMod(a[i][j], q) % q; for (l = j; l < n; ++l) a[k][l] = (a[k][l] + tmp * a[i][l]) % q; } // 第i行を正規化: a[i][j] = 1 にする tmp = invMod(a[i][j], q); for (k = j; k < n; ++k) a[i][k] = a[i][k] * tmp % q; pivot[i++] = j, rank++; } // 解の存在のチェック for (i = rank; i < m; ++i) if (a[i][n-1] != 0) { free(pivot); return 0; } // 解をxに代入(後退代入) for (i = 0; i < rank; ++i) x[i] = a[i][n-1]; for (i = rank; i < n-1; ++i) x[i] = 0; for (i = rank-1; i >= 0; --i) { for (j = pivot[i] + 1; j < n-1; ++j) x[i] -= a[i][j] * x[j]; x[i] -= x[i] / q * q, x[i] = (x[i] + q) % q; // 0 <= x[i] < q に調整 } free(pivot); return 1; }