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

nekoroll.hatenablog.com
の続き

今回やること

  • 下の段と重なっていないマスは点灯しないようにする
  • 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でリスタートできる
をメッセージとして出すようにした
f:id:nekorollykk:20200927004245p:plain

まとめ

  • 状態をもたせてパターンマッチして簡単に処理できた
  • パターンマッチネストするケースもう少しきれいに書きたい
  • 調べたけど速度の変更がイマイチうまく行かなかった
    そもそも速度はsubscriptionでいいのか?
    Process.sleepを組み合わせてTaskでどうにかするのか?
    そもそもラグが生まれるのはupdateの問題だからではないのか?
    BlinkingkeyDownのタイミングの問題
    あたりをぐるぐる悩んでた

次回はこのあたりをなんとかクリアしたい

STACKERゲームを作ろう その5

nekoroll.hatenablog.com
の続き

今回やること

  • 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.keyDownonKeyDown : Decoder msg -> Sub msgになっているので
受け取ったキー入力をDecodeしてSpaceとそれ以外で分けるようにした
ググるkeyCodeがたくさん出てきたんだけど、APIkeyを推奨しているので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 )

新たにKeyDownVariantを定義してSpaceOtherでパターンマッチ
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 }

stoppedLightPointsSpaceを押した時点の光っている箇所、
lightPointsに現在動いている行の光っている箇所がはいっているので
マージし一つの配列にして、配列内に含まれるPointを光らせる様にした

これで積み上げることが出来た!
…と思ったら何かラグい。スペース押したら1マス分絶対ずれる

AB先生のエスパー

マジもんの超能力者でビックリした
画面の更新に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回更新されるのでめちゃくちゃ難化してしまった
ズレはなくなった…気がする(多分)

f:id:nekorollykk:20200923232837g:plain
さながらParty Parrot

何らかの方法で60回を半分にするとか出来ると思うので、後々対応していく
まずは積み上げて動かすことが出来たので一旦ゴール

まとめ

  • Docはちゃんと読みましょう
  • onAnimationFrame便利すぎる
    jsだとsetTimeoutとかでやって大変だった記憶がある

次回

  • 積み上げた際に下の段と重なっていないマスを落とす
  • 1つも重なっていなければゲームオーバー
  • 動き速すぎ問題対処

の予定

Ellieのショートカットキーを今更知った話

Elmo

つい最近知ったんですがElmユーザってElmerじゃなくてElmoなんですね
かわいくてステキ

f:id:nekorollykk:20200922200243p:plain
エルモダヨー

みんなだいすきEllie

ellie-app.com
The Elm Live Editor
もちろんみんな使ってるよね

ブラウザさえあればElmが書ける

サブPCでも、スマホでも、Amazon Fire Stickでも書ける!

f:id:nekorollykk:20200922193411j:plain
(書きやすいとは言ってない)

どこでも書ける

外出先でも、お布団でも、トイレでも、お風呂でも書ける!

f:id:nekorollykk:20200922193700j:plain
(壊れないとは言っていない)

こんな便利なエディタに対する悩みが1つだけあった

ショートカットキーがわからない

ショートカットキーどこ?

エディタのどこかにヘルプがあるのか!

f:id:nekorollykk:20200922194907g:plain
Ellie内を必死に探す
ない

リンク先にあるのか!

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

nekoroll.hatenablog.com
の続き

今回やること

  • n個ずつ点灯させる
  • n秒毎に1マス隣に移動する
  • 端に辿り着いたら反転させる
    f:id:nekorollykk:20200922185935g:plain
    反転している様子

完成品はこちら
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.columnSizelightCountから計算
List.rangeyの範囲を生成しmakeStartPointx一番下の段に固定しつつ
初期の光る範囲を生成する

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で更新

これで左右に光っているマスを移動できるようになった

まとめ

  • ::の意味がわかって配列操作がちょっと出来るようになってきた

    www.amazon.co.jp
    コチラの本でバッチリ理解しました、皆さん買いましょう
  • -+が中置演算子である性質を上手く利用できた気がする
    個人的にこれがトップクラスにありがたかった。他の言語でもほしいと本気で思った
  • 実装中に違和感(よくない部分)が何となく感じ取れるようになってきた気がする
    • fieldSizeがべた書きで多数出現
    • 行のhead,tailが毎回使いづらい
    • 実装上ありえないはずの[] ->のパターンマッチ
      この辺りはリファクタで解消予定
  • subscription便利だし使いやすい
    他の処理に移動速度が紛れ込まないのが特にいい
  • 実装が楽しい(n回目)
    当然ロジックの誤りでバグったりするんだけどjsや言語として苦しむことが一切ない
    全て自分のミスで、かつコンパイラママが全て教えてくれる
    Elmを業務で使えたらコードフォーマットとかルールではなく、もっと本質的なロジックとか仕様について着目して指摘しやすいんだろうなぁ…と妄想した

STACKERゲームを作ろう その3

nekoroll.hatenablog.com
の続き

せっかく色々教わったので、まずはリファクタから対応した
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を点滅させる
    f:id:nekorollykk:20200918071324p:plain

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を点滅させるよう実装

