12
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

UniposAdvent Calendar 2021

Day 13

elm-context-html の紹介

Last updated at Posted at 2021-12-12

個人的な勉強と、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 を利用した実装を想定しているためです。

Context.elm
type Context =
    Context { lang: Maybe Lang } 

langSelector: Context -> Lang
langSelector (Context context) =
    context.lang |> Maybe.withDefault En
Main.elm
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

12
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?