先日業務で1からElmアプリケーションを作りきったのでそのときの学びをメモっておく。
1. Model / Msg / View のような分割をしない
- Rails などのフレームワークからきた人がやりがち。
- Elm でファイル分割をするのはモジュール単位でのカプセル化をするときだけでよい。
- なので基本的に1画面につき1モジュールとして、その中に Model / Msg / View / Update などを書いていく。
- ここからさらにデータ構造として抽出できるものがあれば後述の Opaque Type として切り出す。
- Elm は React や Vue.js のようなコンポーネント指向フレームワークではない。
- 基本的に1画面につき1モジュールとして作る。
- 再利用したい画面のパーツは関数として作ればよい。
3. Opaque Type の活用
- Elmにはクラスはないが、モジュールシステムを使うことで公開する型、関数などを制限したカプセル化ができる。典型的なものは以下のふたつ。
3.1. 値オブジェクト
アプリケーションのドメインの固有な型を作ることで仕様をカプセル化する
module Bookmark.Title exposing (Title, new)
type Title =
Title String
new : String -> Title
new value =
Title value
ただ String をカプセル化しているだけにも見えるが、このように型を定義することで String をアプリケーション固有のドメインとして特化させることができる。
これには以下の利点がある
- バリデーションなどのロジックをこのモジュールに実装することで責務分割がしやすくなる。
- 関数のシグネチャから利用意図が分かりやすくなる
String -> String -> String
よりもTitle -> Description -> Bookmark
のほうが、コードに与えられた意味が理解しやすい。
3.2. コレクションオブジェクト
これが意外と使いドコロが多い。集合を型として表現したカプセル化。
例えば以下のような Bookmark 型があるとする
module Bookmark exposing (Bookmark, isInvalid)
type Bookmark
= Valid Title Description
| Invalid
isValid : Bookmark -> Bool
isValid bookmark =
case bookmark of
Valid _ _ ->
True
Invalid ->
False
その上で Bookmark のコレクションを扱う Bookmarks 型を定義する
module Bookmarks exposing (Bookmarks, new, size)
import Bookmark exposing (Bookmark)
type Bookmarks =
Bookmarks (List Bookmark)
new : List Bookmark -> Bookmarks
new bookmarks =
Bookmarks bookmarks
size : Bookmarks -> Int
size bookmarks =
bookmarks |> List.filter Bookmark.isValid |> List.length
このコレクションオブジェクトのいいところは利用者側がコレクションの仕様を詳しく知らずに使えること
- Bookmarks のコレクションが何で実装されているか (List, Set, etc...) は利用者側は意識しなくてよい
- Bookmark のサイズがどのような基準で計算されるかは利用者側は意識しなくてよい
言い換えれば bookmark のコレクションにまつわるロジックの変更はすべてこのモジュールに閉じたものになる。
4. Record をインターフェースとして使う
Record はデータ構造に別名をつけただけのもの (type alias だし)
0.19からはタプルが2値しかとることができなくなったぶん Record が3つ以上の値をもつタブルの代替になった感がある。
Extensible Record を使った依存の切り離し
Record を部分的に満たすインターフェース (Extensible Record) を使うことによって特定のデータ構造への依存を分離できる
たとえば以下のコードは toSession
が Model に依存している
type alias Model =
{ session : Session
, currentUser : User
, bookmarks : Bookmarks
}
toSession : Model -> Session
toSession { session } =
session
これはよくある例で、Model はデータが多くなればなるほど多くの関数が依存するようになりがちだ
実際には toSession
関数は session
だけが存在するレコードが受け取れればいいので、以下のように Sessionable として定義した Extensible Record に依存させ Model の依存を取り除ける。
type alias Sessionable a =
{ a | session : Session }
toSession : Sessionable a -> Session
toSession { session } =
session
この分離の利点は以下
- テストする際に Model を丸ごとモックする必要がない
- 依存の方向を統一できる (DIP)
特に Elm は循環参照をコンパイラが許さないので、アプリケーションが大規模になればなるほど Extensible Record の利用シーンは増える。
5. データ構造的な DRY よりも状態網羅性を大事にする
定義上の重複を減らそうとするのではなく、状態の網羅性の観点から適切かを考える
type Page
= SignIn
| Bookmarks
| NewBookmark
| Loading
type alias Model =
{ session : Session
, currentUser : Maybe User
, bookmarks : Maybe Bookmarks
, currentPage : Page
}
上記の Model の欠点は以下の2点
- Maybe の意図が不明確
bookmarks
と currentUser
が Nothing なケースが定義から分からない
- ありえない状態がとれる
currentPage
が SignIn なのに Bookmarks
が存在している、など
これを踏まえた上で、上記よりも以下のような Model がより望ましい
type Model
= SignIn
| Loading Session
| Bookmarks User Session Bookmarks
| NewBookmark User Session Bookmarks
この定義を見ると、Session
や Bookmarks
を共通化したい気持ちがでてくるが、状態の網羅性が壊れるくらいであれば共通化はしないほうがいい。共通化するよりも堅牢さを大事にする。
6. Parcelを使うと最速で環境構築できる
いまのところこれが最速だと思う。
qiita.com