github.com
背景
JavaScriptだと「起動時にサーバーへA, B, Cのデータの取得を問い合わせて全てデータがそろったら次の処理へ移る」というような実装をPromise.allで作ることがよくある。
これをElmで雑にやろうとすると以下のようなMaybeまみれのコードが生まれたりする。
type alias Model =
{ user : Maybe User
, posts : Maybe Posts
, favorites : Maybe Favorites
}
こうなるとupdate関数の中でデータを取り出すたびに毎度パターンマッチをしなければいけなくなり、とても冗長なコードになってしまう。また、毎回すべてのデータがJustかどうかをチェックしないといけなくなったりする。これは事故る可能性も高い。
可能であれば「全てのデータがそろっている/いない」を型で表現できるのが理想である。
elm-task-parallelを使う
ここでelm-task-parallelが使える。
まずはModelをこんな感じで設計する。この時点でひとつもMaybeが出てこない。
type Model
= Failed
| Loading (Task.Parallel.State3 Msg User Posts Favorites)
| Loaded User Posts Favorites
Loading
はデータをロード中であることを表現している。同時に取得したいデータの型の数に合わせて、使うStateN
型を変更する。ここではUser
, Posts
, Favorites
という3つのデータを取得待ちするのでState3
を使っている。
上のModelが用意できたので、取得の開始を行うinit関数を実装する。attemptN
という関数が公開されているので、同時に実行したいTaskの数に応じて数字を変えて呼び出す。なお、最大で9個のタスクまで扱うことができる。
init : () -> ( Model, Cmd Msg )
init _ =
Task.Parallel.attempt3
{ task1 = Api.fetchUser
, task2 = Api.fetchPosts
, task3 = Api.fetchFavorites
, onUpdates = LoadingUpdated
, onFailure = LoadingFailed
, onSuccess = LoadingFinished
}
|> Tuple.mapFirst Loading
続いてMsg型を以下のように実装する。ここでも、Msg3
となっているのは、取得を行うデータの数と同じである。
type Msg
= LoadingUpdated (Task.Parallel.Msg3 Http.Error User Posts Favorites)
| LoadingFailed Http.Error
| LoadingFinished User Posts Favorites
上記のMsg型を踏まえてupdate
関数を以下のように作る。
update : Msg -> Model -> ( Model, Cmd Msg )
udpate msg model =
case model of
Loading loadingState ->
case msg of
LoadingUpdated loadingMsg ->
Task.Parallel.update3 loadingState loadingMsg
|> Tuple.mapFirst Loading
LoaadingFailed _ ->
( Failed, Cmd.none )
LoadingFinished user posts favorites ->
( Loaded user posts favorites )
_ ->
( model, Cmd.none )
LoadingUpdated
バリアントは、最初に一気に実行されたTaskの結果を受け取るたびに呼び出され、Loading
バリアントが持つデータを逐次更新していく。パッケージを使う側である我々は、いまどのデータが揃っているのかを気にする必要はない。
最後にLoadedFinished
バリアントが飛んでくれば、ModelをLoaded
に更新するために必要なデータが全部揃う、という流れである。これでもうMaybe型を登場させる必要はない。
内部の実装はどうなっているのか
読んでみると分かるがそんなに難しいことはしていない。
例えば、3つのデータを持つState3
型はこんな感じの実装になっている。
type State3 msg a b c
= State3 (a -> b -> c -> msg) (Maybe a) (Maybe b) (Maybe c)
つまりパッケージの実装でMaybeを隠してくれているという感じである。
あとはupdateN
関数の中で、失敗状態(FailedStateN
)になっていない限り、ひとつづつMaybe型のデータが更新されていく、という流れになっている。
update3 : State3 msg a b c -> Msg3 a b c -> ( State3 msg a b c, Cmd msg )
update3 (State3 onSuccess a b c) msg =
let
next a_ b_ c_ =
( State3 onSuccess a_ b_ c_, Maybe.map3 onSuccess a_ b_ c_ |> toCmd )
in
case msg of
LoadedA3 data ->
next (Just data) b c
LoadedB3 data ->
next a (Just data) c
LoadedC3 data ->
next a b (Just data)
Taskモジュールのmap関数やらandThe関数を使ってつなげていくスタイルだと、どうしても逐次実行で一つづつ結果を待つことになるが、elm-task-parallelは一度Cmdに変換してElmランタイムへ投げることによって本当に並列実行が行われている。
実際にCmd.batch
でTaskの実行が一気に行われているのがattemptN
関数の実装から見て取れる。
attempt3 :
{ task1 : Task x a
, task2 : Task x b
, task3 : Task x c
, onUpdates : Msg3 a b c -> msg
, onSuccess : a -> b -> c -> msg
, onFailure : x -> msg
}
-> ( State3 msg a b c, Cmd msg )
attempt3 { task1, task2, task3, onUpdates, onSuccess, onFailure } =
( State3 onSuccess Nothing Nothing Nothing
, [ task1 |> routeTo (onUpdates << LoadedA3) onFailure
, task2 |> routeTo (onUpdates << LoadedB3) onFailure
, task3 |> routeTo (onUpdates << LoadedC3) onFailure
]
|> Cmd.batch
)