個人的な勉強と、elm開発上の課題からお試しでパッケージを作って公開してみたので、雑に紹介していきます。
elm-contex-html とは?
概要
任意の view 関数で、グローバルな値 (Context) にアクセスできるようにしよう。というのが主なコンセプトです。
React の useContext のように引数のバケツリレーなどをせずに自由に値を取り出せたら良いのになー、と思って作られました。
なので各種APIの命名なども React に寄せてる部分があります。
後から調べたところ似たようなコンセプトのパッケージもいくつか存在するようです。(そかまで深く確認してないので間違ってたらごめんなさい)
使い方
比較
なにはともあれ、これを使う前と後でコードがどのように変化するかを見ていただきたいと思います。
Before
よくあるカウンターです。言語(Lang
)によって表示を出し分けるようになっています。
type alias Model =
{ count : Int, lang : Lang }
type Lang
= Ja
| En
view : Model -> Html Msg
view model =
div []
[ viewIncrementButton model.lang Increment
, div [] [ text <| String.fromInt model.count ]
, viewDecrementButton model.lang Decrement
]
viewIncrementButton: Lang -> Msg -> Html Msg
viewIncrementButton lang incrementMsg =
button [ onClick incrementMsg ] [ viewIncrementButtonText lang ]
-- 実際にここまで細かく view を分けることはほぼないが、説明のために分けている
viewIncrementButtonText: Lang -> Html Msg
viewIncrementButtonText lang =
case lang of
Ja ->
text "1増やす"
En ->
text "Increment"
After
コード量は多少増えてしまっていますが、
各 view 関数の引数から Lang
が消え、必要としていた viewIncrementButtonText
で初めて Lang
を取り出せています。
-- 省略していますが、init/update も書き換えています。
import ContextHtml exposing (ContextHtml, applyContext, button, div, text, useContext)
type alias Model =
{ count : Int }
type alias Context =
{ lang : Lang
}
type alias HtmlWithContext msg =
ContextHtml Context msg
view : (Model, Context) -> Html Msg
view (model, context) =
applyContext context (viewInternal model)
viewInternal: Model -> HtmlWithContext Msg
viewInternal model =
div []
[ viewIncrementButton Increment
, div [] [ text <| String.fromInt model.count ]
, viewDecrementButton Decrement
]
viewIncrementButton: Msg -> HtmlWithContext Msg
viewIncrementButton incrementMsg =
button [ onClick incrementMsg ] [ viewIncrementButtonText ]
viewIncrementButtonText: HtmlWithContext Msg
viewIncrementButtonText =
-- 必要になったタイミングで値を取り出す
useContext .lang <|
\lang ->
case lang of
Ja ->
text "1増やす"
En ->
text "Increment"
何が良くなった?
変わったことは Lang
が引数から消えただけで、そのためにパッケージを追加して、いろいろ書き換えて...、とするのは面倒のようにも感じますが、ある程度実装の規模が大きくなれば恩恵が受けられると思います。
上の実装では Lang
が必要になるまで、間に view 関数が1つくらいしかありませんが、これが4つ5つと増えてくるとだいぶ手間になってきます。Lang
が必要になった場合には、上位の階層の view 関数の引数全てに追加して、逆に不要になった場合には全て除去しなくてはなりません。 言語などは特に必要/不要がころころ変わるのでこれは面倒です。
各種APIについて
ContextHtml
コンテキストを受け取り、Html を返すような型で、以下のように定義されています。
view関数が 標準の Html の代わりに ContextHtml を返すようにすることで、あらゆる箇所で Context にアクセスできるようになっています。
type ContextHtml ctx msg
= ContextHtml (ctx -> Html msg)
実際に使う場合は上の例のようにエイリアスを貼って使うのが無難だと思います。
type alias HtmlWithContext msg =
ContextHtml Context msg
applyContext
ContextHtml
にコンテクストを適用して、Html を得るための関数です。
当然、最上位のview関数以外でも使えるので途中から Context を適用したり、途中から Context を切り替えるためにも使えます。
view (model, context) =
applyContext context (viewInternal model)
useContext
Context から値を取り出すための関数です。
以下のように使うことで値を取り出せます。同時に複数取り出す場合は useContext2
などを用意しているのでそちらを利用します。
view =
useContext .lang <| \lang -> ...
useContext は Context から値を取り出す関数
と 取り出した値を受けとる関数
を引数を取るようにしています。
これは私が例のような実装よりも、以下のような Opaque type を利用した実装を想定しているためです。
type Context =
Context { lang: Maybe Lang }
langSelector: Context -> Lang
langSelector (Context context) =
context.lang |> Maybe.withDefault En
view = useContext langSelector <| \lang -> ...
Context を Opaque type にした上で useContext を使うことには次のようなメリットがあります。
すでに上の例で lang を Lang
から Maybe Lang
に変更しましたが、このような変更が入った場合の対応が容易かつ安全になります。
langSelector のように lang を取得するための関数を切り出して、最初からこれを利用することで、デフォルト値の適用などを共通で行うことができます。
Context は広く利用されることを想定しているため、各でデフォルト値の適用などをすると、実装がブレる可能性が高いです。
そのため、あらかじめ取得ようの関数を切り、それを使い回すことでこのような問題を回避することができます。
その他
-
div, button, text ...
例の段階では特に説明しませんでしたが、全て ContextHtml を返す関数で置き換えられています。
これら以外にも標準の Html にあった関数の代替関数が用意されています。 -
wrapHtml
Html を ContextHtml に変換する関数です。
外部のパッケージや、共通のviewなど Html を返す関数を、ContextHtml を返す関数内で使用するための関数です。
余談
elm歴が浅いことや、その場の勢いで公開したために、v1 公開後すぐに v2 を公開する羽目になりました。
パッケージの公開自体はドキュメントを書いたりと大変でしたが、公開しようとする → 怒られる → 直す を繰り返していけば、基本問題ない形で公開できるのでむしろ安心感がありました。
ただドキュメントようの記述とかファイル作れば良いだけじゃなくて、短くて怒られたりするのはビックリしました。
まだまだ elm は初心者なので、引き続き勉強していきます。
参考にした記事: How to publish an Elm package