elmでライフゲーム

elm

リアクティブプログラミングを軸にした関数型言語であり、Javascriptへ変換されるいわゆるAltJS。 破壊的な操作も関数的に扱える仕組みはモナドではない新しいタイプの操作体系で、Elm Architectureと呼ばれる。

そんなelmに入門。個人的に新しい言語を覚えるときはHello worldの代わりにLifegameを書いているので、例によってelmでもライフゲームから始める。

ライフゲーム

お堅い呼び名はセルオートマトン。実態は単細胞シミュレーション。 あるセルの状態とその周囲の状態からルールを元に次の状態を決定していく。 そのルールは、

  • あるセルの状態1であり、周囲に2つの状態1があれば、次のセルの状態は1。
  • あるセルの状態1であり、周囲に3つの状態1があれば、次のセルの状態は1。
  • あるセルの状態0であり、周囲に3つの状態1があれば、次のセルの状態は1。
  • それ以外は、セルの状態は0となる。

コード

https://github.com/kmtoki/lifegame-elm

レコードと呼ばれる、JSのオブジェクトのようなものでまとめて状態管理。 セルの実態はArray (Array Bool)と2次元配列。

module Lifegame exposing (..)

import Array exposing (Array)


type alias Lifegame =
    { size : { y : Int, x : Int }
    , count : Int
    , isContiune : Bool
    , cells : Array (Array Bool)
    }

init : Int -> Int -> Lifegame
init y x =
    { size = { y = y, x = x }
    , count = 0
    , isContiune = False
    , cells = Array.repeat y <| Array.repeat x False
    }

上記のルールを関数化したものがこれ。

rule : Bool -> Int -> Bool
rule b n =
    case ( b, n ) of
        ( True, 2 ) ->
            True

        ( True, 3 ) ->
            True

        ( False, 3 ) ->
            True

        _ ->
            False

周囲の状態を調べるのがenv。 ルールと周囲の状態を元にセルを更新するのがnext。

env : Int -> Int -> Lifegame -> Int
env y x lg =
    let
        arounds =
            [ ( y - 1, x - 1 )
            , ( y - 1, x )
            , ( y - 1, x + 1 )
            , ( y, x - 1 )
            , ( y, x + 1 )
            , ( y + 1, x - 1 )
            , ( y + 1, x )
            , ( y + 1, x + 1 )
            ]

        f ( y, x ) =
            case Array.get y lg.cells of
                Nothing ->
                    0

                Just xs ->
                    case Array.get x xs of
                        Nothing ->
                            0

                        Just b ->
                            if b then
                                1
                            else
                                0
    in
        List.sum <| List.map f arounds


next : Lifegame -> Lifegame
next lg =
    let
        cells =
            Array.indexedMap
                (\y xs ->
                    Array.indexedMap
                        (\x b -> rule b <| env y x lg)
                        xs
                )
                lg.cells
    in
        { lg
            | count = lg.count + 1
            , cells = cells
        }

ライフゲームの実態は以上のコードで済む。 そのライフゲームをブラウザ上でアニメーションすることがelmでは簡単にできる。 Model Update Viewを基本とし、Subscriptionsでタイマーなどの副作用のある操作などをすることができる。

Modelを各関数で引き回し、それを元にViewがHtmlを生成し、ブラウザ上のイベントをMsgという形でupdateへ送信し、各操作へ振り分けていく。

面白いのが副作用の扱い方。副作用をSub/Cmdで包み関数の返り値として返すとElm Architectureが裏側でよしなにしてくれる。OCamlHaskellとも違うながらも純粋関数的な程を保っている。

今回のコードではランダムとタイマーしか使っていないが、この仕組みでWebsocketなども扱える豊かな表現力ある。

module Main exposing (..)

import Html as H
import Html.Attributes as HA
import Html.Events as HE
import Svg as S
import Svg.Attributes as SA
import Array exposing (Array)
import Time
import List
import String
import Random
import Lifegame


main =
    H.program
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }


type alias Model =
    Lifegame.Lifegame

type Msg
    = New
    | SetY String
    | SetX String
    | Start
    | Next
    | Stop
    | Random
    | RandomSet (Array (Array Bool))


