最近Elmに関する記事を連続して書いています。それはフロントエンドのプログラムもHaskellのようなすっきりとした文法で書ければいいなと思うからです。開発効率も上げたいですし。そして現状どこまで書くことが可能なのかを知りたいという動機からです。React無しでやれるのか?それとも既存のJavaScriptと併用するのが現実的なのか、どうやって?
今までの感触だと、Model-View-Updateループは素晴らしくシンプルに抽象化されていて、学習コストが低く、その範囲内のものであれば、開発効率はすごく高くなる気がします。その範囲というのは、ユーザINPUTのイベント処理をupdateで書いて、その時生じたModelの変更を、viewで画面に反映するというものです。この範囲で済むプログラムならほとんどの仕事をElm Runtimeが自動的に行ってくれるので楽ちんです。乱数を扱うときは、Cmdを使いますが、これもまあわかってしまえば簡単です。それではHttp通信はどうでしょう?今回のテーマです。
今回はHttpについてです。例によって公式サイトを題材にしていきたいと思います。
https://guide.elm-lang.org/architecture/effects/http.html
またElm のCmdアーキテクチャの詳細については以下の記事を参照してください。今回はHttpに焦点を当てたいと思います。
https://qiita.com/sand/items/4efd8eeafd2c33778e08
-- Read more about this program in the official Elm guide:
-- https://guide.elm-lang.org/architecture/effects/http.html
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Http
import Json.Decode as Decode
main =
Html.program
{ init = init "cats"
, view = view
, update = update
, subscriptions = subscriptions
}
-- MODEL
type alias Model =
{ topic : String
, gifUrl : String
}
init : String -> (Model, Cmd Msg)
init topic =
( Model topic "waiting.gif"
, getRandomGif topic
)
-- UPDATE
type Msg
= MorePlease
| NewGif (Result Http.Error String)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
MorePlease ->
(model, getRandomGif model.topic)
NewGif (Ok newUrl) ->
(Model model.topic newUrl, Cmd.none)
NewGif (Err _) ->
(model, Cmd.none)
-- VIEW
view : Model -> Html Msg
view model =
div []
[ h2 [] [text model.topic]
, button [ onClick MorePlease ] [ text "More Please!" ]
, br [] []
, img [src model.gifUrl] []
]
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- HTTP
getRandomGif : String -> Cmd Msg
getRandomGif topic =
let
url =
"https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=" ++ topic
in
Http.send NewGif (Http.get url decodeGifUrl)
decodeGifUrl : Decode.Decoder String
decodeGifUrl =
Decode.at ["data", "image_url"] Decode.string
このプログラムは、猫の画像を表示するものです。ボタンを押してRequestを発行するたびに、ランダムな猫画像のURLが返され(Result)てきます。
以下に詳細を見ていきます。まずmainですが、これは乱数の時と同じです。
https://qiita.com/sand/items/4efd8eeafd2c33778e08
main = Html.program { init = init "cats", view = view, update = update, subscriptions = subscriptions}
次にinitですが、画像urlが"waiting.gif"という存在しないurlなので一瞬表示が崩れます。しかしコマンドgetRandomGif topicが発行され正しいurlが返ってくるので表示は最初の猫画像を示します。
init : String -> (Model, Cmd Msg)
init topic =
( Model topic "waiting.gif"
, getRandomGif topic
)
次にMsgの型ですが、MorePleaseがボタンを押したときのupdateに使われ、NewGifがサーバからurlをgetするというコマンドの処理が終わったときのupdateに使われます。
type Msg
= MorePlease
| NewGif (Result Http.Error String)
Resultは一般的に以下のように定義されたElmの型です。失敗か成功かの値を表現できます。この場合、errorとしてはHttp.Error型が、ValueとしてはString型が期待されています。
http://package.elm-lang.org/packages/elm-lang/core/latest/Result
type Result error value
= Ok value
| Err error
updateは形としては乱数の時と同じです。ボタンクリック時のコマンド(getRandomGif model.topic)が違うのと、コマンドの返却値としてerrorのパタンが追加されたのが異なります。
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
MorePlease ->
(model, getRandomGif model.topic)
NewGif (Ok newUrl) ->
(Model model.topic newUrl, Cmd.none)
NewGif (Err _) ->
(model, Cmd.none)
さてgetRandomGifこそが、サーバとHttp通信するためのコマンドを生み出す関数です。
getRandomGif : String -> Cmd Msg
getRandomGif topic =
let
url =
"https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=" ++ topic
in
Http.send NewGif (Http.get url decodeGifUrl)
特に以下の行に注目します。
Http.send NewGif (Http.get url decodeGifUrl)
この一行は一見複雑に関数を組み合わせていますが、要するにCmdを創り出す関数を呼んでいます。以下の型宣言が理解を厳密なものとしてくれます。
http://package.elm-lang.org/packages/elm-lang/http/latest/Http
Msg型のコンストラクタNewGifは以下のような関数とみることができます。
NewGif : Result Error value -> msg
Http.getは定義により以下の型になります。
Http.get : String -> Decode.Decoder value -> Http.Request value
decodeGifUrl : Decode.Decoder String
上の3つの型がちょうど以下のHttp.sendの型に当てはまります。値としてCmd msgを得ることができます。
Http.send : (Result Error value -> msg) -> Http.Request value -> Cmd msg
さてここでわからないのがサーバ側がReturnしてくれる実際の値です。modelのgifUrlは都度変更されますが、model.topicはcatsのままで変更されないことに注意しましょう。以下のリクエストをブラウザから直接発行してみます。
https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=cats
以下がその時にReturnされてくるjsonです。
{
"data": {
"caption": "",
"fixed_height_downsampled_height": "200",
"fixed_height_downsampled_url": "https://media1.giphy.com/media/l41lTEXcm2fWlcidy/200_d.gif",
"fixed_height_downsampled_width": "299",
"fixed_height_small_height": "100",
"fixed_height_small_still_url": "https://media1.giphy.com/media/l41lTEXcm2fWlcidy/100_s.gif",
"fixed_height_small_url": "https://media1.giphy.com/media/l41lTEXcm2fWlcidy/100.gif",
"fixed_height_small_width": "150",
"fixed_width_downsampled_height": "134",
"fixed_width_downsampled_url": "https://media1.giphy.com/media/l41lTEXcm2fWlcidy/200w_d.gif",
"fixed_width_downsampled_width": "200",
"fixed_width_small_height": "67",
"fixed_width_small_still_url": "https://media1.giphy.com/media/l41lTEXcm2fWlcidy/100w_s.gif",
"fixed_width_small_url": "https://media1.giphy.com/media/l41lTEXcm2fWlcidy/100w.gif",
"fixed_width_small_width": "100",
"id": "l41lTEXcm2fWlcidy",
"image_frames": "40",
"image_height": "214",
"image_mp4_url": "https://media1.giphy.com/media/l41lTEXcm2fWlcidy/giphy.mp4",
"image_original_url": "https://media1.giphy.com/media/l41lTEXcm2fWlcidy/giphy.gif",
"image_url": "https://media1.giphy.com/media/l41lTEXcm2fWlcidy/giphy.gif",
"image_width": "320",
"type": "gif",
"url": "http://giphy.com/gifs/afvpets-cats-pets-afv-l41lTEXcm2fWlcidy",
"username": "afvpets"
},
"meta": {
"msg": "OK",
"response_id": "5a345ffb47626a30733460a4",
"status": 200
}
}
さて最後になりますが、このJson値はelmオブジェクトに変換され、decodeGifUrl で望む項目の値にアクセスします。つまりdata階層のimage_urlの値をgetします。
https://guide.elm-lang.org/interop/json.html
decodeGifUrl : Decode.Decoder String
decodeGifUrl =
Decode.at ["data", "image_url"] Decode.string
今回のHttp通信は前回の乱数と同じようにCmdを通してElm Runtimeに仕事を依頼するものでした。ElmのHttp moduleにはhttp通信のためのAPIが一式そろっています。このCmdアーキテクチャの枠組みで、Emlでも安心してサーバとの通信が行えそうです。