Elmで逐次的に or 並列でHttpを叩く方法

## ソースコードとサンプル
[サンプル](https://niso1985.github.io/elm-playground/)
[ソースコード](https://github.com/niso1985/elm-playground/blob/master/src/Pages/MultiHttpRequest.elm)
ページ遷移入ってますが、ほぼそのままMain.elmとかに使えるはず。

## 準備
Httpは副作用が発生する手続きである。
Elmで副作用が発生する手続きは`Cmd Msg`で表現されるが、
複数の副作用が連続するコードはTaskに変換したほうが楽。
ということで、まずはHttpをTaskで利用できるようにする。

### HttpでのTask利用
HttpをTaskで利用するには３つの材料が必要。

- Http.task
- Resolver
- Json.Decode [^1]

[^1]: 今回のサンプル上では使っていないが、JSONを利用するのであれば必要。そして、通常HTTPの利用シーンとしてJSONレスポンスを得ることがほとんどなので、ほぼ必須となる。

`Http.task`はHTTPリクエストを`Task`型で返し、`Resolver`はそのリクエストのレスポンスを受け取り、処理する関数である。

```elm
type alias ErrorMessage =
    String


jsonResolver : Http.Resolver ErrorMessage String
jsonResolver =
    -- いちいちHttpErrorをUpdateで処理するのは二度手間なので、Resolverで処理する
    Http.stringResolver <|
        \response ->
            case response of
                Http.BadUrl_ url ->
                    Err <| "Bad URL: " ++ url

                Http.Timeout_ ->
                    Err "Request Timeout"

                Http.NetworkError_ ->
                    Err "Network Error"

                -- BadStatusでもbodyが得られるので、レスポンスによってメッセージを分けたいのであれば、BadStatus用のDecoderを用意して、デコードしてやればよい
                Http.BadStatus_ metadata body ->
                    Err <| "BadStatus: " ++ String.fromInt metadata.statusCode ++ "\nBody: " ++ body

                Http.GoodStatus_ metadata body ->
                    -- 大抵の場合はここで以下のような形でJSONのデコードを行う
                    {-
                       case Json.Decode.decodeString decoder body of
                           Ok value ->
                               Ok value
                           Err err ->
                               Err <| "レスポンスのデコードに失敗しました。\n" ++ Json.Decode.errorToString err
                    -}
                    -- decoderはこの関数の引数で渡すと良い
                    Ok body


requestGet : String -> Task ErrorMessage String
requestGet url =
    Http.task
        { method = "GET"
        , headers = []
        , url = url
        , body = Http.emptyBody
        , resolver = jsonResolver
        , timeout = Nothing
        }

```

これでGETでのHTTPリクエストがTaskとして得られるようになったので、これを逐次的に実行したり、並列に実行するだけである。

#### おまけ(Taskの使い方)
Taskはelm-guideなどにも記載がほぼないので、馴染みがないかもしれないが、最悪これだけ覚えておけばよい。

- Taskは最終的に`Cmd Msg`として実行されるので、`Cmd Msg`の中間表現である
- `Cmd Msg`に変換するには`Task.perform`か`Task.attempt`を使う
- `Task.perform`は必ず成功するTaskに使う
- `Task.attempt`は失敗するかもしれないTaskに使う(Httpを使うケースはこちら)

詳細な説明は、@ababup1192 さんの[ElmのTaskにこんにちは](https://qiita.com/ababup1192/items/008c0adac797d90c1dff)を参照してください。


## Httpを逐次的(シーケンシャル)に実行
Task化したHttpリクエストを逐次的に実行するには、TaskのListを作り、`Task.sequence`をしてTaskをまとめ、`Task.attempt`を呼べばよい。
下記コードは４つのendpointに対してHTTP GETをシーケンシャルに呼び出すコードである。
request1〜４が順番に呼ばれるが、リクエストが失敗したタイミングで処理が止まり、次のリクエストは呼び出されない。

なお、request2とrequest3の呼び出しに`Task.andThen`を使っているが、これは**１つ前のリクエストの結果を使ってリクエストを行う**という時に利用できる。

```elm
-- 抜粋コード
requestSequential : Task ErrorMessage (List String)
requestSequential =
    let
        request1 =
            requestGet "https://dog.ceo/api/breeds/image/random"

        request2 b =
            let
                _ =
                    Debug.log "Response1: " b

                -- 前のAPIのレスポンスを使って、次のAPIを呼び出すようなAPIが簡単に見つからなかったので、
                -- 以前の結果を使っているよという証明で、Debug.logを使ってconsole.logに書き出しています。
            in
            requestGet "https://data.ripple.com/v2/ledgers/"

        request3 c =
            let
                _ =
                    Debug.log "Response2: " c
            in
            requestGet "http://openlibrary.org/people/george08/lists.json"

        request4 =
            requestGet "https://binaryjazz.us/wp-json/genrenator/v1/genre/"
    in
    -- 以前のリクエストの結果を使って、次のリクエストを行うにはTask.andThenを使う
    -- Task.sequenceはList (Task a)をTask (List a)にするもの。
    -- よって、このTaskは最終的に「request3の結果とrequest4の結果」を取得することになる
    -- どこかのTaskが失敗した時点で以降の処理が中断し、エラーが返される
    Task.sequence
        [ request1
            |> Task.andThen (\r -> request2 r)
            |> Task.andThen (\r -> request3 r)
        , request4
        ]

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        RequestSequential ->
            -- Task.attemptでTask.sequenceしたTaskを呼び出す
            ( { results = [] }, Task.attempt GotSequential requestSequential ) 

        GotSequential result ->
            -- GotSequentialは「最終的に成功したか失敗したか」の１回しか呼び出されない
            case result of
                Ok response ->
                    ( { model | results = response }, Cmd.none )

                Err e ->
                    -- とりあえず文字列にして結果の代わりに表示でもしておく
                    ( { model | results = [ e ] }, Cmd.none )


```

## Httpを並列に実行
こちらは超シンプルで、`Task.attempt`で得られた結果の`Cmd Msg`を`Cmd.batch`で呼び出せば良い。
`Cmd.batch`は複数の`Cmd Msg`を同時(並列)に実行する際に使われる。

```elm
-- 抜粋コード
requestParallel : Cmd Msg
requestParallel =
    let
        request1 =
            requestGet "https://dog.ceo/api/breeds/image/random"

        request2 =
            requestGet "https://data.ripple.com/v2/ledgers/"

        request3 =
            requestGet "http://openlibrary.org/people/george08/lists.json"

        request4 =
            requestGet "https://binaryjazz.us/wp-json/genrenator/v1/genre/"
    in
    -- 並列に実行する場合はCmd.batchを使う
    -- 並列に実行するので、以前のリクエストを使って次のリクエストを行うことはできない
    -- 必ず全てのリクエストの結果が必要な場合などに使うと良い
    Cmd.batch
        [ Task.attempt GotParallel <| request1
        , Task.attempt GotParallel <| request2
        , Task.attempt GotParallel <| request3
        , Task.attempt GotParallel <| request4
        ]

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        RequestParallel ->
            ( { results = [] }, requestParallel )

        GotParallel result ->
            -- GotParallelはリクエストが発生した回数分呼ばれる。なので、今回は４回呼ばれる。
            -- 複数の結果を受け取ることになるので、結果が得られるたびにmodelへ追加している
            case result of
                Ok response ->
                    ( { model | results = response :: model.results }, Cmd.none )

                Err e ->
                    ( { model | results = e :: model.results }, Cmd.none )

```

## あとがき
超平たく説明しているので詳細は、@ababup1192 さんの[ElmでHttpをわかってしまおう](https://qiita.com/ababup1192/items/b03fce202e1018bc4992)も合わせてご確認ください。