LoginSignup
3
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-05

ソースコードとサンプル

サンプル
ソースコード
ページ遷移入ってますが、ほぼそのままMain.elmとかに使えるはず。

準備

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

HttpでのTask利用

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

  • Http.task
  • Resolver
  • Json.Decode 1

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

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.performTask.attemptを使う
  • Task.performは必ず成功するTaskに使う
  • Task.attemptは失敗するかもしれないTaskに使う(Httpを使うケースはこちら)

詳細な説明は、@ababup1192 さんのElmのTaskにこんにちはを参照してください。

Httpを逐次的(シーケンシャル)に実行

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

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

-- 抜粋コード
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は「最終的に成功したか失敗したか」の1回しか呼び出されない
            case result of
                Ok response ->
                    ( { model | results = response }, Cmd.none )

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


Httpを並列に実行

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

-- 抜粋コード
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はリクエストが発生した回数分呼ばれる。なので、今回は4回呼ばれる。
            -- 複数の結果を受け取ることになるので、結果が得られるたびに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をわかってしまおうも合わせてご確認ください。


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

3
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0