はじめに
本記事はElm2(完全版) Advent Calendar 2018の8日目の記事です。
去年、Elmの初心者向けの記事を書かせていただいたのですが、実際にモノが作れていなかったのが心残りだったので、今回は成果物をちゃんと作って記事にしてみました。
"初心者が形あるものをどうやって作っていったか"
それなりに参考になる箇所もあるかと思いますので、どうぞご覧くださいませ。
注意!!
今回使用したElmのバージョンは0.19です
また、本記事の内容はベストプラクティスではありません
筆者が学習も兼ねてトイアプリを作成した際のまとめですのでご了承ください
Elmについて(知ってる人は飛ばしてください)
Elm
はWebフロントエンドの開発に適した、関数型プログラミング言語です。
いままで、そんな言語耳にしたことがないよーっていう人は
私の去年の記事を見るのも良いですが、やっぱり初めはこれ見るのが一番良いよって🐐さんが言ってました。
Elm Guide
https://guide.elm-lang.org/
開発者の原本に相当するページです。
公式ドキュメントを読んで、その言語自体の思想を学ぶのが上達の一番の近道🐐
英語なので、日本語で読みたい人は以下を見てください1。
https://guide.elm-lang.jp/
ほかには、少し怖い人が書いた以下の記事も参考になります。
今勢いのある言語 Elm
https://qiita.com/ababup1192/items/c48df0495728f89f756a
手を付け始めてからアプリが完成するまで
今回、以下のトイアプリを作成しましたので、ぜひページを開いて触ってみてください。
elm-map-creator
<成果物>
http://35.221.113.240
<テーマの選定理由>
- 千里の道もマス目から
- 本当は[軍儀] (https://dic.pixiv.net/a/軍儀)が作りたい
<今回実践したこと>
①縦横2次元のマス目を描画するプログラムを作成(本記事で説明)
②それを斜め上から俯瞰するような、クォータービュー(等角投影図)を描画するプログラムを作成
③マス目に置かれた赤、黄、緑、青の4色の駒が、ボタンを押せば2次元とクォータービューを互いに連動して動くロジックを作成
②と③は17日目の記事『Elmでクォータービューを実装する』で説明します2。
本業が忙しかったこともあり、去年の記事投稿後、あまりElmに触れていませんでしたが(言い訳)、頑張ってアプリを作ることを目標に今年の11月頃から手を付け始めました。
Elm0.19をインストールし直してアプリ開発をはじめてから、記事にして恥ずかしくない出来栄えになるまで約1か月ほど掛かっています。
当方、社内プロキシに阻まれてElm REPL
すら開けない社内PC環境ですが、GCEでインスタンスを作成してその中にcreate-elm-appを導入することで社内プロキシを回避しつつ、本番環境での開発およびアップロードに成功してます(詳しくは闇の魔術に対する防衛術 13日目の記事で説明しますすみませんポエムになりました)。
create-elm-appの説明は省きますが、elm-app build
やelm-app install
など利用しやすいコマンドが揃っていて便利です。本番環境へのアップロードの際には、バッチを組んでelm-format
とelm-app build
を同時に実行していました。
アプリの中身について
それではアプリの中身(ソースコード)の説明に移ります。
縦横2次元のマス目を描画するために
線と線が交わればマス目になります。つまり、マス目を描画するためにはまず、線を描けるようにする必要があります3。
線をどうやって空間上に配置するか
線は点の集合です。何もない所に点は打てないので、座標が必要です。
ということでPoint型を作成して座標を定義します4。その後、再帰の考え方を使ってリスト化する関数を作成します5。
-- (x,y)座標と自身の情報を保持するPoint型を作成
type alias Point a =
{ x : Int
, y : Int
, point : Array a
}
-- マス目を作るために座標のリストを作成する
createList : Int -> Int -> List (Point String)
createList i j =
case j of
0 ->
Point 0 0 (Array.fromList [ "nothing" ]) :: []
_ ->
Point i j (Array.fromList [ "nothing" ]) :: createList i (j - 1)
Elmの通常のリストをインデックス参照しながら操作するのは難しかったので、何かいいパッケージが無いかなーと探していたところ、以下の記事を見つけました6。
[Elm]一覧のViewを自在に操る miyamoen/select-list
https://package.elm-lang.org/packages/miyamoen/select-list/latest/
パッケージの内容を知りたい方はリンクを辿ってみてください。
これを使ってリストをSelectList化し、xy座標の格子点を作成します。
{- リストをSelectList化(maxElementは原点を含む要素の最大数)
-}
createSelectList : Int -> SelectList.SelectList (Point String)
createSelectList n =
SelectList.fromLists []
(Point 0 0 (Array.fromList [ "nothing" ]))
-- 所望の順序と逆で出力されてしまうため反転させる
(createList n (maxElement - 1) |> List.reverse)
-- 空要素を削除(うまいこと作ればこれは必要なし)
|> SelectList.attempt SelectList.delete
{- 格子点のx座標のSelectListを得る関数
-}
getPointXList : Int -> SelectList.SelectList Float
getPointXList n =
createSelectList n
|> SelectList.map .x
|> SelectList.map (\m -> m * tileWidth)
|> SelectList.map toFloat
{- 格子点のy座標のSelectListを得る関数
-}
getPointYList : Int -> SelectList.SelectList Float
getPointYList n =
createSelectList n
|> SelectList.map .y
|> SelectList.map (\m -> m * tileHeight)
|> SelectList.map toFloat
ちゃんとSelectListが作れているか、適宜Elm REPL
を開いて確認を行っています。
例えば…
createSelectList 5
は、x=5(0≦y≦7)
のSelectList
getPointXList 5
はx座標の(画面描画時のピクセル単位に対応させた)SelectList
上の画像を見れば分かるように、createSelectList
は値を渡すとx軸方向に可変なSelectListが作られます(イメージとしては"n列目の線を引くためのリスト"が作成される)。格子点を作るための関数が準備できたところで、線の描画のために点列を出力する関数を作成しています。今回のマス目の作成方針は、
-
drawColumnList
で列方向(=y軸方向)、drawRowList
で行方向(=x軸方向)に伸びる線をmaxElement-1
個分作って、それぞれ並べる
ことでマス目を作っています7。maxElement
は原点を含む要素の最大数、tileWidth/tileHeight
はマス目の一片の長さで、それぞれ値を指定しています(maxElementは8、tileWidthとtileHeightは50です。ピクセル単位で描画する必要があるので、tileWidth
とtileHeight
はある程度大きな値の方が良いです)。
{- 列方向に線を描くための関数: 所望の順序と逆で出力される
-}
outputColumnRev : Int -> Float -> List ( Float, Float )
outputColumnRev columnNum orderNum =
-- columnNum : 出力したい要素の数 orderNum : 列の何番目に線を描くか指定
let
head =
getPointYList maxElement |> SelectList.selectHead
in
if columnNum == (head |> SelectList.index) then
[]
else
( orderNum
, getPointYList maxElement
|> SelectList.attempt (SelectList.selectBy (columnNum - 1))
|> SelectList.selected
)
:: outputColumnRev (columnNum - 1) orderNum
{- outputColumRevに対してList.inverseを適用し、出力順序を反転
-}
outputColumn : Int -> Float -> List ( Float, Float )
outputColumn columnNum orderNum =
outputColumnRev columnNum orderNum |> List.reverse
{- outputColumnをSvg化する
-}
drawColumn : Int -> Svg msg
drawColumn i =
polyline
[ SvgAt.fill FillNone
, stroke Color.black
, points <| outputColumn (maxElement + 1) (i * tileWidth |> toFloat)
]
[]
{- 再帰を使ってリスト化し、複数描けるようにする
-}
drawColumnList : Int -> List (Svg msg)
drawColumnList i =
case i of
0 ->
drawColumn 0 :: []
_ ->
drawColumn i :: drawColumnList (i - 1)
outputColumn
の動きをご覧いただくと、SelectList
のパワーが実感できると思います。
下図を見てください。ある特定の列に対して、望む数の点列を並べることができています(実際に有効なのはmaxElement-1
個分です)8。
一方、行方向の描画のためのリストoutputRow
はoutputColumn
を利用しています。
行方向に伸びる線は、「y座標が固定された、x軸方向に可変な点列の集合」と捉えることができ、これは単純にoutputColumn
のタプルを入れ替えることで表現できます。
swap : ( a, b ) -> ( b, a )
swap ( a, b ) =
( b, a )
outputRow : Int -> Float -> List ( Float, Float )
outputRow rowNum orderNum =
outputColumn rowNum orderNum |> List.map swap
drawRow : Int -> Svg msg
drawRow i =
polyline
[ SvgAt.fill FillNone
, stroke Color.black
, points <| outputRow maxElement (i * tileHeight |> toFloat)
]
[]
drawRowList : Int -> List (Svg msg)
drawRowList i =
case i of
0 ->
drawRow 0 :: []
_ ->
drawRow i :: drawRowList (i - 1)
これらを用意したのち、List.foldr (::)
で畳み込みを行いつつdrawColumnList
とdrawRowList
をまとめてSvg化する関数drawBoard
を作成し、見通しを良くしています。
{- List.foldr(::)で畳み込みを行いつつdrawColumnListとdrawRowListをまとめてSvg化する -}
drawBoard : Int -> Svg msg
drawBoard i =
svg [ SvgAt.width (px 300), SvgAt.height (px 300), viewBox 0 0 300 300 ] <|
List.foldr (::) (drawRowList maxElement) (drawColumnList maxElement)
Svg型については触れていませんでしたが、今回、既存パッケージのTypedSvgを使ってSVG (Scalable Vector Graphics)による描画を行っています。polyline
要素を使って、並べた点列に沿って線を引くことができます。
もうマス目は完成していますが、これだけだと物足りないのでその上に駒を置いてみます。4色用意しましたが、駒の描画の記述はほぼ同じなので、赤色の駒を描画する方法だけ例に取って説明します。
-- 2次元のマス目に色付きタイルを置くための関数(赤、黄、緑、青の4色を用意)
drawRectRed : Int -> Int -> Svg msg
drawRectRed i j =
rect
[ x <|
px <|
(getPointXList i |> SelectList.selectWhileLoopBy i |> SelectList.selected)
, y <|
px <|
(getPointYList j |> SelectList.selectWhileLoopBy j |> SelectList.selected)
, SvgAt.width (px tileWidth)
, SvgAt.height (px tileHeight)
, SvgAt.fill (Fill (Color.rgb255 208 16 76))
, stroke Color.black
, strokeWidth (pt 1.0)
]
[]
SelectList.selectWhileLoopBy i |> SelectList.selected
でSelectListのi番目の要素を取り出すことができます。
ここまでやれば後はview
に入れてやるだけで下図のようなきれいな画像が描けます9。
マス目の左上端が原点(0,0)に相当し、駒を指定の位置に配置したい場合、駒の左上の座標を指定するとよいです(今回の例では(0,0)、(1,1)、(2,2)、(3,3)にそれぞれ駒を配置しました)
---- MODEL ----
view : Model -> Html Msg
view model =
svg
[ SvgAt.width (px 300), SvgAt.height (px 300), viewBox 0 0 300 300 ]
[ drawBoard maxElement
, drawRectsRed 0 0
, drawRectsBlue 1 1
, drawRectsGreen 2 2
, drawRectsYellow 3 3
]
今回の成長ポイント
- 導入したパッケージの関数の動きや利用例が分からない -> 手を動かして動作を理解することを試みた(Elm REPLを使って関数に具体的な値を入れてみて動きを確かめた)
- Elm Packagesを参照しながら他人のコードが追えるようになった
- 再帰や畳み込みの使い方を覚えた
- TypedSvgを使ってWebページに描画できるようになった
- create-elm-appの使い方を覚えた
あとがき
毎日少しずつ関数を作っていき、次第に大きなモノになっていくと進捗が目に見える形で進んでいったので、楽しかったし励みになりました。
今回、色々と実験的に試したことが多かったですが、今後はもう少し効率性を求めつつ、
スピード感を意識しながらElmを書いていきたいです10。
ということで17日目の記事もよろしくお願いします(こちらの方が内容濃いと思います)。
参考ページ
おまけ
力を開放すれば無限にマス目を描くこともできますし、長方形のマス目を描くこともできます(ただしviewBox
の調整が必要)
話の続き
Elmでクォータービューを実装する
https://qiita.com/enma/items/4daad227b05761549e9a
注釈
-
上の記事の日本語バージョンです。有志が翻訳してます ↩
-
いわゆる分割商法です ↩
-
何らかの値を追加することができますが、今回は使いませんでした ↩
-
変数を2つ同時に動かすのは今回やめました。
Array
に"nothing"を入れてますが、入れなくても問題はありません。あと、今まで再帰の考えを知っていても、実践したことが無かったです。。いい機会だったので今回、再帰を使いまくってます ↩ -
今回原点を用意したので(インデックスは0から数えられるようにしたので)、線として描画できる要素数は1つ減って、
maxElement-1
となってます ↩ -
maxElement-1
個を超えた要素は0
が出力されます(これはSelectList.selectBy
の特徴) ↩ -
簡単のために、完成版から部分的に内容を一部変更しています。ちなみに、
Html Msg
の型を作ってview
に渡す時にはsvg
要素を一気に渡す必要があります(でないと画像が分離されて上手く描画ができません) ↩