2次元格子ランダムウォーク

Haskellで2次元格子ランダムウォークを書いてみた。
全然、Haskellで書いた気がせず不満。
IOと評価を分けたいが次の課題。

import Data.IORef
import Control.Applicative
import Data.Foldable
import System.Random.Mersenne
import System.Exit (exitWith, ExitCode(ExitSuccess))
import Graphics.UI.GLUT as GLUT
import Graphics.Rendering.FTGL as FTGL

--Applicativeを使って見やすい表現に
instance Num a => Num (Vertex3 a) where
    v1 + v2 = (+) <$> v1 <*> v2
    v1 - v2 = (-) <$> v1 <*> v2
    v1 * v2 = (*) <$> v1 <*> v2 -- 内積やスカラー積を求めるときに使う
    abs = undefined
    signum = undefined
    fromInteger = undefined

data State = State {
      mtGen   :: MTGen,                   -- 乱数生成の文脈
      points  :: IORef [Vertex3 GLfloat], -- 軌跡
      counter :: IORef Int,               -- 試行回数
      limit   :: Int,                     -- 最大試行回数
      font    :: FTGL.Font,               -- Bitmap Fontの情報
      xAccumulate :: IORef Double,        -- x変位の積算値
      yAccumulate :: IORef Double,        -- y変位の積算値
      xSquareAccumulate :: IORef Double,  -- (x変位)^2の積算値
      ySquareAccumulate :: IORef Double   -- (y変位)^2の積算値
    }

--内積
dot :: (Foldable t, Num (t a), Num a) => t a -> t a -> a
dot v1 v2 = Data.Foldable.foldl1 (+) v'
    where 
      v' = v1 * v2

--スカラ積
scala :: (Functor t, Num a, Num (t a)) => a -> t a -> t a
scala s v = (* s) <$> v

--タイマインターバル(msec)
timerInterval = 100

makeState :: Int -> FTGL.Font -> IO State
makeState limit' font' = do 
  newGen <- newMTGen Nothing
  initPoints <- newIORef [ Vertex3 100 100 0]
  newCounter <- newIORef 0
  newX <- newIORef 0
  newY <- newIORef 0
  newsX <- newIORef 0
  newsY <- newIORef 0
  return $ State { 
               mtGen   = newGen, 
               points  = initPoints, 
               counter = newCounter,
               limit = limit',
               font  = font',
               xAccumulate = newX,
               yAccumulate = newY,
               xSquareAccumulate = newsX,
               ySquareAccumulate = newsY
             }

display :: State -> DisplayCallback
display state = do
  points <- readIORef $ points state
  clear [ ColorBuffer ]
  -- 単位行列を読み込む
  loadIdentity
  
  -- 頂点配列設定
  -- presevingMarixが行列のPush/Popを行なってくれる
  GLUT.preservingMatrix $ do 
                  color (Color3 1 1 1 :: Color3 GLfloat) -- 色は白
                  -- renderPrimitiveが glBegin() - glEnd
                  GLUT.renderPrimitive LineStrip $ Prelude.mapM_ vertex [
                           Vertex3 200  0    0,
                           Vertex3 0    0    0,
                           Vertex3 0    200  0 :: Vertex3 GLfloat]
                  color (Color3 1 0 0 :: Color3 GLfloat) -- 色は赤
                  GLUT.renderPrimitive LineStrip $ Prelude.mapM_ vertex points
  GLUT.preservingMatrix $ do
                  color (Color3 1 1 1 :: Color3 GLfloat) -- 色は白
                  rasterPos (Vertex3 0 200 0::Vertex3 GLfloat)
                  nowCount <- get $ counter state
                  FTGL.renderFont (font state) ("N=" ++ (show nowCount)) All                 
                  xAcc <- get $ xAccumulate state
                  yAcc <- get $ yAccumulate state
                  xsAcc <- get $ xSquareAccumulate state
                  ysAcc <- get $ ySquareAccumulate state
                  let xAvg = xAcc/(fromIntegral $ nowCount)
                      yAvg = yAcc/(fromIntegral $ nowCount)
                      xsAvg = xsAcc/(fromIntegral $ nowCount)
                      ysAvg = ysAcc/(fromIntegral $ nowCount)
                      xSigma = xsAvg - xAvg ** 2
                      ySigma = ysAvg - yAvg ** 2
                  rasterPos (Vertex3 0 196 0::Vertex3 GLfloat)
                  FTGL.renderFont (font state) ("<x>=" ++ (show xAvg)) All
                  rasterPos (Vertex3 65 196 0::Vertex3 GLfloat)
                  FTGL.renderFont (font state) ("<y>=" ++ (show xAvg)) All
                  rasterPos (Vertex3 0 192 0::Vertex3 GLfloat)
                  FTGL.renderFont (font state) ("<x^2>=" ++ (show xsAvg)) All 
                  rasterPos (Vertex3 65 192 0::Vertex3 GLfloat)
                  FTGL.renderFont (font state) ("<y^2>=" ++ (show xsAvg)) All 
                  rasterPos (Vertex3 0 188 0::Vertex3 GLfloat)
                  FTGL.renderFont (font state) ("Sigma_x=" ++ (show xSigma)) All
                  rasterPos (Vertex3 65 188 0::Vertex3 GLfloat)
                  FTGL.renderFont (font state) (" Sigma_y=" ++ (show ySigma)) All 
  swapBuffers

