16
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ElmAdvent Calendar 2017

Day 5

elmについてlifegameをつくりながらいろいろ初学者向けに説明してみた。

Last updated at Posted at 2017-12-06

この記事をみて、なんとなく書こうと思いました。

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みたいなのがあればなぁ...とか思うんですが、まーしかたないですね。
ちなみに、こういうデータを作るとき、たぶん初学者は引数の順番に悩むと思うんですよね。たとえば、getStatesetState、の順番をかえて

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のいいところなんじゃないでしょうか。

また、スッキリ書けるのは、静的型のおかげでもあります。その型チェックがなければ、コードがホントにこれでいいか分からないかもしれません。

おわり。

16
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?