Fortranと呼ばれる古代言語を書き連ねる日々に嫌気が差したので、副作用という穢れを払うためHaskellの力を借りることにしました.
いまもHaskellの勉強中なのですが、結局モナドは、関数や値を包み込みこんで<<=
といった演算子や特定の関数で操作を行うことを明示する、OOPでいうカプセル化の概念に近いものなのかな?
まぁやっぱりプログラミングを学ぶには見るより、書くのが一番だと思うので、コンソール上で動作する簡単なLifeGameを書いてみました.
##ゲーム状態の定義
フィールドの状態としてはセルをその座標Pos
型で示し、全体としてはBoard
型で管理します.
Board
は生きたセルを要素とするPos
型のリストです. また必要なライブラリもimportしておきます.
import Control.Monad
import System.Random
type Pos = (Int, Int)
type Board = [Pos]
##フィールドのサイズ
変更しやすくするため横幅width
と縦幅height
も関数で定義しておきます.
width :: Int
width = 50
height :: Int
height = 50
##各セルの状態チェック
次にPos
型の座標からBoard
内にそのセルがあるかをチェックし、そのセルが生きているかをBool
型で返す関数isAlive
を用意します. また逆にisEmpty
関数はセルにライフがいないかを返します.
isAlive :: Board -> Pos -> Bool
isAlive b p = p `elem` b
isEmpty :: Board -> Pos -> Bool
isEmpty b p = not (isAlive b p)
##周りのセル取得
neighbers
関数はセル座標Pos
からその周り8方向のセルを取得します. 今回はフィールドから外に出ると逆側に出る周期条件を採用しているため、wrap
関数で境界部分の周囲のセル取得に対応します.
wrap :: Pos -> Pos
wrap (x, y) = (((x-1) `mod` width) + 1,
((y-1) `mod` height)+ 1)
neighbers :: Pos -> [Pos]
neighbers (x, y) = map wrap [(x-1, y-1), (x, y-1),
(x+1, y-1), (x-1, y),
(x+1, y), (x-1, y+1),
(x, y+1), (x+1, y+1)]
##次世代のライフ
liveneighbers
関数は周囲の生きたセルの数を「Pos
型の引数から返す関数」を返す関数です.
そしてsurvivors
は現世代から次世代で生き残るセルのリストを返します.
一方births
は次世代で新たにライフが生まれるセルのリストを返します.
births
内で使っているrmdups
はリストから重複要素を除いて1つにします.
そして最後にsurvivors
とbirths
からnextgen
で次世代のライフを取得します.
liveneighbers :: Board -> Pos -> Int
liveneighbers b = length . filter (isAlive b) . neighbers
survivors :: Board -> [Pos]
survivors b = [p | p <- b, liveneighbers b p `elem` [2, 3]]
rmdups :: Eq a => [a] -> [a]
rmdups [] = []
rmdups (x:xs) = x : rmdups (filter (/= x) xs)
births :: Board -> [Pos]
births b = [p | p <- rmdups (concatMap neighbers b),
isEmpty b p,
liveneighbers b p == 3]
nextgen :: Board -> Board
nextgen b = survivors b ++ births b
##描画用関数
次にコンソールに表示するための関数を用意します.
cls
はコンソールの画面をクリアします. そしてgoto
でカーソルの場所を移動させ、writeat
でライフを表示します. 実際に画面表示をするのはshowcells
で、アクションリストの要素アクションを順次実行するseqn
関数を使って描画していきます.
cls :: IO ()
cls = putStr "\ESC[2J"
goto :: Pos -> IO ()
goto (x, y) = putStr ("\ESC[" ++ show y ++ ";" ++ show x ++ "H")
writeat :: Pos -> String -> IO ()
writeat p xs = do goto p
putStr xs
seqn :: [IO a] -> IO ()
seqn [] = return ()
seqn (a:as) = do a
seqn as
showcells :: Board -> IO ()
showcells b = seqn [writeat p "O" | p <- b]
##ゲームサイクル
最後にゲーム本体を定義します.
wait
は世代間で描画の間を取るためだけの関数です.
lifegame :: Board -> IO ()
lifegame b = do cls
showcells b
wait 5000
lifegame (nextgen b)
##初期状態
lifegame
関数を呼ぶには初期状態のBoard
を与えてあげないと行けないので、それを返す関数を用意します.
makeLife
で乱数を使って任意の場所のセル座標を返します.
そしてinitBoard
で指定した数のライフを持つBoard
を取得します.今回はBoard
サイズの1/5の数としています.
makeLife :: IO Pos
makeLife = do
x <- getStdRandom $ randomR(1, width) :: IO Int
y <- getStdRandom $ randomR(1, height) :: IO Int
return (x, y)
initBoard :: IO Board
initBoard = replicateM (width * height `div` 5) makeLife
##完成!!!
main :: IO ()
main = lifegame =<< initBoard
さぁ実行してみましょう!!!!!
...........
はい!!!すばらしいですね!!!すばらしいです!!!
..........
閑話休題
関数型でまともにコード書いたのは初めてですが、haskellの型->型の型を強く意識させる仕様は関数の細分化を促し、そのため関数それぞれの独立性が強いように感じました. 安定性・安全性が高いといわれているのもなんだかわかる気がします. なにより単純な関数の組み合わせで複雑な処理が簡潔に書けるのは気持ちいいですね. 次は練習がてら前々からやろうと思ってたProject EulerをHaskellで書いてみようかな.