Elmパターンを翻訳しました
Elmで使う標準的な設計パターンをまとめた記事を発見しました。作者の元githubはこれ?
Elm patterns - Elm Patterns (sporto.github.io)
そのため、完全になぞったわけではないですが、自分の理解も含めて簡単に翻訳してみました。
※正しく理解したい人は原文を読んでもらったほうがいいかと思います。省いたり、自分がわかりやすい表現に直したりしていますので。
Type blindness
- 型が同じで、かつ容易に混同しやすいものに対し、ユニークな型を設定することで混同を防ぐ。
Anti-Pattern
priceInDollars: Float
priceInDollars = 2.0
priceInEuros : Float
priceInEuros = 1.0
-- これだと、DollarsとEurosが同じFloatのため、足し算ができてしまう
Pattern
type Dollar = Dollar Float
priceInDollars : Dollar
priceInDollar =
Dollar 2.0
2022年5月9日追記
- このパターンを使用すると、後で値をunwrapする必要があるため、その手間をかけてでも混同を避けたい場合に使用するとよい。
Minimize boolean usage
- Booleanの曖昧さはコードの意図や情報を導きやすく、結果的に曖昧なロジックになりやすい。
例1:
- BooleanをCustome Typeで置き換えることで曖昧さを回避する。
Anti-Pattern
bookFlight "Elm" True
Pattern
type CustomerStatus
= Premium
| Regular
| Economy
- このように記述することで、CustomerStatusをダイレクトに記述することができ、bookFlightの意図を明確にできる。
bookFlight "ELM" Premium
例2:
- booleanをカスタム型に置き換えることで、booleanによる曖昧さを除去し、コンパイラを活用したより安全なコードを実現する。
Pattern
time =
case isValid formData of
Ok validFormData ->
submitForm validFormData
Err errors ->
showErrors errors
- この場合、submitFormはきっちりした型のデータを受け取ることができる。
※ カスタム型の保持データとしてvaildFormDataを受け取るため、formDataがErrでもおかしな値をsubmitFormが受け取ることはない
Named arguments
- 関数の引数の順番があいまいな場合がある。
例:
isBefore : Date -> Date -> Bool
- 例えばこの場合だと、通常は「パイプライン」として使うために、比較対象は第2引数にあると考えるかもしれないが、そうではないかもしれない。
- このようなコードは曖昧で、エラーを引き起こしやすい。
- そのため、下記のように「レコード」を引数として使用することができる。
isBefore : { subject: Date, comparedTo: Date } -> Bool
- このような書き方は「パイプライン」とは相性が良くないが、より正確であり間違えにくい。
Wrap early, unwrap late
- [[type blindness]]を避けるために、ユニークな型でラッピングするかもしれない。
- 例えば外部ソースから値をDecodeする場合などは、できるだけ早い段階で型をラップしておいたほうが良い
- また、アンラップはなるべく遅い方が良い
例:
Anti-Pattern
displayPriceInDollars : Float -> String
displayPriceInDollars price =
"USD$" ++ String.fromFloat price
- この場合、例えばEurosなどの値が外から入ってくることを止める手段がなく、下記の関数に異なる通貨(ドルやユーロといった)のリストを送ってしまう可能性がある。
calculateTotalPrice: List Float -> Float
Pattern
type Dollar = Dollar Float
displayPriceInDollars : Dollar -> String
displayPriceInDollars (Dollar price) =
"USD$" ++ String.fromFloat price
- このように書くことで、なるべくDollar型を使うことを強制できる
Unwrap Maybe and Result early
- もし
Maybe
,Result
,RemoteData
型があるなら、一般的になるべく早くUnwrapすることが望ましい
Anti-Pattern
userCard : Maybe User -> Html Msg
userCard maybeUser =
div []
[ userInfo maybeUser
, userActivity maybeUser
]
- この場合、userInfoとuserActivyは両方ともMaybe User型であり、つまりはUnwrapが何度もViewの中で現れることになる。
Pattern
userCard maybeUser =
case maybeUser of
Nothing ->
div [] [ ... ]
Just user ->
div []
[ userInfo user
, userActivity user
]
- このようにViewの補助関数でUserを取得しておくことで、ほとんどのViewが書きやすく、テストもしやすくなります。
Make impossible states impossible
- Elmは素晴らしく、表現力のある型システムを持っており、「ありえない状態」を退けることができる。
例:
Anti-Pattern
- 通常のパターンだと、データが読み込まれている間の「クルクル」を表示するための真偽値がある。
type alias Model =
{ isLoading: Bool
, data: Maybe Data
}
- ただし、この場合
isLoading = false
かつdata = nothing
という状態をとりうる。これは何を意味するのか?これは、絶対に有り得ない状態なのかもしれない。
Pattern
type RemoteData
= Loading
| Loaded Data
type alias Model =
{ data : RemoteData }
この話題についての参照動画 ※英語
Parse don't validate
- 外部データ(ユーザー入力やリモートデータなど)がある場合、そのデータを使用する前に検証するのが一般的なパターンである。
Anti-pattern
type alias UserInput =
{ name: Maybe String
, age: Maybe Int
}
isValidUser : UserInput -> Bool
- このアプローチの問題点は、実行したあとでも依然として検証していない値を保持していることである。
Pattern
- 良い解決方法としては、インプットをParseして、検証済みの型としてreturnすることである。
例:
type alias UserInput =
{ name: Maybe String
, age: Maybe Int
}
type alias ValidUser =
{ name: String
, age: Int
}
validateUser : UserInput -> Result String ValidUser
- この方法によって、後々まで検証済みの値として保持される。
The builder pattern
- 多くの引数を渡さなければならないとき、関数は下記のようになる。
module Button exposing (..)
type alias Args =
{ isEnabled : Bool
, label : String
, hexColor : String
, ...
}
btn: Args -> Html msg
- 呼び出し側は下記のようになる。
import Button
Button.btn { isEnabled = True, label = "Click me", ....}
- この問題は、引数をArgsに追加した場合、関数が呼び出されている箇所を一個ずつすべて変更しなければならないことである。
Pattern
- builderパターンを使えば、最小限必要な情報の引数だけを用い、必要な場合に引数を最適化すればよい。
module Button exposing (..)
newArgs: String -> Args
newArgs label =
{ isEnabled = True
, label = label
, hexColor: "#ABC"
, ...
}
withIsEnabled : Bool -> Args -> Args
withIsEnabled isEnabled args =
{ args | isEnabled = isEnabled }
btn: Args -> Html msg
-
このモジュールは初期引数を作成する関数と、引数を変更する一連の関数を作成する。一般に、withを接頭辞として用いる。
-
呼び出す側は下記のようになる
import Button
aButton =
Button.newArgs "Click me"
|> Button.withIsEnabled False
|> Button.withHexColor "#123"
|> Button.btn
- この方法の良いところは、新しい引数が加わっても呼び出し元すべてを変更する必要がないことである。
テスト工場として
- このパターンはテストに対して非常に有効である。例えばUserをテストするときなどは、基本的なUserからスタートして、そのあとに最適化した別のパターンを追加すればよい。
Arguments list
- Builder patternは構成を構築するもの。
- これは、下記のような純粋なListにも使用可能。
view =
shape
[ scale 0.5 0.5 0.5
, position 0 -6 -13
, rotation -90 0 0
]
- このモジュールは一般的な型を返す一連の関数として定義される。例として
scale : Float -> Float -> Float -> Attribute
rotation : Int -> Int -> Int -> Attribute
-
これは、List Attributeとして定義することで、関数を構成に使うことができる。
-
このパターンは、elm/html, elm/svg, elm-css, elm-uiの中で使用されている。
-
このパターンは不透明な型と一緒に使うのが最適。通常、呼び出し元が返された型(例えば属性)にアクセスできるようにしたいとは思わないからである。
-
このパターンは、すべての引数がオプションである場合に最適です。なぜなら、すべての引数がオプションの場合は、呼び出し側が空のリストを渡すことを避けることができないからです。
2022年5月10日追記 ※原文にはありません
例:
div [] []
のような場合で、id
やclass
といったオプションが必ず必要となった場合には、div: id -> class -> ...
のようになってしまう。これだと非常に使用しにくいため、それぞれのオプションをAttribute型として返すことで、List Attributeとしてひとまとめにできる。これによって、空のリストが呼び出し側から渡されても問題ない。
*ここの意味がちょっとわからない。
* Twitterでご指摘いただき、原文とは変わりますがちょっと修正しました。
Type iterator
- すべてがカスタム型による変数になっているリストだと、新しい変数を加えたときにしばしばリストに加えるのをわすれてしまう。
例:
module Color exposing (Color(..), all)
type Color
= Red
| Yellow
| Green
| Blue -- Recently added
all : List Color
all =
[ Red, Yellow, Green ]
-- Forgot to add Blue here
-
コンパイラがリマインドしてくれるのはとてもありがたい。非常に多くの変数がある場合は特にそう感じる。
-
コンパイラはcase文においても、変数が追加されていない場合はリマインドしてくれる。
module Color exposing (Color(..), all)
type Color
= Red
| Yellow
| Green
| Blue -- Recently added
all : List Color
all =
next [] |> List.reverse
next : List Color -> List Color
next list =
case List.head list of
Nothing ->
Red :: list |> next
Just Red ->
Yellow :: list |> next
Just Yellow ->
Green :: list |> next
Just Green ->
list -- return the list as is on the final variant
-- Blue is still missing, but now, the compiler will complain
-- until we add it here
重要: 上記のnext関数の中で、wildecard matchingである_
を使ってはならない。こうすると、コンパイラは新しい変数が加わったことを見逃してしまう。
以上、ざっとなぞってみました。関数型言語で、きちんとこのようにパターンがまとまっているのは見たことがなかったので、とても参考になりました。今読んでいる良いコード/悪いコードで学ぶ設計入門 で紹介されている書き方とも通じるところがあるのと、それを関数言語としてどうやって実装するか、ということもわかってとてもよかったです。
今回は、元の記事の前半部だけ訳したので、後半も今後訳していきたいですね。