できた🆒
f:id:nekorollykk:20200918070446g:plain

まとめ

  • subscriptionでn秒ごとの制御できるの簡単
  • リファクタしたら行数は増えたけどコードの見通しが良くなった
  • ちょっとパイプ慣れてきた、面白い🐊(>>,<<は除く)

次回は指定した範囲のマスを点滅させるを実装します

おまけ

  • n*mのマス目生成は需要があると思いライブラリを探した package.elm-lang.org
    良さげならライブラリがあった
    どうやって実装しているか気になってソース読んでみたけどわからんかった😇
    qiita.com
    こちらの記事でも紹介されているのでメジャーなライブラリなのかな?

STACKERゲームを作ろう その2.9999

nekoroll.hatenablog.com
前回のコードをAB先生がリファクタしてくれました
なので、自分なりに読み解いて技を盗んじゃおうってやつです
f:id:nekorollykk:20200915230201p:plain

リプライとソースセットで読み解いていこう

AB先生が想定していたコード

ellie-app.com

関数

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を渡すとが返ってくるので、シンプルで凄い
Boxdivを生成する処理でも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を取り

  1. getPoint currentBox でリスト内の1Boxの座標を取得
  2. getPoint boxでクリックしたBoxの座標を取得
  3. 一致していたらinvertBox関数に渡し色を変化させる
  4. 不一致なら処理せずそのまま帰す

僕の実装だと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だけ分岐したいとは思っていたけど、出来なかったので悔しい

ついでに学ぶパイプ演算子

f:id:nekorollykk:20200916010928p:plain
Basics - core 1.0.5

今回のケースだと以下の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に変換する処理…の前にstrtrimして空白を削るのか」
🐊「strtrimして空白を削って、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
🐊「strtrimして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)

個人的にはカッコがないほうが読みやすいと思ったけど、好みの領域な気もする
カッコでの書き方は慣れてきたのであえて<|使うようにしてみたいお気持ち💭

<< >>

🐊💤
残念ながらワニの脳みそでは理解できなかったようです
合成はまだしないしまた合う日まで物置にしまっておきます

知っておくべきありがたいご指摘

import List.Extra as Exを使おうとして結局自分で再実装したまま残してました(反省)

import List.Extra as Listが面白くて
core.Listと重複するものがないから正に拡張になる
当然List.groupsOfが使えるしcoreList.allList.mapも使える

たまたまAB先生が教えてくれたからList.Extra as Listを知ったけど
こういうベストプラクティスってどこにあるんだろう?
List.ExtraのドキュメントにもGithubにも無かった(と思う)
こういう細かいベストプラクティスがまとまった何かがあると凄い嬉しい

PointBoxは分けるべきという話

は「対象のモノの役割」をちゃんと考えて適切なコードに落とし込むが出来ていなかった
AB先生が書いている通り、色と座標は別で処理するべきで一つの塊のレコードとして持つべきではなかった
色と座標を分けることでコードがキレイになるし、意味も明確になる
仮に色が多色増えても座標に影響は出ないし、座標にzが増えても色に影響は出ない
と適切に責務が分離したコードを書くことが出来る
これElmに限らず業務のプログラムでもとても大切なことだと思う

レコード同士の比較が出来る


(知らなかった)
box.x === xってやっていたけどBox PointとすることでPoint同士の比較が出来る
つまりこれもレコードの形に依存せず比較が出来る
box.xだとzが増えたときに比較のロジックも修正が必要になるけれど
Pointの比較にしているとPointzを足すだけで比較のロジックは影響なし

なんだこれElmすごすぎないか

AB先生のリファクタ最終形

ellie-app.com

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 }
  • InvertLightBoxを引数に取る、引数の時点で展開可能なので展開(前教わった)
  • BoxPointをレコードとして持っているので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のみになった
BoxColorの責務を分けているおかげで、ここのコードもわかりやすくなっている

まとめ

  • それぞれの役割を理解してちゃんと責務を分けることが大切
    Jsonのデコードではある程度考えたつもりだったけど、処理するコードで全く考えられていなかった
    特にカスタム型や型エリアスの扱いがまだまだ未熟で、練習が必要だなと感じた
  • カスタム型で分けつつ、パターンマッチを使って安全にわかりやすく処理すべき
  • パイプ演算子わかりやすくてオシャレなので使いたい
  • 自分が実装した下手なコードを他者であるAB先生が直せるの凄くない?
    細かい設計なんて一切話していないし、こうしたいもほとんど話していない
    AB先生の力ももちろんあるんだけどElmの堅牢さの真髄を見た気がする
    実際自分でリファクタするときもコンパイラに従うだけで、リファクタを進めることが出来たので、噂通りホントにコンパイラが強い言語だなと思った

めっちゃ長文になってしまった
AB先生のリファクタ凄いのとElm凄いという気持ちでいっぱい
この文章書いているうちに以前作ったTODOリストリファクタしたくてたまらなくなってきた🐊
でも今はSTACKERゲーム完成を優先で、もっと力つけてからリファクタチャンレジしよう

繰り返しになりますがAB先生本当にありがとうございますm( )m