この記事はElm Advent Calendar 2016 の4日目です。
会社で書かせてもらってるElm製アプリが10000行を超えたので、現時点で個人的にこれはと思うベストプラクティスを実際のソース付きで書いてみる。
(アプリについての情報は機会があれば)
1.必ずスタイルガイドに従う
行数が増える傾向にあるが、かなり読みやすくなるので絶対に従った方が良い。
関連コミット (let...in
中に空行を挿入している)
2.データ構造にタプルを使わない
例えばマウスの位置などをタプルで(Int, Int)
のように書きたくなる。しかし後悔するのでやめた方が良い。
-- 微妙 calculateX : Model -> Int calculateX model = let (x, y) = model.position in max 0 x
-- 微妙 calculateX : Model -> Int calculateX model = max 0 (Tuple.first model.position)
タプルから値を取り出すにはlet...in
中で展開するか、Tuple.first
などを使うしかない。単純に汚くなるし、first
は値の意味を表していない。代わりにtype alias Position = { x : Int, y : Int }
を使う。
-- 良い calculateX : Model -> Int calculateX model = max 0 model.position.x
このように、明らかにすっきりする。他にも領域を表すのに(Int, Int, Int, Int)
などを使っていると、(left, top, right, bottom)
なのか(left, top, width, height)
なのか分からなくてとても混乱する。代わりに、先ほどのPosition
やtype alias Size = { width : Int, height : Int }
を使うと分かりやすくなる。
タプルは文字通り、複数の結果を「組」にして返したい用途で一時的に使うのが良い。データ構造を表すのに使うと失敗する。
- 関連コミット (ありとあらゆる場所にあった
(Int, Int)
の大部分をPosition
に置き換えた)
3.型の別名(type alias)を使って可読性を上げる
型注釈がStringだらけになると何を表しているのか分からなって混乱する。
-- 微妙 setPerson : String -> String -> Model -> Model
これを次のようにすると、何をしているのか分かるようになる。
-- 良い type alias ObjectId = String type alias PersonId = String setPerson : ObjectId -> PersonId -> Model -> Model
これに合わせて変数名もただのid
からxxxId
に変更したところ、とても読みやすくなった。
- 関連コミット (
String
型をPersonId
、FloorId
などに置き換えた)
4.import時にexposingするのは型のみにする
次のようにimportすると関数がどのモジュール由来だかすぐに分からなくなる。
-- 悪い import Foo.Bar exposing (..)
exposingするのは型のみにして、関数はモジュール名から書いた方が良い。
-- 良い import Foo.Bar as Bar exposing (Bar)
- 関連コミット (
exposing (..)
をやめてモジュール名から関数を書くようにした)
5.一つのモジュールから沢山の型を公開しない
これも同じで、型がどのモジュール由来だかすぐに分からなくなる。Javaほどガチガチにする必要はないが、基本的にモジュール名と同じ名前の型をひとつだけ公開するのが良い。
-- 微妙 import Foo.Bar exposing (Bar, Baz, Pen, Pineapple, Apple)
- 関連コミット (
ObjectsOperation
モジュールにあったDirection
型をDirection
モジュールに分離した)
6.パイプを使う
Maybe
やList
を使っていると、どんどんcase...of
が増えて読みづらくなる。
-- 微妙 message = case List.head (List.reverse (getPeople model)) of Just person -> person.name ++ "was the first person." Nothing -> "No one found."
そこで、パイプが使える。メソッドチェーンのようなものだ。
-- 良い message = getPeople model |> List.reverse |> List.head |> Maybe.map (\person -> person.name ++ "was the first person.") |> Maybe.withDefault "No one found."
視線もあっちにいったりこっちに行ったりしなくて良くなる。ただし慣れるまでには少し時間がかかる。早めに慣れよう。
- 関連コミット (Maybe型の処理をパイプで書き直した)
7.CmdよりもTaskベースのAPIを提供する
CmdはTaskより一段抽象度が高く、連鎖させることを許可していない。
Taskであれば、次のように色々連鎖させたリクエストを投げることが出来る。
getUser config |> Task.andThen(\user -> getOrganization config user.orgId |> Task.map(\org -> { user = user, org = org })) |> Task.onError(\err -> Task.succeed (Error err)) |> Task.perform GotInfo
8.Subscriptionをケチる
Virtual DOMという富豪的な仕組みを採用している以上、出来ることならMouse.moves
のような頻繁に起こるイベントは避けたい。
subscriptions
の型はModel -> Sub msg
となっているので、モデルの状態に応じて蛇口を緩めるようにすればいい。次の例は公式のドラッグ&ドロップの例より。
subscriptions : Model -> Sub Msg subscriptions model = case model.drag of Nothing -> Sub.none Just _ -> Sub.batch [ Mouse.moves DragAt, Mouse.ups DragEnd ]
- 関連コミット (問答無用で垂れ流していた
Mouse.moves
をドラッグ中に限定した)
9.データ構造を真剣に考える
正しいデータ構造(型)を定義することで、堅牢性を上げることが出来る。単純な例だと「必ず1つ以上の値を持つリスト」をList a
型で表現することもできるが、これだと何かの拍子に空のリストになる可能性がある。そこで次のようなデータを作る。
type alias NonEmptyList a = { head : a , tail : List a }
これによって、必ずhead
(最初の要素)が存在することは保証できる。「何らかの操作をした時にリストの長さが0であってはならない」というテストも不要になる。
もっと具体的・現実的な例は、Richard Feldman氏のトーク「Making Impossible State Impossible(不可能な状態を不可能にする)」で沢山紹介されているので、ぜひ参照してほしい。
10.カプセル化する
堅牢性に貢献する要素として、データ構造の正しさと同じくらい重要なのがカプセル化だ(これも上のトークで触れられている)。
例えば、先ほどの例では要素が空にならないことは保証できたが、新しい要素を間違えてtail
に突っ込んでしまったら全て台無しになってしまう。
そうならないように、Elmではデータへの操作をモジュール内の関数に限定するテクニックがある。
-- 良い type NonEmptyList a = NonEmptyList { head : a , tail : List a } add : a -> NonEmptyList a -> NonEmptyList a add a (NonEmptyList list) = NonEmptyList { head = a , tail = list.head :: list.tail }
新しいバージョンのNonEmptyList
は、モジュール外で直接head
やtail
を触ることを禁止している。代わりに、公開APIとしてadd
関数を提供する。
こうすることで、「新しい要素をheadに追加し、それまでheadにあったものをtailの先頭に移動させる」という一連の操作が安全であることを保証できる。
詳細はこちらの記事にも書いている。
11.コンポーネントにすべきかを真剣に考える
Elmにおいてコンポーネントとは「Model / Update / View を一緒に提供する」モジュールを指す。
よくある失敗パターンは、再利用可能で疎結合なコンポーネントを作ろうとして本来ひとつであるアプリケーションロジックを分業してしまう事だ。「検索欄はこっちが提供する。ボタンが押されたら具体的な検索ロジックはそっちでやってくれ。結果をコールバックしたらこっちでキャッシュしてあげる。キャッシュをクリアしたいときは命令してくれ。結果の表示はこっちでやる。」もっとシンプルに、入力欄・検索ロジック・結果表示のビューに分ければ良くて、コンポーネントである必要がない。
続いてよくある失敗パターンは、画面を縦横に分割していって、そのままコンポーネントに見立ててしまうことだ。例えば、大抵どんなサイトにもヘッダがある。だから「ヘッダ」コンポーネントを作りたくなる。ところが、ヘッダには色んな機能(ログアウト、設定など)があるから、それらのロジックをどんどん吸い込んでしまい、ページによって内容の過不足が出ておかしなことになる。ヘッダに求められるのは「メニューを渡したらそれを横に並べる」機能で、ヘッダ自体がそれらの機能を持っている必要はない。
すべてはコンポーネント「ではない」。不要な複雑さを持ち込む前に「普通のビューじゃ駄目なのか」をまず検討したい。
- 関連コミット (「隣にある」という理由だけでフォームと一体になっていたファイル読み込み用のモジュールを切り出した)