Resultの型を共通化してUpdate処理を軽量化する

ElmでAPIにリクエストを送るようなコードを書いていると以下のようなコードが度々登場するかと思います。

```elm
import Http exposing (Request, Error(..), request, emptyBody, expectString)


-- Http Request


sampleRequest : Request String
sampleRequest =
  Http.request
    { method = "GET"
    , headers = []
    , url = "https://httpbin.org"
    , body = emptyBody
    , expect = expectString
    , timeout = Nothing
    , withCredentials = False
    }


-- Model


type alias Model =
  { errorMessage: String
  , contents: String
  }


-- Update


type Msg
  = DoRequest
  | EndRequest (Result Http.Error String)


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    DoRequest ->
      model ! [ Http.send EndRequest sampleRequest ]

    EndRequest result ->
      case result of
        Ok value ->
          { model | contents = value } ! []

        Err err ->
          let
            errorMessage = err |> toMessage
          in
            { model | errorMessage = errorMessage } ! []


toMessage : Http.Error -> String
toMessage err =
  case err of
    BadUrl message ->
      message

    Timeout ->
      "Timeout"

    NetworkError ->
      "NetworkError"

    BadStatus response ->
      response.status.message

    BadPayload message response ->
      message
```

この時、`update`関数の`EndRequest`の中で、エラー時と成功時のModelを更新し新たなMsgを送っていますが各々に手続き的なコードが発生してしまい冗長である上にテストも書き難いです。

これを解決するために`Result`の結果を元にエラー時、成功時に返す型をコールバック関数を仲介して同じものにするヘルパーを用意してみます。

```elm
-- Helper

toFlat : (a -> b) -> (x -> b) -> Result x a -> b
toFlat okCallBack errCallback result =
  case result of
    Ok value ->
      okCallback value

    Err err ->
      errCallback err
```

これで`update`関数の`EndRequest`の処理は以下のように書くことができるようになります。

```elm
EndRequest result ->
  result
    |> toFlat
      (\value -> { model | contents = value })
      (\err -> { model | errorMessage = err |> toMessage })
    |> toTuple Cmd.none
    |> swap

-- Helper

toTuple : a -> b -> (a, b)
toTuple first second = (first, second)

swap : (a, b) -> (b, a)
swap t = (Tuple.second t, Tuple.first t)
```

`Result`の結果の型を共通にする関数は外に切り出すことでテストも容易に出来るようになります。[^1]

```elm
{-| imports for doc tests.

    >>> import Http exposing (Error(BadUrl))
-}


{-| update model of contents.

    >>> ((updateSuccess { contents = "", errorMessage = "" }) "result").contents
    "result"

    >>> ((updateSuccess { contents = "", errorMessage = "" }) "result").errorMessage
    ""
-}
updateSuccess : Model -> String -> Model
updateSuccess model =
  \value -> { model | contents = value }


{-| update model of errorMessage.

    >>> ((updateError { contents = "", errorMessage = "" }) (BadUrl "result")).contents
    ""

    >>> ((updateError { contents = "", errorMessage = "" }) (BadUrl "result")).errorMessage
    "result"
-}
updateError : Model -> Http.Error -> Model
updateError model =
  \err -> { model | errorMessage = err |> toMessage }
```

こうしておくと、`update`関数の`EndRequest`の処理はさらにシンプルに！

```elm
EndRequest result ->
  result
    |> toFlat (updateSuccess model) (updateError model)
    |> toTuple Cmd.none
    |> swap
```

これらのコードをあわせて、一通り動作するコードは下記のようになります。

