Help us understand the problem. What is going on with this article?

一覧のViewを自在に操る elm-reference

最近 Elm-jp 界隈で、Vue.js の Tree View Example のようなものをどうやって実現するかで盛り上がっていました。
この機会に以前からほしいと思っていた機能を提供する elm-reference を作成しました。

この記事では、View 上のイベントで List を更新する際に生じる課題を取り上げ、elm-reference による解決方法をご紹介します。

elm-reference-small.png

一覧の View にまつわる課題

世の中には一覧を表示して操作するようなアプリケーションが多く存在します。
例えば、よくあるTODOアプリもTODOのリストを追加したり削除したり内容を編集したりします。
他にも管理画面などではユーザー一覧ページや記事一覧ページなどはよくあるパターンです。

このようにモデルに List を含み、その List を描画した各要素について何らかの操作を行うような UI の例として、以下に簡素なプログラムを一部抜粋しました。

init : ( Model, Cmd Msg )
init =
    ( { nums = [ 1, 2, 3, 4, 5, 6 ]
      }
    , Cmd.none
    )


type alias Model =
    { nums : List Int
    }


type Msg
    = ClickNumber Int


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ClickNumber idx ->
            ( { model
                | nums =
                    List.Extra.updateAt idx ((+) 1) model.nums
              }
            , Cmd.none
            )


view : Model -> Html Msg
view model =
    div [] <| List.indexedMap renderRow model.nums


renderRow : Int -> Int -> Html Msg
renderRow idx n =
    div
        [ Events.onClick (ClickNumber idx)
        ]
        [ text <| toString n
        ]

このアプリケーションは、画面に描画される複数の数字のうち1つをクリックすると、その数字が +1 されるだけのものです。
注目していただきたいのは Msg の定義で、ClickNumber Int という形式にすることで、クリックされた数字がリストの何番目の要素か (インデックス) を update に知らせています。

こういったインデックスを渡すやり方は一般的なものですが、どうも回りくどい気がします。
今回の例ではそこまで困りませんが、たとえば次の例のように List がネストしたような構造をしていたら「n番目の要素のm番目の要素の...」と指定する必要があり、気が狂いそうです...

type alias Model =
    { tree : List Node
    }


type Node
    = Node Int (List Node)

Mutable なプログラミング言語の「参照」がほしい

実はこの課題は Mutable な操作ができる言語やデータ構造ではそこまで問題になりません。
ここで言っている「参照」とは例えば JS の以下のような機能です。

> arr = [ {val: 1}, {val: 2}, {val: 3} ]
> x = arr[1]
> x.val = 3
> arr
[ { val: 1 }, { val: 3 }, { val: 3 } ]

x という参照を編集すると、大元のデータである arr も更新されています。
もし Elm に参照のような仕組みがあれば、先程のアプリケーションにおいてインデックスの代わりに参照を update に渡すことができます。

type Msg
    = ClickNumber SomeSortOfReference

これが可能になったら Tree のような複雑な例も簡単に解決できそうです!

elm-reference はこのような操作を可能にすることを目的として開発されました。

elm-reference の基本操作

elm-reference には thisroot という概念があります。
this が現在注目している値 (JS の例では x = arr [1]) で、 root がその値に依存する大元の値 (JS の例では arr) を意味します。

Reference this root がメインの型で、以下の関数で thisroot を取り出すことができます。

this : Reference this root -> this
root : Reference this root -> root

Reference this root の値は基本的に fromRecord で作成します。

fromRecord : { this : a, rootWith : a -> root } -> Reference a root

fromRecordrootWith フィールドの値は、root の値がどのように this に依存しているかを示しています。
例えば、 root が Int の List で、 this がその3番目の要素である場合は以下のように Reference を作成できます。

ref : Reference Int (List Int)
ref = fromRecord
    { this = 3
    , rootWith = \x -> [1,2] ++ x :: [4,5]
    }

this ref
--> 3

root ref
--> [ 1, 2, 3, 4, 5 ]

では、いよいよ更新操作を行ってみましょう!

modify : (a -> a) -> Reference a root -> Reference a root

先程定義した refthis の値を変更してみます。

ref2 : Reference Int (List Int)
ref2 = modify (\n -> n + 1) ref

this ref
--> 3
this ref2
--> 4

root ref
--> [ 1, 2, 3, 4, 5 ]
root ref2
--> [ 1, 2, 4, 4, 5 ]

this 自体の値が1増えただけではなく、 root の3番目の要素の値にもしっかり反映されていることがわかります。
これが elm-reference の基本機能です。

elm-reference をつかって書き直してみる

では実際に elm-reference を使うと先程のクリックした数字を +1 するプログラムがどう変わるか見てみましょう。

type Msg
    = ClickNumber (Reference Int (List Int))


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ClickNumber ref ->
            ( { model
                | nums =
                    Reference.root <| Reference.modify ((+) 1) ref
              }
            , Cmd.none
            )


view : Model -> Html Msg
view model =
    div [] <| Reference.List.unwrap renderRow <| Reference.top model.nums


renderRow : Reference Int (List Int) -> Html Msg
renderRow ref =
    div
        [ Events.onClick (ClickNumber ref)
        ]
        [ text <| toString <| Reference.this ref
        ]

まず、ClickNumber とともに update に渡されるものが、インデックスから Reference に変更されました。

Reference Int (List Int)

この型表記から分かるように、クリックされた数字が this、その数字を含んだ全数字のリスト (model.nums) が root の参照を渡しています。

update 関数の内部では、model.nums の値を以下の値で上書きしています。

Reference.root <| Reference.modify ((+) 1) ref

まず this の値(ここではクリックされた数字)を +1 して、Reference.rootroot の値を取得しています。
これで、クリックされた数字の部分だけが +1 された全数字のリストを得ることができます。

view のところではなにやら見慣れない関数が使われています。

div [] <| Reference.List.unwrap renderRow <| Reference.top model.nums

まず Reference.top 関数は thisroot が同じ Reference を作成する関数です。

top : a -> Reference a a

fromRecord を使えば以下のように書けます。

top a = fromRecord { this = a, rootWith = identity }

次に Reference.List.unwrap です。
この関数は少し仰々しい型をしています。

unwrap : (Reference a x -> b) -> Reference (List a) x -> List b

これは List.indexedMap と比べるとわかりやすいでしょう。

unwrap :     (Reference a x -> b) -> Reference (List a) x -> List b
indexedMap : (Int ->    a   -> b) ->            List a    -> List b
  • 第1引数でインデックスと値を渡す代わりに参照を渡す
  • 第2引数としてリストの代わりにリストを this として持つ参照を渡す

という2つの違いがあるだけです。

第2引数の参照を作成するために Reference.topmodel.nums を変換しています。

もっと知りたい場合は...

ここでご紹介した数字を +1 するコードの全体像や、Treeの場合の例などは elm-reference の example ディレクトリで実際に見たり試したりできます。

ぜひ elm-reference をマスターして、一覧のView を自在にあやつってください!

P_20180712_164559_vHDR_Auto.jpg
さくらちゃんにご飯をあげる

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away