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
これらのコードをあわせて、一通り動作するコードは下記のようになります。
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
関数を簡潔に保てて良いのではないでしょうか?
同じことを実現出来るライブラリがありました
unpackじゃなくてもmapとmapErrorでhttps://t.co/J9z1S9ZXwl > Resultの型を共通化してUpdate処理を軽量化する by @motoyan_k on @Qiita https://t.co/dyboMo184I
— Miyamo (@miyamo_madoka) June 13, 2017
Resultの結果を同じ型にする機能は既にresult-extraというライブラリで実現されているようです2。
このライブラリを利用すると、下記のように書くことが出来ます。
import Result.Extra exposing (unpack)
EndRequest result ->
result
|> unpack (updateError model) (updateSuccess model)
|> toTuple Cmd.none
|> swap
-
これはelm-doc-testを利用したテストコードになります。 ↩
-
@miyamo_madoka ありがとうございます! ↩