```elm:Sample.elm
module Sample exposing (..)


{-| imports for doc tests.

    >>> import Http exposing (Error(BadUrl))
-}


import Html exposing (Html, div, p, button, text)
import Html.Attributes exposing (style)
import Html.Events exposing (onClick)
import Http exposing (Request, Error(..), request, emptyBody, expectString)


-- Http Request


sampleRequest : Request String
sampleRequest =
  Http.request
    { method = "GET"
    , headers = []
    , url = "https://httpbin.org"
    , body = emptyBody
    , expect = expectString
    , timeout = Nothing
    , withCredentials = False
    }


-- Model


type alias Model =
  { errorMessage: String
  , contents: String
  }


initModel : Model
initModel =
  { errorMessage = ""
  , contents = ""
  }


-- Update


type Msg
  = DoRequest
  | EndRequest (Result Http.Error String)


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    DoRequest ->
      model ! [ Http.send EndRequest sampleRequest ]

    EndRequest result ->
      result
        |> toFlat (updateSuccess model) (updateError model)
        |> toTuple Cmd.none
        |> swap


toMessage : Http.Error -> String
toMessage err =
  case err of
    BadUrl message ->
      message

    Timeout ->
      "Timeout"

    NetworkError ->
      "NetworkError"

    BadStatus response ->
      response.status.message

    BadPayload message response ->
      message


{-| update model of contents.

    >>> ((updateSuccess { contents = "", errorMessage = "" }) "result").contents
    "result"

    >>> ((updateSuccess { contents = "", errorMessage = "" }) "result").errorMessage
    ""
-}
updateSuccess : Model -> String -> Model
updateSuccess model =
  \value -> { model | contents = value }


{-| update model of errorMessage.

    >>> ((updateError { contents = "", errorMessage = "" }) (BadUrl "result")).contents
    ""

    >>> ((updateError { contents = "", errorMessage = "" }) (BadUrl "result")).errorMessage
    "result"
-}
updateError : Model -> Http.Error -> Model
updateError model =
  \err -> { model | errorMessage = err |> toMessage }


-- View


view : Model -> Html Msg
view model =
  div [ style [("margin", "10px")] ]
    [ p [ style [("color", "red")] ] [ text model.errorMessage ]
    , p [] [ text model.contents ]
    , div []
      [ button [ onClick DoRequest ] [ text "読み込み" ]
      ]
    ]


-- Main


main : Program Never Model Msg
main =
  Html.program
    { init = initModel ! []
    , update = update
    , subscriptions = always Sub.none
    , view = view
    }


-- Helper


toTuple : a -> b -> (a, b)
toTuple first second = (first, second)


swap : (a, b) -> (b, a)
swap t = (Tuple.second t, Tuple.first t)


toFlat : (a -> b) -> (x -> b) -> Result x a -> b
toFlat okCallback errCallback result =
  case result of
    Ok value ->
      okCallback value
      
    Err err ->
      errCallback err
```

Httpの結果が成功であれ、失敗であれやることはModelを更新して新しいメッセージを送るだけなので、こんな感じのヘルパーを用意しておくと`update`関数を簡潔に保てて良いのではないでしょうか？

## 同じことを実現出来るライブラリがありました

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">unpackじゃなくてもmapとmapErrorで<a href="https://t.co/J9z1S9ZXwl">https://t.co/J9z1S9ZXwl</a> &gt; Resultの型を共通化してUpdate処理を軽量化する by <a href="https://twitter.com/motoyan_k">@motoyan_k</a> on <a href="https://twitter.com/Qiita">@Qiita</a> <a href="https://t.co/dyboMo184I">https://t.co/dyboMo184I</a></p>&mdash; Miyamo (@miyamo_madoka) <a href="https://twitter.com/miyamo_madoka/status/874465074181586944">June 13, 2017</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

Resultの結果を同じ型にする機能は既に[result-extra](http://package.elm-lang.org/packages/elm-community/result-extra/2.2.0/Result-Extra#unpack)というライブラリで実現されているようです[^2]。

このライブラリを利用すると、下記のように書くことが出来ます。

```elm
import Result.Extra exposing (unpack)

EndRequest result ->
  result
    |> unpack (updateError model) (updateSuccess model)
    |> toTuple Cmd.none
    |> swap
```

---

[^1]: これは[elm-doc-test](https://github.com/stoeffel/elm-doc-test)を利用したテストコードになります。
[^2]: @miyamo_madoka ありがとうございます！