LoginSignup
5
0

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-06-13

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

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の結果を元にエラー時、成功時に返す型をコールバック関数を仲介して同じものにするヘルパーを用意してみます。

-- 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の処理は以下のように書くことができるようになります。

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

{-| 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の処理はさらにシンプルに!

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

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

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関数を簡潔に保てて良いのではないでしょうか?

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

Resultの結果を同じ型にする機能は既にresult-extraというライブラリで実現されているようです2

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

import Result.Extra exposing (unpack)

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


  1. これはelm-doc-testを利用したテストコードになります。 

  2. @miyamo_madoka ありがとうございます! 

5
0
0

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
5
0