ElmでHttpをわかってしまおう

Elmの入門者が増えHttpを扱う需要が増えてきたように感じます。Elmの[Httpモジュール](https://package.elm-lang.org/packages/elm/http/latest/Http)は2.0.0にバージョンが上がり1.0.0のコードは参考にしにくく、そもそもHttpを扱うには[Task](https://package.elm-lang.org/packages/elm/core/latest/Task)と呼ばれる非同期処理を扱う仕組みを理解しないといけなく少々敷居が高くなります。Taskは[公式ガイド](https://guide.elm-lang.jp)には触れる程度しか書かれていないため、これも障壁となるため かいつまんで説明出来ればなと思います。

# 課題(前提)

今回扱うAPIは以下のような仕様とします。

- `/test` というルーティング
- クエリで `page` には数字を渡すことができます。ページャを表す
- 1-3ページまでがアクセス可能
- それ以外のページはNot Found
- レスポンスとして、result(string)とisNext(bool)を返す
- resultが今回欲しい文字列
- isNextは次のページが存在すればtrueを返す

Webpack起動時にExpressで書かれたサーバを仕込んでいます。

```javascript
app.get("/test", function(req, res) {
    const pageNum = Number(req.query.page);
    const maxPageNum = 3

    if(pageNum < 1 || maxPageNum < pageNum) {
        res.status(404).send('Page Not found');
    } else {
        res.json({
            result: `page=${pageNum}`, 
            isNext: pageNum < maxPageNum 
        });
    }
});
```

今回欲しい結果は、1-3ページのresultをカンマ区切りにした文字列、
`page=1, page=2, page=3`
とします。

# 実装

## Taskを利用しない(Msg再帰)パターン

まずはTaskを利用せずにCmdで頑張ってみた例を紹介します。CmdでAPIを叩き、Msgで受け取りModelを更新します。そして、その中身を見て再びCmdでAPIを叩くか終了するかを判断します。つまり、**update関数の再帰**をするという考え方になります。

まずはModelとデータ構造の定義です。APIを叩いた中身を溜め続ける`resultChunk`と現在の見ているページ`currentPage`です。Modelは、空のリストと1ページ目で初期化しておきます。APIのレスポンスの型を先程のExpressのAPIと合わせます。

```elm
type alias Model =
    { resultChunk : List String
    , currentPage : Int
    }


init : () -> ( Model, Cmd Msg )
init _ =
    ( { resultChunk = [], currentPage = 1 }, Cmd.none )


type alias Response =
    { result : String, isNext : Bool }
```

ここが本命です！`KickTestServer`でAPIを叩き始めます。後述する`getTestServerResponseWithPage`関数にページ数(1ページ)を渡して呼び出します。このコマンドは、`GotServerResponse`でレスポンスを受け取りパターンマッチでResult型の分岐をします。Okのとき`isNext`を確認してTrueであれば、`resultChunk`に`result`を積んで`currentPage`ページをインクリメントした後に再びAPIを叩きます(GotServerResponseの再帰)。`isNext`がFalseのとき`Cmd.none`つまり再帰を停止します。

```elm
type Msg
    = KickTestServer
    | GotServerResponse (Result Http.Error Response)


update : Msg -> Model -> ( Model, Cmd Msg )
update msg ({ resultChunk, currentPage } as model) =
    case msg of
        KickTestServer ->
            ( { model | resultChunk = [], currentPage = 1 }, getTestServerResponseWithPage 1 )

        GotServerResponse res ->
            case res of
                Ok { result, isNext } ->
                    if isNext then
                        ( { model | resultChunk = result :: resultChunk, currentPage = currentPage + 1 }, getTestServerResponseWithPage currentPage )

                    else
                        ( model, Cmd.none )

                Err err ->
                    ( { model | resultChunk = [ "Error: " ++ httpErrorToString err ] }, Cmd.none )

```

[Http.get](https://package.elm-lang.org/packages/elm/http/latest/Http#get)はCmdを返します。

```elm 
getTestServerResponseWithPage : Int -> Cmd Msg
getTestServerResponseWithPage pageNum =
    let
        expect =
            Http.expectJson GotServerResponse responseDecoder
    in
    Http.get { url = "/test?page=" ++ String.fromInt pageNum, expect = expect }

responseDecoder : Decode.Decoder Response
responseDecoder =
    Decode.map2 Response
        (Decode.field "result" Decode.string)
        (Decode.field "isNext" Decode.bool)

httpErrorToString : Http.Error -> String
httpErrorToString err =
    case err of
        BadUrl _ ->
            "BadUrl"

        Timeout ->
            "Timeout"

        NetworkError ->
            "NetworkError"

        BadStatus _ ->
            "BadStatus"

        BadBody s ->
            "BadBody: " ++ s


view : Model -> Html Msg
view { resultChunk } =
    div [ class "container" ]
        [ button [ onClick <| KickTestServer ] [ text "getPages" ]
        , p [] [ text <| String.join ", " (resultChunk |> List.reverse) ]
        ]
```

## Taskを利用するパターン

Taskを利用するパターンです。先ほどとの違いはupdateの再帰は行わず、Task自身で最終的に欲しい形を組み立てます。そのためGotServcerResponseの成功時の型が`Reponse`から`List Stringとなります`。さらにTask自身で再帰をするためModelからは`currentPage`が必要無くなります。TaskをCmdにするには、[Task.attempt](https://package.elm-lang.org/packages/elm/core/latest/Task#attempt)関数を使います。

```elm
type Msg
    = KickTestServer
    | GotServerResponse (Result Http.Error (List String))


update : Msg -> Model -> ( Model, Cmd Msg )
update msg ({ resultChunk, currentPage } as model) =
    case msg of
        KickTestServer ->
            ( model, Task.attempt GotServerResponse <| chunkResultTask 1 [] )

        GotServerResponse res ->
            case res of
                Ok rc ->
                    ( { model | resultChunk = rc }, Cmd.none )

                Err err ->
                    ( { model | resultChunk = [ "Error: " ++ httpErrorToString err ] }, Cmd.none )
```

[Http.Task](https://package.elm-lang.org/packages/elm/http/latest/Http#task)を利用するとHttpの戻り値をTaskにすることができますが、Http.getなどと比べるとローレイヤの仕組みなので少々面倒なパラメータを設定します。特にHttpのJSON DecoderとResponseを処理するResolverは厄介です。jsonResolverがライブラリには定義されていないためライブラリの[resolve](https://github.com/elm/http/blob/2.0.0/src/Http.elm#L514
)関数を参考にして組み立てます。これはよく使うコードなのでResolverは、いつでも参照できるようにしておくと良いかも知れません。


```elm
getTestServerResponseWithPageTask : Int -> Task Http.Error Response
getTestServerResponseWithPageTask pageNum =
    Http.task
        { method = "GET"
        , headers = []
        , url = "/test?page=" ++ String.fromInt pageNum
        , body = Http.emptyBody
        , resolver = jsonResolver responseDecoder
        , timeout = Nothing
        }

jsonResolver : Decode.Decoder a -> Http.Resolver Http.Error a
jsonResolver decoder =
    Http.stringResolver <|
        \response ->
            case response of
                Http.BadUrl_ url ->
                    Err (Http.BadUrl url)

                Http.Timeout_ ->
                    Err Http.Timeout

                Http.NetworkError_ ->
                    Err Http.NetworkError

                Http.BadStatus_ metadata body ->
                    Err (Http.BadStatus metadata.statusCode)

                Http.GoodStatus_ metadata body ->
                    case Decode.decodeString decoder body of
                        Ok value ->
                            Ok value

                        Err err ->
                            Err (Http.BadBody (Decode.errorToString err))
```

最後にTaskの再帰方法です。単に受け取った結果を元に再びAPIを叩くと、`Task (Task Response)`のような感じでTaskがネストしてしまいます。それをフラットにする関数が[Task.andThen](https://package.elm-lang.org/packages/elm/core/latest/Task#andThen)になります。再帰方法はCmdを利用したときと同じ形になります。最終結果は固定値を[Task.succeed](https://package.elm-lang.org/packages/elm/core/latest/Task#succeed)で返します。

```elm 
chunkResultTask : Int -> List String -> Task Http.Error (List String)
chunkResultTask currentPage resultChunk =
    getTestServerResponseWithPageTask currentPage
        |> Task.andThen
            (\{ result, isNext } ->
                if isNext then
                    chunkResultTask (currentPage + 1) (result :: resultChunk)

                else
                    Task.succeed (result :: resultChunk)
            )
```

## ページ数があらかじめ わかっているとき

最後は取得すべきページ数があらかじめわかっているときです。1-3の数をList.mapで`Task Reponse`にします。欲しいのはその中のresultなので、`Task.map .result`で`List (Task x a)`という型の値にします。実際欲しいのはTaskのListではなく、`Task x (List String)`の形にします。これはよくある変換なので、[Task.sequence](https://package.elm-lang.org/packages/elm/core/latest/Task#sequence)という変換の関数が用意されています。

```elm
KickTestServer ->
            let
                getResultTask =
                    getTestServerResponseWithPageTask >> Task.map .result
            in
            ( model
            , Task.attempt GotServerResponse <| (List.range 1 3 |> List.map getResultTask |> Task.sequence)
            )
```

## ソースコード

ソースコードは、[こちら](https://github.com/ababup1192/elm-http-example)

# まとめ

連続でAPIを叩きたい場合、updateの再帰を利用することで実現することを学びました。しかしその場合は、Modelで一時的な変数を持つ必要があったり再帰のコードがupdateに混ざり複雑になりがちです。Taskを利用することでTaskの中の再帰として解決することができupdateの中身がシンプルになります。つまりこれは、`Cmd`と比べてTaskは変換や合成などの加工がしやすい非同期処理を扱う型だとあることがわかります。これらの道具を使いこなすと一気にElmの世界が楽しくなるので是非マスターしてみましょう！