初めまして!趣味でElmでWebサイトを作っている予備校講師1のkudzuないしはkudzunyanと申します2。
記事は初投稿となりますがけっこう気合を入れて書いたのでフィードバックをいただけると嬉しいです。
はじめに
- この記事は拙作パッケージkudzu-forest/elm-pageの紹介のために書かれたものです。
- 対象読者: ElmでSPAを
- やっている人
- やろうとして挫折した人
- やってみたいと心の片隅で思っている人
- 対象外読者:
- SPAの
Model
やMsg
の型定義を書くのが楽しくて仕方がない人- 引き続き楽しいcoding生活を~
- 万一書き飽きてきたら再訪してください。
-
TEA
って文字列でお茶しか思い浮かばない人- まず公式ガイドを読んでみてください。
- SPAの
TL;DR3
- SPAのページ管理がちょっぴり楽になるパッケージを作りました。
-
Browser.element
やBrowser.sandbox
を置き換えるだけでページ側のモジュールが定義できます。 - どんな
Model
やMsg
が定義されたページでも、利用側では同じ型のデータとして扱えます。 - Elmで作成済みのマルチページのWebアプリがあればほぼコピペだけでSPA化できます。
-
Introduction:今までのSPAの書き方はめんどくさい!
というわけで特に工夫をしない場合のSPAの書き方をおさらいしておきましょう。
本当にめんどっちいのでurl周りは省略してBrowser.element
で書ける範囲だけみてゆきます4。
Browser.application
を用いた本格的なSPAを作る例は当該パッケージのGitHubリポジトリをご覧ください。
工夫しないページモジュールの準備(ここは別にめんどくさくない)
まずは各ページのコンテンツを用意しましょう。
ここではコマンドを用いる例を見たいので、皆さんお馴染みのAn Introduction to 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
を公開していることであり、実を言うと、一行目以外は公式ガイドのコードから全く変更していません5。Ctrl-c
からのCtrl-v
です。
サイコロだけだとsubscriptions
が空で寂しいのでデジタル時計のコードも借りてきましょうか。
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) ]
これでページモジュールの準備は完了です。
メインモジュールの記述(ここが大変めんどくさい・・・)
さて、前節で準備した二つのページモジュールを利用してSPAを組み上げてゆきましょう。
数多の猛者達がコード自動生成等の秘術を編み出されている模様ですが、
根本的には以下のようなコードを走らせていることかと思います6。
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
}
このコードの解説
まずModel
にせよMsg
にせよ、各ページに対応したバリアントを用意してそれぞれのデータをwrapしています。
update
ではMsg
型の値が届いたら、まず現在のModel
と同じページ由来のものかをcase
分岐で照合して、
- 違っていたら処理を破棄(何もしない)
- 合っていたら
Model
とMsg
の中身を取り出して(unwrapして)該当するページのupdate
関数に渡し、返ってきた( そのページのModel, Cmd そのページのMsg)
の各成分を再びwrapして型を合わせる。
ということをしています。参考:Tuple.mapBoth
とCmd.map
上記がわかればinit
、view
、subscriptions
は流れで読めるかと思うので気合で頑張ってください。参考:Sub.map
いやはや、これはめんどっちいですね・・・。この書き方だと,ページ数にほとんど比例する形でコードの行数が増えてゆきます。ちゃんとしたspaではこれにさらにurlの操作が加わってくるわけで、記述量はさらに増えます7。
すべてのページを同じ型のデータとして扱うことでこれらのボイラープレート達を一掃することを狙ったのが、拙作パッケージkudzu-forest/elm-page
になります。
kudzu-forest/elm-pageの紹介
さっそく当該パッケージを用いたSPAの書き方を見てゆきましょう。
英語が大丈夫な方はパッケージサイトも合わせて読んで貰うのが良いかもしれません。chatGPTさんの校正を受けながら書いたのでそんなに酷い英文にはなっていない…はず…。
ページモジュールの修正(めんどくさくない)
まず、Pages/RandomDice.elm
に以下のような修正を加えましょう。8
- 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}
修正後のPages.RandomDiceモジュール
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" ]
]
Pages/DigitalClock
にも同様の処理を施します。
デジタル時計のコード
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) ]
メインモジュールの記述(あんまりめんどくさくない!!)
では、これらの子モジュールを呼び出す親モジュール側の書き方を見てゆきましょう。
親モジュール側でPage
により抽象化された子モジュールのModel
やMsg
をハンドリングするために、
Page
には以下の4つの関数が定義されています。
ハンドリング用の関数:
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
も、親モジュールから見ると同じ型の値として扱えます。
これらを用いた親モジュールの書き方は以下のような感じになります。
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
}
各モジュールに関する場合分けが不要となったため、
コード量がいくらか削減されています。
この効果はページ数が多くなるほど絶大です。
SPAのコード量に圧倒されて手が出ていないという方がいらっしゃいましたら
是非当パッケージをお試しください。
また、このパッケージのGitHubリポジトリを見てもらえば、
- もっと簡単な例
- もっとページ数の多い例
- Urlまわりを扱っている例(親モジュールで
Browseer.application
使うやつ)
等も御覧いただけます。
このパッケージの仕組み(気になる人向け)
ここから先はよろしければ
GitHubリポジトリのsrc/Page.elm
と照合しながらご覧いただければと思います。
さて,実装についてですが,まず一言謝罪させてください。このモジュールの関数名や型定義は利用者を若干騙すような設計になっています。使う分にはわかりやすくなっていると思うのですが、あまり名前のイメージに引っ張られると実装が読みにくくなると思うので気を付けてください。
では内容に入ってゆきましょう。
型の内部実装
本パッケージで扱うPage.Model
、Page.Msg
、Page.Program flag
の型定義は以下のようになっています。このコードで登場するModel
やMsg
という文字列は全て以下の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) )
Unique.Uniqueについて
harrysarson/elm-hacky-uniqueというパッケージ9で定義されたもので、「メッセージを吐いたページと更新しようとしているページが同じページか」という判定を行うために利用しています。ここでいう『同じ』とは、『同じPage.mapInit
の__呼び出し__で初期化された』という意味で、mapInit
に渡す引数が等しくともタイミングが違えば『異なるページ』と判断されます。
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プログラムを動かしてみると良いかもしれません。
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
を用いて外部に切り出すことも可能です。
この発想をコンパイラの協力のもと発展させていけば、
Model
とMsg
が隠蔽されたsandbox
相当の実装まで辿り着けます。
演習:`sandbox`相当の実装(※本パッケージのものとは異なる)
解答例:
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で動かせます。
色々弄って遊んでみてください。
残りの作業
ここまででカギとなるアイデアは紹介できました。さらに副作用を扱えるようにするためには、Cmd Msg
やSub Msg
の取り扱いが必要になりますが、これは上記のように型を調製し、Html.map
、Cmd.map
、及びSub.map
を併用すれば良いだけです。
後の細かい部分は実装を眺めてみてください。
実装紹介の終わりに
ここで紹介したアイデア、特にHtmlへの状態の埋め込みなどはTEAライクなモジュール化を狙うあらゆるパッケージ設計において利用可能なものかと思うので10、
これからパッケージを作ろうという方はぜひ状態の管理方法として検討に入れて頂ければと思います。
ではでは皆さま、Happy Elm coding!
-
どちらを使うかは「にゃん」が許される空気かによります。 ↩
-
TL;DRというのは「too long; did'nt read=長すぎて読めんかったわこのタワケが!」の略だそうですね。すでに青筋が立ち始めていてかつSPAのコードに慣れ親しんでいる方は、拙作パッケージを用いた書き方まで飛んでいただいても大丈夫です。 ↩
-
要は「複数ページの
Model
やMsg
のハンドリングの部分だけ解説する」ということです。ページの切り替えはボタン操作にしちゃいます。 ↩ -
各ページごとの
main
関数は本来不要となりますが、別に書いてあっても邪魔にはならないので放っておいてOKです。そうすればデバッグ時は問題のあるページのみをコンパイルして動作確認をすることができ、より親切なエラーメッセージを享受しながらリファクターできます。 ↩ -
特にリサーチはしていないし自動生成とかは使ったことがないので,見当違いでしたらすみません。 ↩
-
urlのパース処理は「ボイラープレート」というほど単調ではないかもしれませんが、ここでは割愛します。 ↩
-
言い換えると、拙作パッケージから
Page
モジュールをimport
し、Browser.element
と同じ引数をPage.element
に渡した返値を公開してください、ということです。 ↩ -
elm-hacky-uniqueはElmが
NaN
同士の==
による比較でFalse
を吐くというある種のバグを利用して、Elmの世界にJavaScriptオブジェクトの参照比較の概念を持ち込むという闇の魔術をひっそりと実行しているチャレンジングなパッケージです。明日にでもEvan氏がコンパイラを修正して機能を失うリスクが0ではないです。(パフォーマンスの都合で放っておかれているバグだと思われるので限りなく0に近いです。万一の場合はelm-pageもユーザーに一意なキーを登録してもらうような形になるかと思います。) ↩ -
そもそもこのパッケージを作った動機がクイズアプリのための別のパッケージを書いている途中で「Htmlへの状態の埋め込み」を思いついたので、それでどこまでゆけるか試してみよう、ということだったりします。思ったより遠くまでこれたので皆様とシェアさせていただきたくパッケージを整えてこの記事を書いた次第です。 ↩