この記事をみて、なんとなく書こうと思いました。
elmのプログラムの思考の流れがわかるように書こうと思います。
この記事の方針
初学者向けに書いた。あと、ライフゲームの実装はわりと愚直にやったので、実際にはもっとなにかしら最適化すけど、わりと素直にかくようにした。たぶん素直なはず。
ライフゲームとは
まず、excelみたいな、四角がいっぱいならんでいて、白なら生きている、黒色なら死んでいるみたいな感じで、一つの四角が生き物として例えます。そして、四角の生き物の周りが、いなかったら死ぬ、いすぎても死ぬみたいな感じのものです。おおざっぱな説明ですね。セルオートマトンとか言ったりするらしい。
もっとこまかく言うと、lifegameは、四角の周辺に、生きているセルが2つあれば、生まれる、うまれていて2~3つあれば、生命が維持される、1匹以下、もしくは、4匹以上では死ぬみたいなものです。詳しくはググってください。
んで、今回はとりあえず、僕もライフゲームをつくってみようかと。
セルをプログラムで書く
さてさて、elmでライフゲームを書くとき、viscuitみたく、そのまま黒い四角の絵や、白い四角の絵がプログラムでそのまま扱えればいいんですが、そうじゃないので、白い(=生きている)四角と、黒い(=死んでいる)四角を、プログラムで扱えるようにしていきます。
んで、生きているセル(四角とかくのだるいんでもうセルって言いますね)、と死んでいるセルをは、たぶんC言語とかでかけば、まぁ0やら1で表わしたりするんでしょうが、elmの場合は大半は、死んでいるや生きているセルをtypeとかつかって表現しますね。
type CellState = Living | Dead
ああ、elmのシンタックスハイライトしてくれないんですね。Qiitaはクソが! (対応しました。Qiitaありがとう!)さて、↑のコードの説明なんですが、まぁ、データ型をつくってるんですね。んで、セルが生きているか死んでるかを表わします。
セルの位置や並びに関する操作の関数を書く
かるーい設計
さてさて、セルの並びについて、プログラムを書いていきましょう。ここで、まぁ、セルの並びを一次元配列とかで表わすか、一行をセルの配列として、行の配列(2次元配列みたいな感じ)にするかみたいな自由度があると思います。
でも、こういうのって、どっちで表わしても変わらない操作があります。たとえば、ある位置のセルの一個上のセルの状態を取得するとか。んで、そういったデータ型を先に書いておきましょう。また、一緒に、位置に関するデータ型もつくりましょう。
type CellWorld
type CellPosition
-- CellPositionにある、CellStateを求める
getState : CellPosition -> CellWorld -> CellState
-- CellPostionのCellStateを変更する。
setState : CellPostion -> CellState -> CellWorld -> CellWorld
-- 次世代を計算する
nextGeneration : CellWorld -> CellWorld
ついでにCellPositionに関する関数もかいておきましょう。
-- CellPositionの一個上のCellPositionを取得する。
upper : CellPosition -> CellPosition
-- CellPositionの一個下のCellPositionを取得する
lower : CellPosition -> CellPosition
-- CellPositionの右のCellPositionを取得する
right : CellPosition -> CellPosition
-- CellPositionの左のCellPositionを取得する
left : CellPosition -> CellPosition
こういうときに、scalaのtraitみたいなのがあればなぁ...とか思うんですが、まーしかたないですね。
ちなみに、こういうデータを作るとき、たぶん初学者は引数の順番に悩むと思うんですよね。たとえば、getState
やsetState
、の順番をかえて
getState : CellWorld -> CellPosition -> CellState
setState : CellWorld -> CellPosition -> CellState -> CellWorld
nextGeneration : CellWorld -> CellWorld
みたいにしてもいいんじゃね?という感じだと思うんですね。なんで、この順番にしているかって言えば、CellWorldに関する操作だからなんですよね。CellWorldに対して、なにかしら変更を加えたりする関数としてだからです。
こうすると、何が良いのでしょうか? たとえば、setStateを複数つかうことを考えましょう。たとえば、とあるCellとその右のCellのStateを変える関数をつくってみましょう。
たぶん関数型言語に慣れていないと、
setWithRightCell : CellPosition -> CellState -> CellWorld -> CellWorld
setWithRightCell position state world =
let
world1 = setState position state world
rightPosition = right position
world2 = setState rightPosition state world2
in
world2
とか、letを使わずに、
setWithRightCell : CellPosition -> CellState -> CellWorld -> CellWorld
setWithRightCell position state world =
setState (right Position) state (setState position state world)
ととかくと思います。で、チョットelmに慣れてくると。<|
をつかって
setWithRightCell : CellPosition -> CellState -> CellWorld -> CellWorld
setWithRightCell position state world =
setState (right position) state <| setState position state world
とかいたり。まぁ、そこまで括弧を嫌わんでも...という感じですが。そして、これは更に関数合成をつかって、
setWithRightCell : CellPosition -> CellState -> CellWorld -> CellWorld
setWithRightCell position state =
setState (right position) state << (setState position state)
とすることができます。またこれ>>
の順番を逆向きにして、
setWithRightCell : CellPosition -> CellState -> CellWorld -> CellWorld
setWithRightCell position state =
setState position state >> (setState (right position) state)
とすることができます。ちなみに、これ、stateも消すことができるのですが、これはまた今度。
これ、もっとふやして、このようにかくことができます。上下左右のStateを変えるとすると
setCross : CellPosition -> CellState -> CellWorld -> CellWorld
setCroll position state =
(setState position state)
>> (setState (right position) state)
>> (setState (left position) state)
>> (setState (upper position) state)
>> (setState (lower position) state)
みたいな感じにですね。関数合成はこのように、中間の状態に関する変数を無くせるようなメリットがあります。あと、>>
をつかえば処理の順番が上から見ていけば分かるようになります。
何が言いたいかと言えば、関数が合成しやすくなるように関数を定義していくと、簡潔に処理が書けるようになるということです。もし、逆の場合だと、おそらくflip
などを使い引数の順番を変えていくコードが多くなると思います。もし、flip
がたくさんでてきたら、関数の引数の順序を変えてみるといいかもしれません。
ここいらが、簡潔に書くための工夫ですね。ちなみに,CellWorld
は、ある意味文脈と捉えることができますね。こういったのは暗黙的に扱いたいので、引数を先にしておくってのが関数型のデザインとしてありそうだなぁと最近思いますね。
この辺異論はたくさんありそうです。ただ、あくまでも個人的な意見としてはこうがいいかなー?って感じです。もし、なんというか、文化が違えばなんか違う感じもしますね
かるーく実装
さてさて、実装していきますかね。CellWorldと、CellPositionのデータ型について書いていきます。
type alias CellWorld = Array (Array CellState)
type alias CellPosition = (Int, Int)
実装していきます。まずは、CellPositionの関数から
upper : CellPosition -> CellPosition
upper (x,y) = (x,y+1)
lower : CellPosition -> CellPosition
lower (x,y) = (x, y-1)
right : CellPosition -> CellPosition
right (x,y) = (x+1,y)
left : CellPosition -> CellPosition
left (x,y) = (x-1,y)
とくに疑問はないと思います。次は、getState
の関数をかいてみましょう。
-- worldを越えた位置は、端と端をつなげて解釈する。
getState : CellPosition -> CellWorld -> CellState
getState (x,y) world =
case get (y % (height world)) world of
Just row ->
case get (x % (width world)) row of
Just cellState -> cellState
None -> Dead
None -> Dead
うーん、なんかcaseが多くて嫌ですねぇ。これは、わざと書いてみました。もっとスッキリした書き方があります。Maybeの関数をつかって、
getState : CellPosition -> CellWorld -> CellState
getState (x,y) world =
Array.get (y % (height world)) world
|> Maybe.andThen (\row -> Array.get (x % (width world)) row)
|> Maybe.withDefault Dead
とかけば、スッキリします。このように、関数型言語で書くときは、ネストした構造をみたときに、なにかスッキリ書く方法があるかもしれないので、探してみましょう。わからなければ、なんかその辺にいる関数型Freakに聞いてみましょう。
ちなみに、widthとheightもちゃっかり定義してあります。
height : CellWorld -> Int
height world =
Array.length world
width : CellWorld -> Int
width world =
Array.get 0 world
|> Maybe.map Array.length
|> Maybe.withDefault 0
さて、setStateです。
getState : CellPosition -> CellWorld -> CellState
getState (x,y) world =
Array.get (y % (height world)) world
|> Maybe.andThen (\row -> Array.get (x % (width world)) row)
|> Maybe.withDefault Dead
このように工夫して、処理が順番にならんだら、読みやすくないですか?
最後にnextGeneration関数を書いてみましょう。
nextGeneration : CellWorld -> CellWorld
nextGeneration world =
Array.indexedMap (\x row -> Array.indexedMap (\y _ -> nextState (x,y) world) row) world
nextState : CellPosition -> CellWorld -> CellState
nextState position world =
let
cellsAround = List.map (\f -> f position)
[upper << right, upper, upper << left, right, left, lower << right, lower, lower << left]
countLiving =
(List.map (flip getState world) cellsAround)
|> List.filter ((==) Living)
|> List.length
in
if countLiving == 2 || countLiving == 3 then
Living
else
Dead
ここまで書いたら楽勝ですね。
表示する
あそういえば、CellWorldとかをつくるための関数も用意しておきますね。
createRandomWorld : Int -> Int -> Generator CellWorld
createRandomWorld width height =
Random.list width (Random.map (\x -> if x then Living else Dead) Random.bool)
|> Random.map Array.fromList
|> Random.list height
|> Random.map Array.fromList
initWorld : Int -> Int -> CellWorld
initWorld width height =
Array.repeat width Dead
|> Array.repeat height
さて、さていつものThe Elm Architectureですね。
import Html exposing (..)
import Html.Events exposing (..)
import Random exposing (Generator)
import Array exposing (Array)
import Color exposing (Color)
import Collage exposing (..)
import Element exposing (..)
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
type Msg = UpdateWorld | Create CellWorld
update : Msg -> CellWorld -> (CellWorld, Cmd Msg)
update msg world =
case msg of
UpdateWorld -> (nextGeneration world, Cmd.none)
Create newCellWorld -> (newCellWorld, Cmd.none)
-- INIT
init : (CellWorld, Cmd Msg)
init = (initWorld 100 100, Random.generate Create (createRandomWorld 100 100))
-- VIEW
view : CellWorld -> Html Msg
view cellworld =
div []
[ h1 [] [ draw cellworld ]
, button [ onClick UpdateWorld ] [ Html.text "UpdateWorld" ]
]
draw : CellWorld -> Html Msg
draw world =
Array.toList world
|> List.map Array.toList
|> List.indexedMap
(\indexY row ->
List.indexedMap
(\indexX state ->
move (11.0 * (toFloat indexX), 11.0 * (toFloat indexY)) <| filled (cellColor state) (rect 10.0 10.0 )
) row
)
|> List.map group
|> collage (11 * (width world)) (11 * (height world))
|> toHtml
cellColor : CellState -> Color
cellColor state =
case state of
Living ->
Color.white
Dead ->
Color.gray
-- SUBSCRIPTIONS
subscriptions : CellWorld -> Sub Msg
subscriptions model = Sub.none
おわりに
ざっくり、初学者むけに書いたつもりです。んで、なにが言いたいかといえば、おもったよりスッキリ書けるのがelmのいいところなんじゃないでしょうか。
また、スッキリ書けるのは、静的型のおかげでもあります。その型チェックがなければ、コードがホントにこれでいいか分からないかもしれません。
おわり。