関数型よく知らない人のelm入門 番外編 - 失敗した設計たち
elmを触り始めて2週間ほど、かなりスムーズに組めるようになってきました。
規模が1段階あがるごとに「この組み方じゃダメだ!」となり組み直しているので絶対的なスピードは遅いですけど。
約30ファイル、2000行を超えてきたのでちょっと自分なりに把握したコツ、および失敗例をまとめていきます。
※まだ経験浅いので「ベストプラクティス」というには程遠いかと。
※便宜上、オブジェクト指向の用語を多少用います。
失敗例(1) 複数のMsgクラス
当初やってしまったのがコレです。誰もがelmを始めた当初に通るであろう、サブコンポーネント への欲求です。
つまり、独立したMsg-View-Updateをグルーピング化し、そのコンポーネントを簡単に使いまわそう(分離しよう)という発想ですね。
ReactでいうComponentですね。AngularのComponentのほうが近いかも。またネイティブアプリでいうと、AndroidのFragment化になると思います。
これがelmだと難しい。というかめんどくさい。
ちょっとこんな例を考えてみましょう
--Sub module Sub ... type SubMsg = ... type alias SubModel = ... update : SubMsg -> SubModel -> (SubModel, Cmd SubMsg) update msg model = ... view : SubModel -> Html SubMsg view = ...
うまくコンポーネント化できました! ではこれをMainから呼び出してみましょう。
まずは view関数
です
--main module import Sub ... type Msg = ... type alias Model = view : Model -> Html Msg view model = div [] [ (Sub.view model.subModel) --Error! ]
うまく呼び出せました! …これ、ビルドエラー です。
というのも、 Sub.view
は Html SubMsg
を返すんですよね。 でもメインでは Html Msg
を返す必要があります。
つまり、 型が一致していません。 一応、 Html.map
という変換可能は関数はあります。
type Msg = ... | SubMsg Sub.Msg view : Model -> Html Msg view model = div [] [ (Html.map SubMsg (Sub.view model.subModel)) ]
こんな感じでMsg型に変換してあげればいけます。でも面倒なんですよね…
これが1階層ならいいですが、複数階層にわたるとかなりめんどくさいです。
次に メインの update関数
をみていきましょう。
update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of .... SubMsg submsg -> Sub.update submsg model.subModel --Error!
できました! …こちらも ビルドエラー です。
Sub.update
の戻り値は (Sub.Model, Cmd Sub.Msg)
なので、ModelもCmdも一致してません。
やはりこちらも変換が必要です。
update msg model = case msg of .... SubMsg submsg -> let next = Sub.update submsg model.subModel subModel2 = next.first subCmd = next.second in ({model | subMOdel = subModel2}, (Cmd.map SubMsg subCmd))
とはいうもののこちらのほうは別にSubComponentでなくとも似たような構造だったり。
Modelは階層構造化するので、こんな感じのコードになってしまいました。
もいっこ問題が、「Sub側でメインのMsgを受け取って処理したい」時。
これがそこそこあります。逆もしかり。「Sub側のMsgをメイン側でも受け取りたい」ときです。
後者は何とかなります。メインのupdate関数に書けばいいので。
問題は前者。 メインのMsgはそもそも引数の型が違うので受け取れません。
これをどうにかしたい=>Subの受け取りをメインのMsgにする=>subComponentになってない!
というのを何度も経験しました。
というわけで今の自分は、「Msgは全体で一個」にしています。
どのモジュールの update もルートの Msgクラス を受け取り、どのモジュールのview関数も Html Msg を返します。
このほうが取り回しが楽なんですよね。いちいち変換処理が不要なので、どこにでもSub-Viewを配置できます。
ただしMsgの肥大化が課題。よっぽど独立可能なものは今後切り分けてなんとかするかも。
公式サイト で、「SubComponentなんてない、関数呼べ」といってるのを体感した瞬間でした。
たしかにそのほうが合理的です。
失敗例(2) Viewツリーに合わせModelツリーを作る
これもやっちゃった例です。実際にやっちゃった例として、「ヘッダにある検索条件の状態を、Model.headerに置いてしまった」 ことですね。
たしかに検索条件をはヘッダにあります。でも 検索条件自体はヘッダになくていい んですよね。検索状態はヘッダの状態ではない、といいましょうか。
極端な例として、「検索条件入力を左ペインに移動させたら」このModel-Tree構造は破たんします。なんでheaderに検索条件があるんだよ、となりますよね?
さらに「検索TextBoxはヘッダだけど、詳細検索のCheckboxは左ペイン」となるともっと自体は複雑です。
model.header に searchText をおいて、model.leftPane に detailCheckList を置く?それもおかしな話です。
なので、これは「検索条件」というViewとは全く関係ないModel-Treeを置くべきなのです。 model.searchCondition
におくのが正解。
こうすれば、仮にViewが変わっても - 左ペインに移動しても - Model構造は変更する必要がありません。
これこそModelのあるべき姿 です。
ついサブコンポーネント的な思考だとHeader-Viewの下に検索条件を置きたくなりますが、ModelはViewとは別、Modelをどう表現するかがViewのお仕事 と分離して考えましょう。
もちろん場合によっては Model-Tree と View-Tree をあわせたほうが可読性があがりますので、うまく使い分けですね。
失敗例(3) すべて CSS in elm で行う
これもちょっとミスってしまった例。スタイルの記述をすべてelmでがんばった結果、逆にめんどくさくなってしまいました。
特に 疑似クラスは指定不可能。 hover処理をonMouseEnterとonMouseLeaveで処理するぐらいなら、 &:hover のほうが楽です。
どうしても CSS in elm(js)のアプローチではしんどい部分は積極的にcssファイルを操作するようにしました。
ちなみに当初は慣れの都合で CSSプリプロセッサ を導入してましたが、量が少なく逆にビルド時間がめんどくさくなったので 今は生CSS です。
命名規則も量が少ないので短いもので十分。
逆にいうなら、それ以上頑張りたい場合は elm側で共用スタイルを定義しています。
こんな感じ
* { box-sizing : border-box; } a:hover { color: #f00; } .hover_70:hover { opacity : 0.7; }
クラス名は自動で定数ファイルに書き出すとより良いですね(まだそこまではやってない)。なんか作ろうかな。
失敗例(4) 4スペースインデント
これは流儀によるので失敗例とはいいきれないかもしれません。
どうしても当初は2スペースが狭すぎて違和感を感じ4スペースでした。
でも標準のサンプル等に従うと、インデントは2スペースのほうがキレイ に収まるように感じました。
特に配列をカンマが、「次の行の最初にくる」というelm標準スタイルをやろうが独特なんですよね。
div [ style ... , class ... , onClick ... ] [ div [] [] , div [] [] , div [] [] ]
これ、どうしても4スペースでは収まりが悪く感じており当初は末尾にカンマを付けてました。
しかし標準スタイルと違うのはよくない=>先頭カンマにする=>違和感が… となり2スペースに変更。
当初はつらかったですが、なれた今では2スペースのほうが楽だと思っています。
htmlのdocumentツリーの都合、viewのインデントがどうしても深く成ってしまうのも一因です。
でも他言語から戻るとまた違和感…
とこんな感じで、おそらくみんさんが通ってきたバッドプラクティスを順調に歩んでいますw
先人たちの知恵は偉大ですね…
ファイル数に倍なったら、もしくはelm自体の更新でまた組み方が変わると思います。
この失敗例がこれから始める人にとって少しでも参考になれば幸いです。