Elm開発のベースとなるリポジトリを用意した
STACKERゲームが難航したり他のことで忙しかったりしてブログ更新してなかった
github.comを作った
ずっとEllieで開発していたんだけど、いい加減テストも書きたいし
ビルドもできるようになりたい、ということでベースとなるリポジトリを作った
内容
- parcelでビルド、devサーバが動く(yarn build / yarn dev)
- elm-test実行可能(yarn test)
- yarnで作った(yarn触ってみたかった)
GitHub Pagesに公開する予定もあるので、それに合わせてビルド設定とかも変えていく予定
感想
elm-test速いしサクサク書けて楽しい
テスト書きつつ進めたら良かったかなーとちょっと後悔しているけど勉強なのでヨシ!
テストケースの考え方とかは他言語と変わらないんだけど、in-outが言語で保証されているおかげで
めちゃくちゃテストが書きやすい。PHPでひたすらモックしている部分がなくて
純粋にテストのためのテストコードを書いている感じ。楽しい
ちなみにこのリポジトリはAB先生のelm-parcelを参考に作りました 神に感謝 github.com
STACKERゲームを作ろう その6
今回やること
- 下の段と重なっていないマスは点灯しないようにする
- 1つも重なっていなければゲームオーバー
完成品はこちら
ellie-app.com
解説
下の段と重なっていないマスは点灯しないようにする
止めたPoint
からy-1
して、stoppedLightPoints
にあるかどうか見たらOK
update
KeyDown keyType -> case keyType of Space -> let isStacked { x, y } = if List.isEmpty model.stoppedLightPoints then True else List.any (\p -> p == { x = x + 1, y = y }) model.stoppedLightPoints stackedPoints = List.filter isStacked model.lightPoints in ( { model | stoppedLightPoints = model.stoppedLightPoints ++ stackedPoints , lightPoints = setStartPoints nextRow 3 } , Cmd.none )
isStacked
で重なっているかどうか判定
最初の行は重なりがないため、停止済みのPoint
があるかどうかを判定するようにした
stackedPoints
で重なっているPoint
のみ保存しstoppedLightPoints
に保存するようにした
1つも重なっていなければゲームオーバー
- ゲームの状態を持つようにする
- 1つも重なっていなければゲームオーバー
- 重なっているマスがあれば継続
- ついでに
Space
でリスタートできるようにする
Model
type GameState = Playing | GameOver
ゲーム進行中とゲームオーバーを状態として持つようにした
これを各パターンマッチに追加していく
→コンパイラが全部教えてくれるので凄い楽だった
subscriptions
subscriptions : Model -> Sub Msg subscriptions model = case model.gameState of GameOver -> Browser.Events.onKeyDown (D.map KeyDown keyDecoder) Playing -> Sub.batch [ Browser.Events.onAnimationFrame Blinking , Browser.Events.onKeyDown (D.map KeyDown keyDecoder) ]
ゲーム進行中ならボックスの点滅とキーイベントの監視
ゲームオーバー状態ならキーイベント(リスタート用)のsubscription
を定義
update
case model.gameState of GameOver -> case msg of KeyDown keyType -> case keyType of Space -> initialModel () Other -> ( model , Cmd.none ) Blinking _ -> ( model , Cmd.none ) Playing ->
GameState
のパターンマッチを追加
GameOver
状態でSpace
を押すと初期状態に戻りリスタート出来るようにした
パターンマッチがネストするのちょっとどうにか出来ないかなーとここで思った
view
view : Model -> Html Msg view model = let rowBaseSplitedBox = splitBox 7 model.boxList in div [] [ div [] (List.map showBoxRow rowBaseSplitedBox) , div [] [ text <| showGameMessage model.gameState ] ] showGameMessage : GameState -> String showGameMessage gameState = case gameState of Playing -> "PRESS 'SPACE' TO STOP BOX" GameOver -> "GAME OVER! PRESS 'SPACE' TO TRY AGAIN"
ゲーム進行中はスペースで止められること
ゲームオーバー時はSPACE
でリスタートできる
をメッセージとして出すようにした
まとめ
- 状態をもたせてパターンマッチして簡単に処理できた
- パターンマッチネストするケースもう少しきれいに書きたい
- 調べたけど速度の変更がイマイチうまく行かなかった
そもそも速度はsubscription
でいいのか?
Process.sleep
を組み合わせてTask
でどうにかするのか?
そもそもラグが生まれるのはupdate
の問題だからではないのか?
→Blinking
とkeyDown
のタイミングの問題
あたりをぐるぐる悩んでた
次回はこのあたりをなんとかクリアしたい
STACKERゲームを作ろう その5
今回やること
- Spaceキーで止められるようにする
- 止めたら上の段に移動する
完成品はコチラ ellie-app.com
実装解説
Space
キーの入力を待ち受けるようにする
subscription
subscriptions : Model -> Sub Msg subscriptions model = Sub.batch [ Time.every 500 Blinking , Browser.Events.onKeyDown (D.map KeyDown keyDecoder) ]
Sub.Batch
で複数のsubscription
を定義した
キー入力を判別する
type KeyType = Space | Other keyDecoder : Decoder KeyType keyDecoder = D.map toKey (D.field "key" D.string) toKey : String -> KeyType toKey key = case key of " " -> Space _ -> Other
browser/keyboard.md at master · elm/browser · GitHub
Browser.Events - browser 1.0.2
を参考にした
Browser.keyDown
はonKeyDown : Decoder msg -> Sub msg
になっているので
受け取ったキー入力をDecode
してSpace
とそれ以外で分けるようにした
ググるとkeyCode
がたくさん出てきたんだけど、APIはkey
を推奨しているのでkey
にしてみた
update
type Msg = Blinking Time.Posix | KeyDown KeyType type KeyType = Space | Other KeyDown keyType -> case keyType of Space -> let currentPoint = case model.lightPoints of [] -> { x = 0, y = 0 } headPoint :: rest -> headPoint nextRow = { rowSize = currentPoint.x - 1, columnSize = 6 } in ( { model | stoppedLightPoints = model.stoppedLightPoints ++ model.lightPoints , lightPoints = setStartPoints nextRow 3 } , Cmd.none ) Other -> ( model, Cmd.none )
新たにKeyDown
Variantを定義してSpace
とOther
でパターンマッチ
Space
の時は現在の行(厳密にはPoint
)を取得し、光る箇所を一段上の行の右端にするようにした
また、積み重ねていく必要があるためSpace
を押した時点の光っている箇所を保存するstoppedLightPoints
も定義した
光らせる部分
type alias Model = { boxList : List Box , fieldSize : FieldSize , lightPoints : List Point , moveMode : MoveMode , stoppedLightPoints : List Point } update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of Blinking _ -> let concatLightPoints = model.lightPoints ++ model.stoppedLightPoints blinking box = if List.member box.point concatLightPoints then { box | lightMode = LightOn } else { box | lightMode = LightOff }
stoppedLightPoints
にSpace
を押した時点の光っている箇所、
lightPoints
に現在動いている行の光っている箇所がはいっているので
マージし一つの配列にして、配列内に含まれるPoint
を光らせる様にした
これで積み上げることが出来た!
…と思ったら何かラグい。スペース押したら1マス分絶対ずれる
AB先生のエスパー
1Tick分のラグが間違いなくある。描画とModelの更新にズレが有るのかなーなんだろ
— 🐊すがわに🐊 (@nek0roll) 2020年9月23日
内容わからないですがエスパーしますhttps://t.co/Gl9nWs4KTkhttps://t.co/Gl9nWs4KTk
— ABAB↑↓BA (@ababupdownba) 2020年9月23日
いわゆるFPS的なやつです
マジもんの超能力者でビックリした
画面の更新にTIme.every
を使っているけどNGだった
正しくはBrowser.Events.onAnimationFrame
だった
Time - time 1.0.0
ちなみにTime.every
のDocに「アニメーションには使わないほうがいいよ」って書いてあった(大反省)
しかも親切にBrowser.Events.onAnimationFrame
のリンクまである
コンパイラ外でも助けてくれるElmママの優しさに涙が止まりません
onAnimationFrame
を使う
subscription
subscriptions : Model -> Sub Msg subscriptions model = Sub.batch [ Browser.Events.onAnimationFrame Blinking , Browser.Events.onKeyDown (D.map KeyDown keyDecoder) ]
1秒で出来た
ただ問題があって、1秒に60回更新されるのでめちゃくちゃ難化してしまった
ズレはなくなった…気がする(多分)
何らかの方法で60回を半分にするとか出来ると思うので、後々対応していく
まずは積み上げて動かすことが出来たので一旦ゴール
まとめ
- Docはちゃんと読みましょう
onAnimationFrame
便利すぎる
js
だとsetTimeout
とかでやって大変だった記憶がある
次回
- 積み上げた際に下の段と重なっていないマスを落とす
- 1つも重なっていなければゲームオーバー
- 動き速すぎ問題対処
の予定
Ellieのショートカットキーを今更知った話
Elmo
つい最近知ったんですがElm
ユーザってElmer
じゃなくてElmo
なんですね
かわいくてステキ
みんなだいすきEllie
ellie-app.com
The Elm Live Editor
もちろんみんな使ってるよね
ブラウザさえあればElm
が書ける
サブPCでも、スマホでも、Amazon Fire Stickでも書ける!
どこでも書ける
外出先でも、お布団でも、トイレでも、お風呂でも書ける!
こんな便利なエディタに対する悩みが1つだけあった
ショートカットキーがわからない
ショートカットキーどこ?
エディタのどこかにヘルプがあるのか!
リンク先にあるのか!
elmlang.slack.com
github.com
github.com
ない
ヘルプどこ…?
ググった
github.com
意訳
提案 そんな重要じゃないんだけど、コンパイルのショートカットとかあると嬉しいなー 回答 実はココやで、トントン(URLを指差す) 画面に出すべきだけどUI的どうするか検討中や
github.com
確かにあった
ショートカットキーまとめ
機能 | ショートカット |
---|---|
Recompile | Ctrl+Shift+Enter |
Package | Ctrl+Shift+p |
Debug | Ctrl+Shift+d |
Output | Ctrl+Shift+o |
Settings | Ctrl+, |
Reload Output | Ctrl+Shift+r |
Logs | Ctrl+Shift+l |
Save | Ctrl+s |
(大体ブラウザやOSと干渉して動かない気がする)
これでEllie
の使い勝手がまた1つアップした
ありがとうEllie
、ありがとうLuke Westby氏、全てのElmo
に感謝
STACKERゲームを作ろう その4
今回やること
- n個ずつ点灯させる
- n秒毎に1マス隣に移動する
- 端に辿り着いたら反転させる
反転している様子
完成品はこちら
ellie-app.com
(色々気になる部分があったのでリファクタ予定)
実装解説
いらないものを消した
update
type Msg = InvertLight Box InvertLight { point, lightMode } -> let invert box = if point == box.point then { box | lightMode = invertLightMode box.lightMode } else box in ( { model | boxList = List.map invert model.boxList } , Cmd.none )
invertLightMode : LightMode -> LightMode invertLightMode lightMode = case lightMode of LightOn -> LightOff LightOff -> LightOn
onClick (InvertLight box)
クリックイベントによる操作は不要になったので消した
点滅は全てsubscription
駆動になる
n個ずつ点灯させる
Model
type alias Model = { boxList : List Box , fieldSize : FieldSize , lightPoints : List Point }
光らせる範囲の処理のためにサイズが必要になったのでfieldSize
を追加
光っている座標を保持してy
を加算/減算して移動させたかったので
lightPoints
で光っている座標リストを持つようにした
Point
ではなくList Point
なのはn個光らせるため
Init
initialModel : () -> ( Model, Cmd Msg ) initialModel _ = let fieldSize = { rowSize = 10, columnSize = 7 } pointList = makePointMatrix fieldSize in ( { boxList = List.map makeBox pointList , fieldSize = fieldSize , lightPoints = setStartPoints fieldSize 3 , moveMode = Left } , Cmd.none )
initでfieldSize
を保持するようにして、散らばっていたサイズ定義をまとめた
スタートの個数をsetStartPoints
関数に渡し、最初の光るボックスを生成(後述)
setStartPoints
setStartPoints : FieldSize -> Int -> List Point setStartPoints fieldSize lightCount = let makeStartPoint y = { x = fieldSize.rowSize - 1, y = y } headPoint = fieldSize.columnSize - lightCount tailPoint = fieldSize.columnSize - 1 in List.range headPoint tailPoint |> List.map makeStartPoint
fieldSize
と初期で光らせる個数を引数に受ける
光る範囲の先頭と末尾をfieldSize.columnSize
とlightCount
から計算
List.range
でy
の範囲を生成しmakeStartPoint
でx
一番下の段に固定しつつ
初期の光る範囲を生成する
Update
update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of Blinking _ -> let blinking box = if List.member box.point model.lightPoints then { box | lightMode = LightOn } else { box | lightMode = LightOff } in ( { model | boxList = List.map blinking model.boxList } , Cmd.none )
予め定義しておいた初期で光る範囲lightPoints
に含まれているか
で光らせるかどうかを判定しlightMode
を切り替える
n秒毎に1マス隣に移動し、端に辿り着いたら向きが反転する
Model
type MoveMode = Left | Right type alias Model = { boxList : List Box , fieldSize : FieldSize , lightPoints : List Point , moveMode : MoveMode }
y-1
していって端にたどり着きy
がマイナスになったらy
の最大値に戻る
と最初考えていたけどSTACKERゲームは端にたどり着くと反転するので
進行方向をカスタム型として定義
Init
, moveMode = Left
開始進行方向は左
進行方向の判定
getMoveMode : MoveMode -> List Point -> MoveMode getMoveMode moveMode lightPoints = let headPoint = case lightPoints of [] -> { x = 0, y = 0 } head :: rest -> head length = List.length lightPoints tailPoint = case List.drop (length - 1) lightPoints of [] -> { x = 0, y = 0 } tail :: rest -> tail in case moveMode of Left -> if tailPoint.y == 1 then invertMoveMode moveMode else moveMode Right -> if headPoint.y == 5 then invertMoveMode moveMode else moveMode
現在の進行方向と光っている範囲を引数に受ける
光っている範囲の先頭と末尾を取得
進行方向が左の場合、光っている範囲の末尾が端っこなら進行方向を反転
進行方向が右の場合、光っている範囲の先頭が端っこなら進行方向を反転
という実装にした(左が先頭で右が末尾になる)
head,tail
取得時の[] ->
のパターンマッチが不要だなーと実装中思っていた
この辺りでリファクタを考え始めた
向きの反転
invertMoveMode : MoveMode -> MoveMode invertMoveMode moveMode = case moveMode of Left -> Right Right -> Left
シンプルイズベスト
AB先生が教えてくれたinvertLight
のパクリ
光っている範囲の移動
moveLightPoints : List Point -> MoveMode -> List Point moveLightPoints lightPoints moveMode = let movePoint { x, y } = { x = x, y = getOperator moveMode y 1 } in lightPoints |> List.map movePoint getOperator : MoveMode -> number -> number -> number getOperator moveMove = case moveMove of Left -> (-) Right -> (+)
光っている範囲と進行方向を引数に受ける
進行方向が左ならy
を減算、進行方向が右ならy
を加算する必要があるので
左なら(-)
、右なら(+)
という形で+-1
する処理を共通化した
中置演算子+ -
が関数として扱える性質を上手く利用できた気がする
update
update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of Blinking _ -> let blinking box = if List.member box.point model.lightPoints then { box | lightMode = LightOn } else { box | lightMode = LightOff } moveMode = getMoveMode model.moveMode model.lightPoints in ( { model | boxList = List.map blinking model.boxList , lightPoints = moveLightPoints model.lightPoints model.moveMode , moveMode = moveMode } , Cmd.none )
光っている位置に応じて進行方向を更新するためにgetMoveMode
を呼び出し更新
光っている箇所は先程定義したmoveLightPoints
で更新
これで左右に光っているマスを移動できるようになった
まとめ
::
の意味がわかって配列操作がちょっと出来るようになってきた
Listのパターンマッチに使われる中置演算子::やっとわかった
— 🐊すがわに🐊 (@nek0roll) 2020年9月16日
case [1,2,3] of
x::xs ->
の場合xが1でxsが[2,3]なのか
www.amazon.co.jp
コチラの本でバッチリ理解しました、皆さん買いましょう-+
が中置演算子である性質を上手く利用できた気がする
個人的にこれがトップクラスにありがたかった。他の言語でもほしいと本気で思った- 実装中に違和感(よくない部分)が何となく感じ取れるようになってきた気がする
fieldSize
がべた書きで多数出現- 行の
head,tail
が毎回使いづらい - 実装上ありえないはずの
[] ->
のパターンマッチ
この辺りはリファクタで解消予定
subscription
便利だし使いやすい
他の処理に移動速度が紛れ込まないのが特にいい- 実装が楽しい(n回目)
当然ロジックの誤りでバグったりするんだけどjs
や言語として苦しむことが一切ない
全て自分のミスで、かつコンパイラママが全て教えてくれる
Elm
を業務で使えたらコードフォーマットとかルールではなく、もっと本質的なロジックとか仕様について着目して指摘しやすいんだろうなぁ…と妄想した
STACKERゲームを作ろう その3
せっかく色々教わったので、まずはリファクタから対応した
AB先生のコードを真似しつつ、自分なりに良いと思う方法も混ぜてリファクタしてみた
リファクタ
ellie-app.com
大体はAB先生の真似なので、自分で書いた部分だけ解説
Model
ライトの点灯状態
type LightMode = LightOn | LightOff
Color
で持ってもいいかな?と考えたけどSTACKERゲームとしては点灯/消灯しかないので
LightMode
で管理することにした
m*nのマス目の生成
type alias InitSize = { rowSize : Int , columnSize : Int } makePointMatrix : InitSize -> List Point makePointMatrix { rowSize, columnSize } = let rowNumList = List.range 0 (rowSize - 1) columnNumList = List.range 0 (columnSize - 1) in rowNumList |> List.map (\x -> columnNumList |> List.map (\y -> { x = x, y = y }) ) |> List.concat
let~in
記法で行列の番号リストを変数に入れておくようにした
Point
生成は結局List.map
をネストさせる方法しか思いつかなかったので、パイプを使って可能な限り括弧を減らして可読性を上げてみた
|>
で左から右に読めるようになっているので、少しは読みやすい…はず
Model
の初期化
initialModel : Model initialModel = let pointList = makePointMatrix { rowSize = 7, columnSize = 10 } in { boxList = List.map makeBox pointList }
let~in
記法でPoint
の一覧を変数に入れておくようにした
後は大体AB先生のリファクタ後ソースと同じです
今回やること
- n秒ごとに
Box
を点滅させる
guide.elm-lang.org
を参考にしました。完成品はこちら
ellie-app.com
実装解説
Browser.element
に変更
Subscriptions
を扱いたいのでelement
に変更する
update
update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of InvertLight { point, lightMode } -> let invert box = if point == box.point then { box | lightMode = invertLightMode box.lightMode } else box in ({ model | boxList = List.map invert model.boxList } , Cmd.none )
返り値型を(Model, Cmd Msg)
に変更しCmd.none
を返すようにした
initialModel
initialModel : () -> (Model, Cmd Msg) initialModel _ = let pointList = makePointMatrix { rowSize = 7, columnSize = 10 } in ({ boxList = List.map makeBox pointList } , Cmd.none )
引数にflags
を追加(使わないので_
)し、返り値型を(Model, Cmd Msg)
に変更しCmd.none
を返すようにした
subscriptions
の実装
import Time subscriptions : Model -> Sub Msg subscriptions model = Time.every 2000 Tick
2秒毎に更新するsubscriptions
を実装
Time
ライブラリが必要だったのでパッケージ追加してインポートした
subscription
から呼ばれるupdate
Tick
のパターンを定義
type Msg = InvertLight Box | Blinking Time.Posix update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of Blinking _ -> (model, Cmd.none)
とりあえず呼び出せるだけ。受け取ったtime
は使わないので_
にした
マスを点滅させる処理を実装
Blinking _ -> let blinking box = if { x = 0, y = 0 } == box.point then { box | lightMode = invertLightMode box.lightMode } else box in ( { model | boxList = List.map blinking model.boxList } , Cmd.none )
まずは仮実装なのでx=0, y=0
を点滅させるよう実装
できた🆒
まとめ
subscription
でn秒ごとの制御できるの簡単- リファクタしたら行数は増えたけどコードの見通しが良くなった
- ちょっとパイプ慣れてきた、面白い🐊(
>>
,<<
は除く)
次回は指定した範囲のマスを点滅させるを実装します
おまけ
- n*mのマス目生成は需要があると思いライブラリを探した
package.elm-lang.org
良さげならライブラリがあった
どうやって実装しているか気になってソース読んでみたけどわからんかった😇
qiita.com
こちらの記事でも紹介されているのでメジャーなライブラリなのかな?
STACKERゲームを作ろう その2.9999
nekoroll.hatenablog.com
前回のコードをAB先生がリファクタしてくれました
なので、自分なりに読み解いて技を盗んじゃおうってやつです
リプライとソースセットで読み解いていこう
AB先生が想定していたコード
僕が想定していたのはこんな感じですね↓
— ABAB↑↓BA (@ababupdownba) 2020年9月14日
実際にはBoxモジュールが出来て、そこにメソッドが増えていく感じです。このやり方の良いところは、Boxの色の種類が増えたとしても Box自体の性質とBoxを処理しているところで分割統治できるところですね https://t.co/Ib3pBPyN9V
関数
invertBox
関数
invertBox : Box -> Box invertBox box = case box of DarkBox p -> LightBox p LightBox p -> PinkBox p PinkBox p -> DarkBox p
Box
を渡すと次の色のBox
を返すinvertBox
関数
僕のケースだとDark
<-> Light
だったけど多色対応している
Point
(座標)はそのまま引き継いでBox
の種類だけ変化する
→これは何となくイメージできていた(と思う)
getColor
関数
getColor : Box -> String getColor box = case box of LightBox _ -> "red" DarkBox _ -> "white" PinkBox _ -> "pink"
Box
の種類に応じて色を取得するgetColor
関数
これが完全に思いつかなかった
僕のケースだとBox
の種類に応じてdiv
を返すようになっていてコードの重複が気になっていた
この関数だとBox
を渡すと色
が返ってくるので、シンプルで凄い
Box
のdiv
を生成する処理でもbackground-color
にこの結果を渡すだけでOKなので
コードの重複が無くなる🆒
getPoint
関数
getPoint : Box -> Point getPoint box = case box of LightBox p -> p DarkBox p -> p PinkBox p -> p
Box
から座標を取得するgetPoint
関数
レコードじゃなくてカスタム型になっている強みが出ていると思う
元々想定していなかった多色対応があっさりできていて凄い
レコード型だったらisLightOn
の形を変える必要があるけれど、カスタム型なら不要
新たな色の型を追加するだけで対応できる🆒
どの関数も「Box
を渡して処理する」という形で実装されている
List.map
関数
model.boxList
として処理する事が多いと思うけど
どれも関数
に渡しやすい関数になっている
この辺が関数型プログラミングのキモなのかな🐊
各関数の使い方
update
update : Msg -> Model -> Model update msg model = case msg of InvertLight box -> let invertLight currentBox = if getPoint currentBox == getPoint box then invertBox box else currentBox in { model | boxList = List.map invertLight model.boxList }
無理やりパターンマッチで処理した僕のコードと違ってシンプル
invertLight
関数は引数にcurrentBox
を取り
getPoint currentBox
でリスト内の1Boxの座標を取得getPoint box
でクリックしたBox
の座標を取得- 一致していたら
invertBox
関数に渡し色を変化させる - 不一致なら処理せずそのまま帰す
僕の実装だとif b.x == x && b.y == y then
としていたけれど
この書き方のほうが「Box
の座標を取得して比較している」という意図がわかりやすい
また、Point
のみに依存しているので仮にx, y, z
になっても
getPoint
の修正のみでOKなので、他処理に影響が出ない。すごい
showBox
showBox : Box -> Html Msg showBox box = div [ class "box" , style "background-color" <| getColor box , onClick (InvertLight box) ] []
僕のコードだとBox
でパターンマッチして色違いのdiv
を返す重複の多いコードだったけど
この書き方だと本来分岐させたいbackground-color
のみに影響が留まっている
僕もbackground-color
だけ分岐したいとは思っていたけど、出来なかったので悔しい
ついでに学ぶパイプ演算子
今回のケースだと以下の3つは等価
style "background-color" <| getColor box
style "background-color" (box |> getColor)
style "background-color" (box getColor)
ずっと「何だこれは…?f x
でいいのでは…?🤔」と思っていたけれど
今回のケースでようやく読めるようになって、利点も分かった
せっかくなので理解を深めるために個人的な解説をしていく
f x
style "background-color" (box getColor)
何の変哲もない関数適用である
散々書いているだけあってわかりやすい
ただ、関数に関数を渡して…ってやっていくとちょっと直感的じゃ無くなる
例) String.toInt (String.trim str)
日本語も英語も左から右に読むようになっているので、一瞬だけ混乱する
🐊「Int
に変換する処理…の前にstr
をtrim
して空白を削るのか」
🐊「str
をtrim
して空白を削って、Int
に変換する処理か」
左から右に読むと順番が前後してしまう
このぐらい短いと行けるけど、もっと長かったり複雑だと苦しいかも知れない
(そもそもそれは関数に切り出すべきというツッコミは置いといて…)
|>
https://package.elm-lang.org/packages/elm/core/latest/Basics#(|%3E)
style "background-color" (box |> getColor)
今回のケースだと全くマッチしていなくて無駄である
読んでそのまま「パイプでbox
を渡してgetColor
関数を適用する」
なんかで見たことあるなーと思ったらUnix
のパイプだった(ps aux | grep php
とかね)
生きるのは先程の例String.toInt (String.trim str)
のように適用を組み合わせるケース
例を|>
に置き換えると以下のコードになる
str |> String.trim |> String.toInt
🐊「str
をtrim
してInt
に変換する処理か」
何ということでしょう、匠の手により前後が無くなり🐊がスムーズに読めています
直感的に読めて、書ける。なんて素晴らしい演算子…
基本的にこれ使っていこう、って思ったらこんな注意書きがあった
https://guide.elm-lang.jp/appendix/function_types.html#パイプライン
パイプラインは多くの人が好むように、"左から右へ"読むことを可能にするため、コードをすっきりさせることができます。 しかしパイプラインは過度に使われる可能性があります。 3、4つのパイプラインとなった場合トップレベルにヘルパー関数を書いた方がコードがより簡潔になる場合が多くあります。 トップレベルに定義することで変換自体に関数名が付き、その引数にも名前が付けられ、型注釈も書くことになります。 つまりソースコード自体がその意味するところを雄弁に語るようになり、チームメイトや将来の自分自身は感謝することでしょう! 更にロジックをテストすることもより簡単になります。
便利だけど使いすぎは厳禁。やはり基本は関数化
細かく関数化した処理をview
とかで適用するときに使うとか
関数内で他関数を適用するときに使うぐらいが適切かな
<|
style "background-color" <| getColor box
https://package.elm-lang.org/packages/elm/core/latest/Basics#(%3C|)
今回のコードで一番適切な演算子だと思う
style "background-color" "" <- ここに
getColor box`を入れる
という感じですごい直感的に読める
基本的には|>
の方が使われると思うけど、今回のケースのような
カッコがないほうが直感的に読めるケースで使うとよさそう
style "background-color" <| getColor box
style "background-color" (box getColor)
個人的にはカッコがないほうが読みやすいと思ったけど、好みの領域な気もする
カッコでの書き方は慣れてきたのであえて<|
使うようにしてみたいお気持ち💭
<<
>>
🐊💤
残念ながらワニの脳みそでは理解できなかったようです
合成はまだしないしまた合う日まで物置にしまっておきます
知っておくべきありがたいご指摘
あと使ってなかったから消したのですが、
— ABAB↑↓BA (@ababupdownba) 2020年9月14日
import List.Extra as Ex
の部分。これだとDict.Extra などを並行して使おうとした場合に、Exで被ってしまいます
で、Extraシリーズの多分推奨している使われ方ですが
import List.Extra as List
と書けちゃいます。つまりListモジュールをExtra出来ます
import List.Extra as Ex
を使おうとして結局自分で再実装したまま残してました(反省)
import List.Extra as List
が面白くて
core.List
と重複するものがないから正に拡張になる
当然List.groupsOf
が使えるしcore
のList.all
もList.map
も使える
たまたまAB先生が教えてくれたからList.Extra as List
を知ったけど
こういうベストプラクティスってどこにあるんだろう?
List.Extra
のドキュメントにもGithub
にも無かった(と思う)
こういう細かいベストプラクティスがまとまった何かがあると凄い嬉しい
Point
とBox
は分けるべきという話
どうやってDarkとLightを反転させるか…ばかり考えていたせいで、色の処理を分割する発想が全くありませんでした…
— 🐊すがわに🐊 (@nek0roll) 2020年9月15日
同じ処理でもここまでキレイになるんですね…
カスタム型を使わずにレコードで書いてしまい、Elmの良さを活かしきれていないので意識して実装してみます。
本当にありがとうございます
僕が始めPointで分けた方が良いのかな?って違和感は、色と座標は一緒に考える必要はなく 座標の一致などが単に比較だけで出来るので分離した方が良かったっていう結論ですね
— ABAB↑↓BA (@ababupdownba) 2020年9月15日
色は別にboolで扱っても良いんですが、今後拡張するならカスタム型にしてもありかな?ってだけなので蛇足と言えば蛇足です
は「対象のモノの役割」をちゃんと考えて適切なコードに落とし込むが出来ていなかった
AB先生が書いている通り、色と座標は別で処理するべきで一つの塊のレコードとして持つべきではなかった
色と座標を分けることでコードがキレイになるし、意味も明確になる
仮に色が多色増えても座標に影響は出ないし、座標にz
が増えても色に影響は出ない
と適切に責務が分離したコードを書くことが出来る
これElm
に限らず業務のプログラムでもとても大切なことだと思う
レコード同士の比較が出来る
なるほど!色と座標はセットで1レコードとしていましたが、確かに責務?役割?としては別なので、分離するのが正しいですね!
— 🐊すがわに🐊 (@nek0roll) 2020年9月15日
現状点灯/消灯のみなのでBoolでもOKですが、今後色を変える可能性がゼロではないと考えると、拡張性を意識してカスタム型とするのがベストですね…勉強になります。
はいそうすると レコードの比較で座標比較ができます(元コードがx,y取り出して比較していたので) もしくはレコードの比較知らなかったら認知しておくと良さそうです!
— ABAB↑↓BA (@ababupdownba) 2020年9月15日
(知らなかった)
box.x === x
ってやっていたけどBox Point
とすることでPoint
同士の比較が出来る
つまりこれもレコードの形に依存せず比較が出来る
box.x
だとz
が増えたときに比較のロジックも修正が必要になるけれど
Point
の比較にしているとPoint
にz
を足すだけで比較のロジックは影響なし
なんだこれElm
すごすぎないか
AB先生のリファクタ最終形
ちなみに今回のコード Box自体をカスタムタイプでやる方法はあまり良くありません・・・が、色の部分をカスタムタイプでやるのは悪くないと思います。(なので、こうしたらどうなるかなー?みたいな問いかけで言ってました)
— ABAB↑↓BA (@ababupdownba) 2020年9月14日
僕だったら最終的にこうリファクタします↓https://t.co/65Okvpvg44
Model
type Color = Dark | Light type alias Box = { point : Point , color : Color }
色付きBox
+座標だったものが更に分離されて
Box
+座標+色になった
Box
はただのマス目の1つなので、確かに色とBox
がセットは変だった?かもしれない
関数
makeBox
makeBox : Int -> List Box makeBox y = List.map (\x -> let point = { x = x, y = y } in { point = point, color = Dark } ) (List.range 0 6)
let in
を使ってまず座標を変数に格納しておいて
その後座標とデフォルトの色Dark
を指定してBox
を生成
Box
は型エイリアスでレコードコンストラクタが生まれるから
生成するときにBox point Dark
と書くことも出来ると思うんだけど
明示的に{ point = point, color = Dark }
と書いたほうがいいのかな?
turnColor
turnColor : Color -> Color turnColor color = case color of Light -> Dark Dark -> Light
Color
を受け取って色を反転させる
Box
要素が消滅して色だけの責務になった。キレイ
toBackgroundColor
toBackgroundColor : Color -> String toBackgroundColor color = case color of Dark -> "white" Light -> "red"
view
で使用する色の定義を返す
こちらもBox
要素が消えて色だけの責務になった。わかりやすい
update
type Msg = InvertLight Box update : Msg -> Model -> Model update msg model = case msg of InvertLight { point, color } -> let invertLight currentBox = if currentBox.point == point then { currentBox | color = turnColor color } else currentBox in { model | boxList = List.map invertLight model.boxList }
InvertLight
はBox
を引数に取る、引数の時点で展開可能なので展開(前教わった)Box
がPoint
をレコードとして持っているのでgetPoint
関数が消滅して
直接座標の比較が可能になった。凄い直感的- クリックした
Box
と座標が一致してるものはturnColor
で色を反転させる
Box
を返すのではなく色を変える。というコードになった
AB先生の大型リファクタで一番感動したのがupdate
関数
キレイに責務を分けているのでupdate
関数内の色更新処理も凄いキレイ
僕のレコードでデータを持って更新する処理と似ているんだけど、完全に似て非なるものでこっちの方が可読性も保守性も圧倒的に高い
クドいようだけど本当に感動した
showBox
showBox : Box -> Html Msg showBox box = div [ class "box" , style "background-color" <| toBackgroundColor box.color , onClick (InvertLight box) ] []
色の指定がBox
ではなくColor
のみになった
Box
とColor
の責務を分けているおかげで、ここのコードもわかりやすくなっている
まとめ
- それぞれの役割を理解してちゃんと責務を分けることが大切
Json
のデコードではある程度考えたつもりだったけど、処理するコードで全く考えられていなかった
特にカスタム型や型エリアスの扱いがまだまだ未熟で、練習が必要だなと感じた - カスタム型で分けつつ、パターンマッチを使って安全にわかりやすく処理すべき
- パイプ演算子わかりやすくてオシャレなので使いたい
- 自分が実装した下手なコードを他者であるAB先生が直せるの凄くない?
細かい設計なんて一切話していないし、こうしたいもほとんど話していない
AB先生の力ももちろんあるんだけどElm
の堅牢さの真髄を見た気がする
実際自分でリファクタするときもコンパイラに従うだけで、リファクタを進めることが出来たので、噂通りホントにコンパイラが強い言語だなと思った
めっちゃ長文になってしまった
AB先生のリファクタ凄いのとElm
凄いという気持ちでいっぱい
この文章書いているうちに以前作ったTODO
リストリファクタしたくてたまらなくなってきた🐊
でも今はSTACKERゲーム完成を優先で、もっと力つけてからリファクタチャンレジしよう
繰り返しになりますがAB先生本当にありがとうございますm( )m