注意書き
※この記事は自作パッケージの宣伝記事です。年末にもかかわらず仕事が減らない男がムシャクシャして勢いだけで書きました。対象読者はElm中級以上(Elmの清浄な空気に慣れすぎて初期化時の無意味なデフォルト値に嫌悪感を抱くようになってしまった人種)です。
本編
Elmerの皆さん、初期化処理ってめんどくさいですよね?ねっ?
例えば、公式ガイドのTimeの節から借りてきた
init =
\_ ->
( { zone = Time.utc
, time = Time.millisToPosix 0 -- このデフォルト値のせいで時計に一瞬0が表示される
}
, Cmd.batch
[ Task.perform AdjustTimeZone Time.here
, Task.perform Tick Time.now
]
)
はモヤモヤする1けど、
init =
\_ ->
( { zone = Nothing
, time = Nothing
}
, Cmd.batch
[ Task.perform AdjustTimeZone Time.here
, Task.perform Tick Time.now
]
)
-- え、zoneとかtimeとか無いの最初の一瞬だけなのにこの後毎回パターンマッチすんの?
てやるのも
type alias InitialModel = { mZone : Maybe Time.Zone, mTime : Maybe Time.Posix }
type alias RunningModel = { zone : Time.Zone, time : Time.Posix }
type Model
= InitialModel InitialModel
| RunningModel RunningModel
init =
\_ ->
( InitialModel Nothing Nothing
, Cmd.batch
[ Task.perform AdjustTimeZone Time.here
, Task.perform Tick Time.now
]
)
update msg model =
case (msg, model) of ...
view model =
case model of ...
-- え、ここまでやる?(spaでも作るつもりか?)
てやるのも嫌なときってありますよね!?ねっ!?
そこで私が作ったパッケージでは、
type alias InitialModel = { mZone : Maybe Time.Zone, mTime : Maybe Time.Posix }
-- 初期化フェーズが完了するまでの仮Model
type alias Model = { zone : Time.Zone, time : Time.Posix }
-- 実働フェーズで活躍するModel。(Cmdで取得するようなデータもMaybeにしなくてよい!)
main =
Initialization.element
{ init =
\flag ->
( InitialModel Nothing Nothing
, Cmd.batch
[ Task.perform
(\timezone iModel -> { iModel | mZone = timezone })
Time.here
, Task.perform
(\posix iModel -> { iModel | mTime = posix })
Time.now
]
-- どうせ初期化段階ってモデルの更新くらいしかしないんだから
-- `Cmd (InitialModel -> InitialModel)` でいいっしょ。
-- いちいち専用のMsg作らなくていいし。
)
, toRunning =
-- InitializingModelのバリデーションと変換をセットにした、
-- Result (List blockingReason) ( Model, Cmd Msg )を返す関数。
-- Err <リスト> はまだ初期フェーズを抜けられない理由のリスト。
-- Ok ( model, Cmd msg )はいつものinitの返値がOkにくるまれてる感じ。
-- InitialModelの更新のたびに呼ばれて、
-- Okが返ると初期化フェーズ終了。
\iModel ->
let
zoneResult = Result.fromMaybe "タイムゾーン未取得" iModel.mZone
timeResult = Result.fromMaybe "時刻未取得" iModel.mTime
in
Ok (\zone time -> (Model zone time, Cmd.none) )
|> Initialization.accumulate zoneResult
|> Initialization.accumulate timeResult
-- 補助関数accumulateの型は、
-- Result x a -> Result (List x) (a -> b) -> Result (List x) b。
-- 成功結果は一つずつ引数として受け取りつつ、
-- 失敗理由はリストとして積み上げる。
-- (なんでコレ公式に無いんだろ・・・)
, initView =
-- 初期化完了前の画面表示。
-- toRunningの失敗理由とinitializingModelを引数にとる。
\blockingReasons iModel ->
blockingReasons
|> List.map Html.text
|> List.intersperse (Html.br [] [])
|> Html.div []
}
{ subscriptions = ... -- この面々はあたかも初期化フェーズなどなくて
, update = ... -- 初めからtoRunningの返値のOkの中身が
, view = ... -- いつものinitから返されたつもりで書けばOK!
}
という風に書けるようになっています!
今すぐターミナルにelm install kudzu-forest/elm-initializationを打ってエンターキーをタターンッてしちゃってください!ね!!!
ちなみにこのままだとelm make --debug ほげ.elmしたときのデバッガーの
初期化フェーズのMsg表示がゴミ2なんで、
そこを改善するためだけのInitialization.Debuggableというモジュールも作りました3!
Cmdを動的に出したい?順番を制御したい?そんなあなたにはInitialization.Advanced!4
最後に代表的な関数の型シグネチャだけのっけて記事を締めます!興味を持てたらパッケージサイト5も見てみてね!それではみなさん良いお年を!
Initialization.element :
{ init : flag -> ( initializingModel, Cmd (initializingModel -> initializingModel) )
, initView : List blockingReason -> initializingModel -> Html (initializingModel -> initializingModel)
, toRunning : initializingModel -> Result (List blockingReason) ( model, Cmd msg )
}
->
{ subscriptions : model -> Sub msg
, update : msg -> model -> ( model, Cmd msg )
, view : model -> Html msg
}
-> Initialization.Program flag initializingModel model msg
-- blockingReasonはとりあえずStringでOK!
Initialization.Advanced.element :
{ init : flag -> ( initializingModel, Cmd initializingMsg )
, initSubscriptions : initializingModel -> Sub initializingMsg
, initUpdate : initializingMsg -> initializingModel -> ( initializingModel, Cmd initializingMsg )
, initView : List blockingReason -> initializingModel -> Html.Html initializingMsg
, toRunning : initializingModel -> Result (List blockingReason) ( runningModel, Cmd runningMsg )
}
->
{ subscriptions : runningModel -> Sub runningMsg
, update : runningMsg -> runningModel -> ( runningModel, Cmd runningMsg )
, view : runningModel -> Html.Html runningMsg
}
-> Initialization.Advanced.Program flag initializingModel initializingMsg runningModel runningMsg
-- ほとんど独立したアプリを二つ作っている感じ。
-
Evan氏に喧嘩を売っているわけではないです。念のため。(あのページはSubscription周りの説明が主目的なので余計な枝葉は生やさない方針が正しい。) ↩
-
Msgの中身が関数なのでデバッガーでの表示が
<internals>みたいになる。 ↩ -
誰にも使ってもらえないと徒労感が凄すぎて鬱になりそうです!みんな使って! ↩
-
spaとかやってみたことのある人はむしろAdvancedの方が分かりやすいはず。初期化フェーズと実働フェーズを別アプリとして実装できるようにしてtoRunning(上のコードで登場)による遷移を引っ付けただけの設計。(だから初期化フェーズのMsg型も自分で実装しないといけない。) ↩
-
チャッピーことchatgptさんに英文を構成してもらっているのでそこそこ読みやすいと思います!でもチャッピーさん、間違えてたところもベタ褒めしてくれてたので正直不安です!間違えてたり読みにくかったりしたら教えてください! ↩