【Elm】どんなページも十把一絡げ！　～ elm-pageでお手軽SPA開発～

初めまして！趣味でElmでWebサイトを作っている予備校講師[^科目]のkudzuないしはkudzunyanと申します[^nyan]。
記事は初投稿となりますがけっこう気合を入れて書いたのでフィードバックをいただけると嬉しいです。
[^科目]:数学とか物理とかを教えています。作品例：[指数・大数計算練習](https://kudzu.lsv.jp/math/exponential_and_logarithmic_function/index.html)
[^nyan]:どちらを使うかは「にゃん」が許される空気かによります。

# はじめに
- この記事は拙作パッケージ[kudzu-forest/elm-page][package]の紹介のために書かれたものです。
- 対象読者：　ElmでSPAを
  - やっている人
  - やろうとして挫折した人
  - やってみたいと心の片隅で思っている人
- 対象<strong>外</strong>読者：
  - SPAの`Model`や`Msg`の型定義を書くのが楽しくて仕方がない人
    - 引き続き楽しいcoding生活を～:slight_smile:
    - 万一書き飽きてきたら再訪してください。
  - `TEA`って文字列でお茶しか思い浮かばない人
    - まず[公式ガイド](https://guide.elm-lang.jp/):flag_jp:を読んでみてください。 

## TL;DR[^tldr]
- SPAのページ管理がちょっぴり楽になるパッケージを作りました。
  - `Browser.element`や`Browser.sandbox`を置き換えるだけでページ側のモジュールが定義できます。
  - どんな`Model`や`Msg`が定義されたページでも、利用側では同じ型のデータとして扱えます。
  - Elmで作成済みのマルチページのWebアプリがあればほぼコピペだけでSPA化できます。

[^tldr]:TL;DRというのは「too long; did'nt read＝長すぎて読めんかったわこのタワケが！」の略だそうですね。すでに青筋が立ち始めていてかつSPAのコードに慣れ親しんでいる方は、[拙作パッケージを用いた書き方](#kudzu-forestelm-pageの紹介)まで飛んでいただいても大丈夫です。

# Introduction：今までのSPAの書き方はめんどくさい！
というわけで特に工夫をしない場合のSPAの書き方をおさらいしておきましょう。

本当にめんどっちいのでurl周りは省略して`Browser.element`で書ける範囲だけみてゆきます[^url操作について]。
`Browser.application`を用いた本格的なSPAを作る例は[当該パッケージのGitHubリポジトリ][github]をご覧ください。

[^url操作について]:要は「複数ページの`Model`や`Msg`のハンドリングの部分だけ解説する」ということです。ページの切り替えはボタン操作にしちゃいます。


## 工夫しないページモジュールの準備（ここは別にめんどくさくない）
まずは各ページのコンテンツを用意しましょう。
ここではコマンドを用いる例を見たいので、皆さんお馴染みのAn Introduction to Elmからランダムにサイコロの目を表示するアプリを借りてくることにします。

```elm:src/Pages/RandomDice.elm
module Pages.RandomDice exposing (Model, Msg, init, subscriptions, update, view)
-- Press a button to generate a random number between 1 and 6.
--
-- Read how it works:
--   https://guide.elm-lang.org/effects/random.html
--

import Browser
import Html exposing (..)
import Html.Events exposing (..)
import Random



-- MAIN


main =
  Browser.element
    { init = init
    , update = update
    , subscriptions = subscriptions
    , view = view
    }



-- MODEL


type alias Model =
  { dieFace : Int
  }


init : () -> (Model, Cmd Msg)
init _ =
  ( Model 1
  , Cmd.none
  )



-- UPDATE


type Msg
  = Roll
  | NewFace Int


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Roll ->
      ( model
      , Random.generate NewFace (Random.int 1 6)
      )

    NewFace newFace ->
      ( Model newFace
      , Cmd.none
      )



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
  Sub.none



-- VIEW


view : Model -> Html Msg
view model =
  div []
    [ h1 [] [ text (String.fromInt model.dieFace) ]
    , button [ onClick Roll ] [ text "Roll" ]
    ]
```

ここで重要なのはこのモジュールが`Model`,`Msg`,`init`,`subscriptions`,`update`,`view`を公開していることであり、実を言うと、一行目以外は公式ガイドのコードから全く変更していません[^main関数について]。`Ctrl-c`からの`Ctrl-v`です。
[^main関数について]:各ページごとの`main`関数は本来不要となりますが、別に書いてあっても邪魔にはならないので放っておいてOKです。そうすればデバッグ時は問題のあるページのみをコンパイルして動作確認をすることができ、より親切なエラーメッセージを享受しながらリファクターできます。

サイコロだけだと`subscriptions`が空で寂しいのでデジタル時計のコードも借りてきましょうか。

<details><summary>Pages/DigitalClock.elmのコード（上とやることは一緒）</summary>

<div>

```elm:src/Pages/DigitalClock.elm
module Pages.DigitalClock exposing (Model, Msg, init, subscriptions, update, view)
-- Show the current time in your time zone.
--
-- Read how it works:
--   https://guide.elm-lang.org/effects/time.html
--
-- For an analog clock, check out this SVG example:
--   https://elm-lang.org/examples/clock
--

import Browser
import Html exposing (..)
import Task
import Time



-- MAIN


main =
  Browser.element
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }



-- MODEL


type alias Model =
  { zone : Time.Zone
  , time : Time.Posix
  }


init : () -> (Model, Cmd Msg)
init _ =
  ( Model Time.utc (Time.millisToPosix 0)
  , Task.perform AdjustTimeZone Time.here
  )



-- UPDATE


type Msg
  = Tick Time.Posix
  | AdjustTimeZone Time.Zone



update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Tick newTime ->
      ( { model | time = newTime }
      , Cmd.none
      )

    AdjustTimeZone newZone ->
      ( { model | zone = newZone }
      , Cmd.none
      )



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
  Time.every 1000 Tick



-- VIEW


view : Model -> Html Msg
view model =
  let
    hour   = String.fromInt (Time.toHour   model.zone model.time)
    minute = String.fromInt (Time.toMinute model.zone model.time)
    second = String.fromInt (Time.toSecond model.zone model.time)
  in
  h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]
```

</div>
</details>

これでページモジュールの準備は完了です。

## メインモジュールの記述（ここが大変めんどくさい・・・）

さて、前節で準備した二つのページモジュールを利用してSPAを組み上げてゆきましょう。
数多の猛者達がコード自動生成等の秘術を編み出されている模様ですが、
根本的には以下のようなコードを走らせていることかと思います[^リサーチ不足]。
[^リサーチ不足]:特にリサーチはしていないし自動生成とかは使ったことがないので，見当違いでしたらすみません。

```elm:src/Main.elm
module Main exposing (..)

import Browser
import Html as H exposing (Html)
import Html.Events as HE
import Pages.DigitalClock
import Pages.RandomDice


type Model
    = RandomDiceModel Pages.RandomDice.Model
      --こんな感じでページ数分の分岐を書く。
    | DigitalClockModel Pages.DigitalClock.Model


type Msg
    = GotRandomDiceMsg Pages.RandomDice.Msg
      --こんな感じでページ数分の分岐を（略）
    | GotDigitalClockMsg Pages.DigitalClock.Msg
    | PageChanged ( Model, Cmd Msg )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case ( msg, model ) of
        ( GotRandomDiceMsg innerMsg, RandomDiceModel innerModel ) ->
            Pages.RandomDice.update
                innerMsg
                innerModel
                |> Tuple.mapBoth
                    RandomDiceModel
                    (Cmd.map GotRandomDiceMsg)

        --こんな感じでページ数分の（略）
        ( GotDigitalClockMsg innerMsg, DigitalClockModel innerModel ) ->
            Pages.DigitalClock.update
                innerMsg
                innerModel
                |> Tuple.mapBoth
                    DigitalClockModel
                    (Cmd.map GotDigitalClockMsg)

        ( PageChanged ( newModel, newCmd ), _ ) ->
            ( newModel, newCmd )

        _ ->
            --リンタの設定によっては怒られるけど握りつぶしとく。
            ( model, Cmd.none )


init : () -> ( Model, Cmd Msg )
init _ =
    Pages.RandomDice.init ()
        |> Tuple.mapBoth
            RandomDiceModel
            (Cmd.map GotRandomDiceMsg)


subscriptions : Model -> Sub Msg
subscriptions model =
    case model of
        RandomDiceModel innerModel ->
            Pages.RandomDice.subscriptions innerModel
                |> Sub.map GotRandomDiceMsg

        --こんな感じで（略）
        DigitalClockModel innerModel ->
            Pages.DigitalClock.subscriptions innerModel
                |> Sub.map GotDigitalClockMsg


view : Model -> Html Msg
view model =
    H.div []
        [ case model of
            RandomDiceModel innerModel ->
                Pages.RandomDice.view innerModel
                    |> H.map GotRandomDiceMsg

            --こんな（略）
            DigitalClockModel innerModel ->
                Pages.DigitalClock.view innerModel
                    |> H.map GotDigitalClockMsg
        , H.button
            [ Pages.RandomDice.init ()
                |> Tuple.mapBoth
                    RandomDiceModel
                    (Cmd.map GotRandomDiceMsg)
                |> PageChanged
                |> HE.onClick
            ]
            [ H.text "サイコロを振る" ]
        , H.br [] []
        , H.button
            [ Pages.DigitalClock.init ()
                |> Tuple.mapBoth
                    DigitalClockModel
                    (Cmd.map GotDigitalClockMsg)
                |> PageChanged
                |> HE.onClick
            ]
            [ H.text "時刻を見る" ]
        ]


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , subscriptions = subscriptions
        , update = update
        , view = view
        }
```

<details><summary>このコードの解説</summary>

まず`Model`にせよ`Msg`にせよ、各ページに対応したバリアントを用意してそれぞれのデータをwrapしています。

`update`では`Msg`型の値が届いたら、まず現在の`Model`と同じページ由来のものかを`case`分岐で照合して、
- 違っていたら処理を破棄（何もしない）
- 合っていたら`Model`と`Msg`の中身を取り出して(unwrapして)該当するページの`update`関数に渡し、返ってきた`( そのページのModel, Cmd そのページのMsg)`の各成分を再びwrapして型を合わせる。

ということをしています。参考：[`Tuple.mapBoth`](https://package.elm-lang.org/packages/elm/core/latest/Tuple#mapBoth)と[`Cmd.map`](https://package.elm-lang.org/packages/elm/core/latest/Platform-Cmd#map)

上記がわかれば`init`、`view`、`subscriptions`は流れで読めるかと思うので気合で頑張ってください。参考:[`Sub.map`](https://package.elm-lang.org/packages/elm/core/latest/Platform-Sub#map)

</details>

いやはや、これはめんどっちいですね・・・。この書き方だと，ページ数にほとんど比例する形でコードの行数が増えてゆきます。ちゃんとしたspaではこれにさらにurlの操作が加わってくるわけで、記述量はさらに増えます[^urlのパース]。

[^urlのパース]:urlのパース処理は「ボイラープレート」というほど単調ではないかもしれませんが、ここでは割愛します。

すべてのページを同じ型のデータとして扱うことでこれらのボイラープレート達を一掃することを狙ったのが、拙作パッケージ`kudzu-forest/elm-page`になります。


# kudzu-forest/elm-pageの紹介
さっそく当該パッケージを用いたSPAの書き方を見てゆきましょう。

英語が大丈夫な方は[パッケージサイト](https://package.elm-lang.org/packages/kudzu-forest/elm-page/latest/Page)も合わせて読んで貰うのが良いかもしれません。chatGPTさんの校正を受けながら書いたのでそんなに酷い英文にはなっていない…はず…。

## ページモジュールの修正（めんどくさくない）
まず、`Pages/RandomDice.elm`に以下のような修正を加えましょう。[^修正点]
```diff:Pages/RandomDice.elm
- module Pages.RandomDice exposing (Model, Msg, init, subscriptions, update, view)
+ module Pages.RandomDice exposing (program)

+ import Page

+ program : Page.Program ()
+ program =
+     Page.element { init = init, subscriptions = subscriptions, update = update, view = view}
```
[^修正点]:言い換えると、拙作パッケージから`Page`モジュールを`import`し、`Browser.element`と同じ引数を`Page.element`に渡した返値を公開してください、ということです。

<details><summary>修正後のPages.RandomDiceモジュール</summary>

<div>

```elm:src/Pages/RandomDice.elm
module Pages.RandomDice exposing (program)
-- Press a button to generate a random number between 1 and 6.
--
-- Read how it works:
--   https://guide.elm-lang.org/effects/random.html
--

import Browser
import Html exposing (..)
import Html.Events exposing (..)
import Page
import Random



-- MAIN


main =
  Browser.element
    { init = init
    , update = update
    , subscriptions = subscriptions
    , view = view
    }

program =
  Page.element
    { init = init
    , update = update
    , subscriptions = subscriptions
    , view = view
    }


-- MODEL


type alias Model =
  { dieFace : Int
  }


init : () -> (Model, Cmd Msg)
init _ =
  ( Model 1
  , Cmd.none
  )



-- UPDATE


type Msg
  = Roll
  | NewFace Int


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Roll ->
      ( model
      , Random.generate NewFace (Random.int 1 6)
      )

    NewFace newFace ->
      ( Model newFace
      , Cmd.none
      )



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
  Sub.none



-- VIEW


view : Model -> Html Msg
view model =
  div []
    [ h1 [] [ text (String.fromInt model.dieFace) ]
    , button [ onClick Roll ] [ text "Roll" ]
    ]
```

</div>

</details>

`Pages/DigitalClock`にも同様の処理を施します。

<details><summary>デジタル時計のコード</summary>

<div>

```elm:src/Pages/DigitalClock.elm
module Pages.DigitalClock exposing (program)
-- Show the current time in your time zone.
--
-- Read how it works:
--   https://guide.elm-lang.org/effects/time.html
--
-- For an analog clock, check out this SVG example:
--   https://elm-lang.org/examples/clock
--

import Browser
import Html exposing (..)
import Page
import Task
import Time



-- MAIN


main =
  Browser.element
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }

program : Page.Program ()
program =
  Page.element
    { init = init
    , view = view
    , update = update
    , subscriptions = subscriptions
    }


-- MODEL


type alias Model =
  { zone : Time.Zone
  , time : Time.Posix
  }


init : () -> (Model, Cmd Msg)
init _ =
  ( Model Time.utc (Time.millisToPosix 0)
  , Task.perform AdjustTimeZone Time.here
  )



-- UPDATE


type Msg
  = Tick Time.Posix
  | AdjustTimeZone Time.Zone



update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Tick newTime ->
      ( { model | time = newTime }
      , Cmd.none
      )

    AdjustTimeZone newZone ->
      ( { model | zone = newZone }
      , Cmd.none
      )



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
  Time.every 1000 Tick



-- VIEW


view : Model -> Html Msg
view model =
  let
    hour   = String.fromInt (Time.toHour   model.zone model.time)
    minute = String.fromInt (Time.toMinute model.zone model.time)
    second = String.fromInt (Time.toSecond model.zone model.time)
  in
  h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]
```

</div>

</details>

## メインモジュールの記述（あんまりめんどくさくない！！）

では、これらの子モジュールを呼び出す親モジュール側の書き方を見てゆきましょう。
親モジュール側で`Page`により抽象化された子モジュールの`Model`や`Msg`をハンドリングするために、
`Page`には以下の４つの関数が定義されています。

###### ハンドリング用の関数：

```elm
Page.mapInit :
    ( (Page.Model, Cmd Page.Msg) -> appMsg )
      -> Page.Program flag
      -> flag
      -> appMsg

Page.mapSubscriptions : ( Page.Msg -> appMsg ) -> Page.Model -> Sub appMsg

Page.mapView : ( Page.Msg -> appMsg ) -> Page.Model -> Html appMsg

Page.update : Page.Msg -> Page.Model -> ( Page.Model, Cmd Page.Msg )

```
ここで，`appMsg`はページを利用する側のモジュール（今回の記事では`src/Main.elm`）で定義された`Msg`です。
関数名と型注釈から各々の役割を想像してみてください。
イメージとしては、
- `Page.Model`が子モジュールの`Model`に相当する型。
- `Page.Msg`は子モジュールの`Msg`に相当する型
- `mapInit`は、
    - 普通のアプリケーションで`init`が`(Model, Cmd Msg)`を返すのと同様に、`Page.Program flag`に`flag`を渡して初期化したらモデルとコマンドのペア`( Page.Model, Cmd Page.Msg)`が返ってくる仕組みになっている。
    - ただし、それを親モジュールのメッセージ（ここでは`appMsg`という型名になっている。）に変換する必要があるので、その変換器を第一引数として渡す。
- `mapView`及び`mapSubscriptions`は、
    - 第一引数として`Page.Msg`から親モジュールの`appMsg`への変換タグを渡す。
    - 親モジュールの`update`で、`Page.Msg`を当該`Page.Model`とともに`Page.update`に渡し、子モジュールのモデルとコマンドのペア`( Page.Model, Cmd Page.Msg)`による更新処理を記述する。

という感じです。
ここで特筆すべきは、`Page.Model`にしろ`Page.Msg`にしろ、型変数を持っていないという点です。
要は、`Pages.RandomDice`で定義された`program`も`Pages.DigitalClock`で定義された`program`も、親モジュールから見ると同じ型の値として扱えます。

これらを用いた親モジュールの書き方は以下のような感じになります。

```elm:src/Main.elm
module Main exposing (..)

import Browser
import Html as H exposing (..)
import Html.Events as HE
import Page
import Pages.DigitalClock as DigitalClock
import Pages.RandomDice as RandomDice


type alias Model =
    -- ページごとのバリアントは不要！
    { page : Page.Model
    }


type
    Msg
    -- ページごとバリアントは不要！
    = PageInitialized ( Page.Model, Cmd Page.Msg )
    | GotPageMsg Page.Msg


init : () -> ( Model, Cmd Msg )
init _ =
    -- ここでは親モジュールの初期化時に
    -- 子モジュールの初期化も同時に行っている。
    update
        (Page.mapInit PageInitialized
            RandomDice.program
            ()
         --  この()はflag。
        )
        (Model Page.empty)


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        PageInitialized ( initialPage, initialCmd ) ->
            ( { model | page = initialPage }
            , Cmd.map GotPageMsg initialCmd
            )

        GotPageMsg pMsg ->
            let
                ( newPage, pageCmd ) =
                    Page.update pMsg model.page
            in
            ( { model | page = newPage }
            , Cmd.map GotPageMsg pageCmd
            )


subscriptions : Model -> Sub Msg
subscriptions model =
    Page.mapSubscriptions GotPageMsg model.page


view : Model -> Html Msg
view model =
    div []
        [ Page.mapView GotPageMsg model.page
        , H.br [] []
        , H.text ""
        , H.br [] []
        , button
            [ HE.onClick <|
                Page.mapInit PageInitialized
                    RandomDice.program
                    ()
            ]
            [ text "サイコロを振る" ]
        , H.br [] []
        , button
            [ HE.onClick <|
                Page.mapInit PageInitialized
                    DigitalClock.program
                    ()
            ]
            [ text "時刻を見る" ]
        ]


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , subscriptions = subscriptions
        , update = update
        , view = view
        }
```

各モジュールに関する場合分けが不要となったため、
コード量がいくらか削減されています。
この効果は<strong>ページ数が多くなるほど絶大</strong>です。

SPAのコード量に圧倒されて手が出ていないという方がいらっしゃいましたら
是非当パッケージをお試しください。

また、[このパッケージのGitHubリポジトリ](https://github.com/kudzu-forest/elm-page)を見てもらえば、
- もっと簡単な例
- もっとページ数の多い例
- Urlまわりを扱っている例（親モジュールで`Browseer.application`使うやつ）

等も御覧いただけます。

# このパッケージの仕組み（気になる人向け）

ここから先はよろしければ
[GitHubリポジトリ](https://github.com/kudzu-forest/elm-page)の`src/Page.elm`と照合しながらご覧いただければと思います。

さて，実装についてですが，まず一言謝罪させてください。このモジュールの関数名や型定義は利用者を若干騙すような設計になっています。使う分にはわかりやすくなっていると思うのですが、あまり名前のイメージに引っ張られると実装が読みにくくなると思うので気を付けてください。

では内容に入ってゆきましょう。

## 型の内部実装
本パッケージで扱う`Page.Model`、`Page.Msg`、`Page.Program flag`の型定義は以下のようになっています。このコードで登場する`Model`や`Msg`という文字列は全て以下の`Page`モジュールで定義された値であることに気を付けてください。

```elm:Page

type Model
    = Model
        { key : Unique.Unique
        , html : Html Msg
        , sub : Sub Msg
        }

type Msg
    = Updated ( () -> ( Model, Cmd Msg) )

type Program flag
    = Program ( flag -> ( Model, Cmd Msg) )
```

<details><summary>Unique.Uniqueについて</summary>

[harrysarson/elm-hacky-unique](https://package.elm-lang.org/packages/harrysarson/elm-hacky-unique/1.0.0/)というパッケージ[^unique]で定義されたもので、「メッセージを吐いたページと更新しようとしているページが同じページか」という判定を行うために利用しています。ここでいう『同じ』とは、『同じ`Page.mapInit`の__呼び出し__で初期化された』という意味で、`mapInit`に渡す引数が等しくともタイミングが違えば『異なるページ』と判断されます。

</details>

[^unique]:elm-hacky-uniqueはElmが`NaN`同士の`==`による比較で`False`を吐くというある種のバグを利用して、Elmの世界にJavaScriptオブジェクトの参照比較の概念を持ち込むという闇の魔術をひっそりと実行しているチャレンジングなパッケージです。明日にでもEvan氏がコンパイラを修正して機能を失うリスクが0ではないです。（パフォーマンスの都合で放っておかれているバグだと思われるので限りなく0に近いです。万一の場合はelm-pageもユーザーに一意なキーを登録してもらうような形になるかと思います。）

Opaque typeを得るため&型の無限ループを許容するためのタグを無視すれば、`Page.Model`は`Html Page.Msg`と`Sub Page.Msg`の組で、
`Page.Msg`は組`( Page.Model, Cmd Page.Msg)`を得る処理を遅延させたものであるようです。
この段階ですでに再帰的構造になっていてわかりにくいですが、
これと[先ほどの4関数](#ハンドリング用の関数)を見比べると処理内容が想像しやすいかもしれません。

## 各関数の実装
実は`Page.update`は型シグネチャこそ我々が普段書いている`update`関数に寄せてありますが、やっていることは`Page.Msg`型のunwrapにすぎません。その際、第一引数の内部から引っ張り出した`Page.Model`と第二引数として渡した`Page.Model`のキー（`Unique.Unique`）を比較し、不一致な場合は処理を放棄する設計になっています。
この設計により時間がかかる処理をコマンドで出してから他のページに遷移した際、処理が終わって前のページに強制的に引き戻されるという事象を防ぐことができます。同時に、バックグラウンドで`Page.Model`を保持しておけばそちらのアップデートは可能となっています。

また、`mapView`や`mapSubscriptions`は単純に`Page.Model`の中身を取り出して第一引数の関数を用いて`Html.map`や`Sub.map`で変換しているだけです。
`mapInit`も似たようなものですね。

そうなると、各子モジュールの方で定義された`Model`型の値はどこに保存されているのか、という事が気になってきます。

## `Pages.DigitalClock.Model`のありか
結論から言うと、`Page.Model`の`html`や`sub`の中に保存されている、という事になります。

`Html msg`が状態をもてる、という極端な例として[以下の独立したElmプログラム](https://ellie-app.com/sJ4Kt5yKyD5a1)を動かしてみると良いかもしれません。
```elm
module MinimalCounter exposing (..)

import Browser
import Html as H exposing (Html)
import Html.Events as HE


type alias Model =
    Html Msg


type Msg
    = Updated (() -> Model)


init : Int -> Html Msg
init n =
    H.div []
        [ H.button
            [ HE.onClick <| Updated (\() -> init (n + 1))
            ]
            [ H.text "+" ]
        , H.text <| String.fromInt n
        ]


main : Program () Model Msg
main =
    Browser.sandbox
        { init = init 0
        , update = \(Updated f) model -> f ()
        , view = identity
        }
```
このコードでは`Model`自身は`Int`を保持していないにもかかわらず、
あたかも整数値が記録されているかのように振る舞います。
まったく同じ定義の`Model`と`Msg`でも、`init`の部分だけうまくいじれば
`String`型の値を保持させることもできるでしょう。
要は`Html Msg`が`Msg`を生じるために必要な情報として`Model`を保持させてしまえばいいのです。
この関数をHtmlのツリー構造にハードコードするのではなく、
[`Html.map`](https://package.elm-lang.org/packages/elm/html/latest/Html#map)を用いて外部に切り出すことも可能です。
この発想をコンパイラの協力のもと発展させていけば、
`Model`と`Msg`が隠蔽された`sandbox`相当の実装まで辿り着けます。

<details><summary>演習：`sandbox`相当の実装（※本パッケージのものとは異なる）</summary>

解答例：

<div>

```elm
module SandboxTest exposing (..)

import Browser
import Html as H exposing (Html)
import Html.Events as HE


type OuterMsg
    = Updated (() -> Html OuterMsg)


type alias Model =
    Html OuterMsg


sandbox :
    { init : model
    , update : msg -> model -> model
    , view : model -> Html msg
    }
    -> Html OuterMsg
sandbox { init, update, view } =
    let
        mapper : model -> msg -> OuterMsg
        mapper model msg =
            Updated <|
                \() ->
                    sandbox
                        { init = update msg model
                        , update = update
                        , view = view
                        }
    in
    H.map (mapper init) <| view init
    
-- 使用例：


type InnerMsg
    = Increment
    | Decrement


type alias InnerModel =
    Int


counter : Html OuterMsg
counter =
    -- 別モジュールに切り分けてもOK！
    let
        init : InnerModel
        init =
            0
            
        update : InnerMsg -> InnerModel -> InnerModel
        update msg model =
            case msg of
                Increment ->
                    model + 1

                Decrement ->
                    model - 1
                    
        view : InnerModel -> Html InnerMsg
        view model =
            H.div []
                [ H.button [ HE.onClick Decrement ] [ H.text "-" ]
                , H.br [] []
                , H.text <| String.fromInt model
                , H.br [] []
                , H.button [ HE.onClick Increment ] [ H.text "+" ]
                ]
    in
    sandbox { init = init, update = update, view = view }
    
type alias AppModel =
    Html OuterMsg
    
type AppMsg
    = GotOuterMsg OuterMsg
    
main : Program () AppModel AppMsg
main =
    let
        init : AppModel
        init =
            counter
            
        update : AppMsg -> AppModel -> AppModel
        update msg model =
            case msg of
                GotOuterMsg (Updated f) ->
                    f ()
                    
        view : AppModel -> Html AppMsg
        view model =
            H.map GotOuterMsg model
    in
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }
```

このコードはこのまま[Ellie](https://ellie-app.com/sJ3ZdgZ9cLda1)で動かせます。
色々弄って遊んでみてください。

</div>

</details>

## 残りの作業

ここまででカギとなるアイデアは紹介できました。さらに副作用を扱えるようにするためには、`Cmd Msg`や`Sub Msg`の取り扱いが必要になりますが、これは上記のように型を調製し、`Html.map`、`Cmd.map`、及び`Sub.map`を併用すれば良いだけです。
後の細かい部分は実装を眺めてみてください。

## 実装紹介の終わりに
ここで紹介したアイデア、特にHtmlへの状態の埋め込みなどはTEAライクなモジュール化を狙うあらゆるパッケージ設計において利用可能なものかと思うので[^背景]、
これからパッケージを作ろうという方はぜひ状態の管理方法として検討に入れて頂ければと思います。

ではでは皆さま、Happy Elm coding!

[^背景]:そもそもこのパッケージを作った動機が[クイズアプリのための別のパッケージ](https://package.elm-lang.org/packages/kudzu-forest/elm-questions/latest)を書いている途中で「Htmlへの状態の埋め込み」を思いついたので、それでどこまでゆけるか試してみよう、ということだったりします。思ったより遠くまでこれたので皆様とシェアさせていただきたくパッケージを整えてこの記事を書いた次第です。


[package]: https://package.elm-lang.org/packages/kudzu-forest/elm-page/latest/
[GitHub]: https://github.com/kudzu-forest/elm-page