-- ウインドウサイズが変更されたときに呼ばれる
reshape:: ReshapeCallback
reshape size = do
  viewport $= (Position 0 0, size) -- ウインドウ全体を使う
  
  --射影行列の設定
  matrixMode $= Projection
  loadIdentity
  --正射影を使います
  --二次元平面を使いたいのでこれでOK
  ortho (-5) 205 (-5) 205 (-1) 1
  --Modelviewモードに戻す
  matrixMode $= Modelview 0
  loadIdentity

--Keyboard入力 ESCだけ引き取り
keyboardMouse :: State -> KeyboardMouseCallback
keyboardMouse _ (Char '\27') Down _ _ = exitWith ExitSuccess
keyboardMouse _ _ _ _ _ = return ()

update::State -> IO()
update state = do
  updatePos <- random (mtGen state) :: IO Int
  counter' <- get $ counter state 
  lastPoints <- get $ points state
  let dirc = updatePos `mod` 4 
      newPos = decideNewPos dirc $ last lastPoints
      newPoints = lastPoints ++ [newPos]
      limit' = limit state
  statistic state dirc
-- 終了判定
  if isEnd counter' limit' newPos then
      do
        putStr "reach limit:"
        print limit'
  else 
      do
        writeIORef (points state) newPoints
        modifyIORef (counter state) (+1)
        GLUT.addTimerCallback timerInterval $ update state
  GLUT.postRedisplay Nothing

  where
    decideDelta :: Int -> Vertex3 GLfloat
    decideDelta 0 = Vertex3 1     0  0 :: Vertex3 GLfloat
    decideDelta 1 = Vertex3 (-1)  0  0 :: Vertex3 GLfloat
    decideDelta 2 = Vertex3 0     1  0 :: Vertex3 GLfloat
    decideDelta 3 = Vertex3 0   (-1) 0 :: Vertex3 GLfloat
    decideDelta _ = Vertex3 0     0  0 :: Vertex3 GLfloat
    decideNewPos :: Int -> Vertex3 GLfloat -> Vertex3 GLfloat
    decideNewPos n lastPos = lastPos + decideDelta n
    --統計情報を取るための関数
    statistic :: State -> Int -> IO ()
    statistic state n = do
      let x' = (fromRational.toRational.dot ex $ decideDelta n) :: Double
      let y' = (fromRational.toRational.dot ey $ decideDelta n) :: Double
      modifyIORef (xAccumulate state) (+ x')
      modifyIORef (yAccumulate state) (+ y')
      modifyIORef (xSquareAccumulate state) (+ x'**2)
      modifyIORef (ySquareAccumulate state) (+ y'**2)
      return ()
    ex = Vertex3 1 0 0 -- xの基底
    ey = Vertex3 0 1 0 -- yの基底
    --終了判定指定した回数または、壁にぶつかれば終了
    isEnd :: Int -> Int -> Vertex3 GLfloat -> Bool
    isEnd c l p = (c >= l) || 
                  dot ex p == 200 || 
                  dot ey p == 200 || 
                  dot ex p == 0   || 
                  dot ey p == 0  

main::IO ()
main = do 
  -- ウインドウタイトルと引数を取る
  (progName, _args) <- GLUT.getArgsAndInitialize
  let limit = if null _args then 10000
              else read $ head _args :: Int
  -- $=はData.StateVerの演算子
  GLUT.initialDisplayMode    $= [DoubleBuffered, RGBMode]
  GLUT.initialWindowSize     $= Size 800 800
  GLUT.initialWindowPosition $= Position 100 100
  GLUT.createWindow "2D Simple Random Walk"
  font <- FTGL.createBitmapFont "/usr/share/fonts/meguri/meguri.ttf"
  FTGL.setFontFaceSize font 18 18
  state <- makeState limit font
  --背景色は黒
  GLUT.clearColor $= Color4 0 0 0 0                
  -- displayコールバックを登録.描画が指示されたときに発生
  GLUT.displayCallback $= display state 
  -- windowSizeなどが変更されたときに発生するEventCallback                
  GLUT.reshapeCallback $= Just reshape
  GLUT.keyboardMouseCallback $= Just (keyboardMouse state)
  -- TimerCallback登録
  GLUT.addTimerCallback timerInterval $ update state
  mainLoop

参考にしたpage
プログラミング/Haskell/GLUT - Flightless wing
zmx.jp
GLUTによる「手抜き」OpenGL入門