init : ( Model, Cmd Msg )
init =
    ( Lifegame.init 0 0, Cmd.none )


subscriptions : Model -> Sub Msg
subscriptions model =
    if model.isContiune then
        Time.every (Time.millisecond * 100) <| always Next
    else
        Sub.map (always Stop) Sub.none


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        New ->
            ( Lifegame.init model.size.y model.size.x, Cmd.none )

        SetY s ->
            case String.toInt s of
                Ok n ->
                    ( { model | size = { x = model.size.x, y = n } }, Cmd.none )

                Err _ ->
                    ( model, Cmd.none )

        SetX s ->
            case String.toInt s of
                Ok n ->
                    ( { model | size = { y = model.size.y, x = n } }, Cmd.none )

                Err _ ->
                    ( model, Cmd.none )

        Start ->
            ( { model | isContiune = True }, Cmd.none )

        Next ->
            ( Lifegame.next model, Cmd.none )

        Stop ->
            ( { model | isContiune = False }, Cmd.none )

        RandomSet cells ->
            ( { model | cells = cells }, Cmd.none )

        Random ->
            let
                gen =
                    Random.map Array.fromList <|
                        Random.list model.size.y <|
                            Random.map Array.fromList <|
                                Random.list model.size.x Random.bool
            in
                ( model, Random.generate RandomSet gen )


view : Model -> H.Html Msg
view model =
    H.div []
        [ H.div []
            [ H.input [ HA.type_ "number", HA.placeholder "y", HE.onInput SetY ] []
            , H.input [ HA.type_ "number", HA.placeholder "x", HE.onInput SetX ] []
            , H.button [ HE.onClick New ] [ H.text "New" ]
            , H.button [ HE.onClick Start ] [ H.text "Start" ]
            , H.button [ HE.onClick Stop ] [ H.text "Stop" ]
            , H.button [ HE.onClick Random ] [ H.text "Random" ]
            ]
        , H.div [] [ H.text (toString model.count) ]
        , H.div [] [ viewLifegame model ]
        ]


viewLifegame : Model -> H.Html Msg
viewLifegame model =
    S.svg
        [ SA.width (toString <| model.size.x * 50)
        , SA.height (toString <| model.size.y * 50)
        ]
        (List.concat <|
            Array.toList <|
                Array.indexedMap
                    (\y xs ->
                        Array.toList <|
                            Array.indexedMap
                                (\x b ->
                                    S.rect
                                        [ SA.x (toString <| x * 5)
                                        , SA.y (toString <| y * 5)
                                        , SA.width "5"
                                        , SA.height "5"
                                        , SA.fill
                                            (if b then
                                                "green"
                                             else
                                                "black"
                                            )
                                        , SA.stroke "black"
                                        ]
                                        []
                                )
                                xs
                    )
                    model.cells
        )

出来上がり

f:id:kumatoki:20170731172704g:plain

所感

元々はFRPベースだったらしく、小難しい概念で副作用を取り扱っていようだがElm Architectureとやらのおかげでずいぶん簡単に入門できた。パターンマッチばかりで関数合成が少なくなってしまったが語彙の問題なので覚えればすぐ簡潔にかけるようになるだろう。elm-formatにはちょっと趣味と合わない部分があるが、まあ人様に見やすく編集しやすいコード書式という意味でelm-formatで統一的に合わせていくのは良いと思う。
HTMLイベントの発火の分、Msgを追加していかないといけないのは面倒なのでどうにかまとめたいが方法がまだよくわからない。あとTaskという概念もあるらしく、非同期通信などを使う場合は必要になってくるらしい。Haskellで関数型の思考には慣れているので書き味は程よく気持ちのいいものであったが、Haskellと比べ、型安全なライブラリ設計なので冗長的な部分も多々ある。型クラスの導入も議論されているらしいがHaskellの標準ライブラリの型クラス化などを見ている限り、なかなか型クラスも面倒なところがありそうなので、無いなら無いで良いのかもしれない。データ操作が明示的なのも、書くときは面倒だが、読むときに楽できるので良い部分もある。
フロントエンドの知識はまるでないので、これを機会に少し覚えていけたらと思うだけで特に勉強しない結末。