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