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