Elm 0.19 に入門してみました。
前に参加したElm入門ハンズオンで「Elmを勉強するならライフゲームを作るのが良いらしい」と聞いた気がしたのでライフゲームを作りました。
作ったもの
途中で止めたり、手動で次の世代を表示できたりします。
Dependency
実装
ソースコードはこちらです。
https://github.com/amanoese/elm-life-game
デザインはBootstrap ( Elm Bootstrap )を採用しました。
またライフゲームの描画にはSVG( SVG in Elm )を採用しました。
Canvs(elm-canvas)を利用するのが楽そう?ですが、
調べたところ elm-canvas は何度か仕様が大きく変更されているらしく、
参考にできるコードが存在するのか不安だったため採用を見送りました。
デプロイ(Netlify)
Netlify にデプロイします。
NetlifyでElmが直接サポートされているわけではありませんが、
Node.js と yarn がサポートされているため Elm のコードも Build&Deploy できます。
packege.json の dependency に elm を登録します。
また、Build スクリプトを記載します。
{
・・・省略
"devDependencies": {
"elm": "^0.19.0-bugfix2"
},
"scripts": {
"build": "elm make src/Main.elm --output dest/index.html",
}
}
あとは Netlify の Build Command に、yarn build
を記載すればデプロイできました。
ハマったところ
Elm 0.19 の Browser.element
について情報が少ない
副作用を伴う処理はElm 0.19ではHtml.program
ではなくBrowser.element
を利用する必要があります。
一応解説してくれている方がいるのですが、初心者なのでハマりました。
Elm 0.18 で使われていたHtml.program
は以下のような型です。
program :
{ init : ( model, Cmd msg )
, view : model -> Html msg
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
}
-> Program Never
Elm 0.19でのBrowser.element
は、以下のような型になっています。
element :
{ init : flags -> ( model, Cmd msg ) -- ← ここが異なる
, view : model -> Html msg
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
}
-> Program flags model msg
以前のHtml.program
ではinitが値だったのですが、Browser.element
では関数になっており引数が必須になっています。*
初期化するための関数initを定義しないといけないのですが、
私は、Browser.sandbox
で大枠を実装した後にBrowser.element
に移行しようしたため下記のようなエラーを出してしまいました。
-- TYPE MISMATCH -------------------------------------------------- src\Main.elm
The 1st argument to `element` is not what I expect:
133| Browser.element { init = init , update = update, view = view ,subscriptions = subscriptions }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This argument is a record of type:
{ init : ( Model, Cmd Msg )
, subscriptions : Model -> Sub Msg
, update : Msg -> Model -> ( Model, Cmd Msg )
, view : Model -> Html Msg
}
But `element` needs the 1st argument to be:
{ init : flags -> ( Model, Cmd Msg )
, subscriptions : Model -> Sub Msg
, update : Msg -> Model -> ( Model, Cmd Msg )
, view : Model -> Html Msg
}
いや、わかってますよ……
この型引数 flag
は下記のような初期化したいときのためにjavascript側から値を渡すために用意されてる引数らしいです。
Elm.Main.init(0);
init : Int -> model
init num = ( {num = num} , Cmd.none)
今回のアプリケーションでは初期値は不要なので引数を渡さない方法が知りたかったのですが、
1~2時間悩んだ結果下記のようなコードでいけるとわかりました。
init : () -> model
init _ = ( { num = 0 } , Cmd.none )
入力が存在しない場合は空タプルを渡すとよいのでしょうか?
Random(副作用) 難しい問題
Msg
のパターンマッチを利用が必要になることは、解説無しには理解できなかったと思います。
下記の記事がとても参考になりました。@sandさんに感謝です。
それでも、ネストしたリストにランダムな値を与える綺麗な方法がわからなかったため、
今回は下記のように、一度ランダムな数をすべて生成した後、分割するようにして解決しています。
type Msg
= Init
| RandomList (List Int)
init : () -> (Model, Cmd Msg)
init _ =
( { cells = initCells 0 , numberOfCells = 30 } ,Cmd.none)
update: Msg -> Model-> (Model , Cmd Msg)
update msg model =
let { cells, numberOfCells } = model
in
case msg of
Init ->
(model, Random.generate RandomList (Random.list (numberOfCells ^ 2) (Random.int 0 1))) --フラットなrandamのintのリスト
RandomList randomInts ->
( { model | cells = flattenCells <| split numberOfCells randomInts } -- リストを n * n のネストしたリストに分割
, Cmd.none
)
なんか遅い
最初は List だけで実装していたのですが、パフォーマンスが悪すぎて調べたところ実装がLinked-Listらしいです。
下記の記事が参考になりました。しかし、下記の記事は古いため直和型の部分については無視したほうが良い?と思います。
Listで書いていた部分をDictで書き直すと爆速になりました。@philoponさんに感謝です。
subscriptionsについて
Randomを理解した後だとハマらずに実装できました。
感想
今の知識でスクラッチしたらもっと綺麗に書けるなーと思うほど結構勉強になりました。
「Elmが凄い言語だよ!」って話は3~4年前から聞いていたのですが、
純粋関数型言語でフロントエンドが実装できるって触れ込みの誇大広告ぽさと、
若い言語で廃れていくんだろうな……という理由から勝手に避けていました。
しかし、2019年になった今でも人気のある言語の一つであり、
Elm入門ハンズオンに参加した結果かなり実践的な言語と実感しました。
Elm楽しいですね。