Runner in the High

技術のことをかくこころみ

2019年現時点でのElmベストプラクティス6選

先日業務で1からElmアプリケーションを作りきったのでそのときの学びをメモっておく。

1. Model / Msg / View のような分割をしない

  • Rails などのフレームワークからきた人がやりがち。
  • Elm でファイル分割をするのはモジュール単位でのカプセル化をするときだけでよい。
  • なので基本的に1画面につき1モジュールとして、その中に Model / Msg / View / Update などを書いていく。
  • ここからさらにデータ構造として抽出できるものがあれば後述の Opaque Type として切り出す。

2. コンポーネント指向と混同しない

  • 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 型があるとする

-- 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 型を定義する

-- Bookmarks
-- 上で定義したBookmarkの集合を表現した型

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 の意図が不明確
    • bookmarkscurrentUser が Nothing なケースが定義から分からない
  • ありえない状態がとれる
    • currentPage が SignIn なのに Bookmarks が存在している、など

これを踏まえた上で、上記よりも以下のような Model がより望ましい

type Model
    = SignIn
    | Loading Session
    | Bookmarks User Session Bookmarks
    | NewBookmark User Session Bookmarks

この定義を見ると、SessionBookmarks を共通化したい気持ちがでてくるが、状態の網羅性が壊れるくらいであれば共通化はしないほうがいい。共通化するよりも堅牢さを大事にする。

6. Parcelを使うと最速で環境構築できる

いまのところこれが最速だと思う。

qiita.com