Elmの基礎的な勉強を2週間程度行ってきました。なんとなく感じがわかったところでもう少し実践的なコードを書いてみたいと思いました。Httpの使い方をもう少し掘り下げたいと思います。今回はRest APIを叩く簡単なクライアントを作成します。ちなみにここ数年人気の脳科学者(中野信子さん)によると、新しいことを学習してから3週間ぐらいで回路ができてくるみたいです。もう少しでElmの脳回路ができる予定です。頑張ります。
1.サーバ
まずjson-serverをインストールします。
npm install -g json-server
json-serverはモック環境を提供してくれます。以下のようにdb.jsonファイルを作成します。
{
"path1": { "tags": ["猫","犬","クジラ","ヤギ","タカ"] }
,"path2": { "articles":
[
{ "title": "12月29日 晴れ", "story": "今日は晴れでした。"}
,{ "title": "12月30日 曇り", "story": "今日は大掃除をしました。エアコンの掃除は大変でした。"}
,{ "title": "12月31日 晴れのち雨", "story": "今日は大晦日です。夜更かしします。"}
]
}
}
db.jsonがあるディレクトリで以下のようにしてサーバを立ち上げれば終了です。
json-server db.json --port 3090
ブラウザから以下のURLを叩くと、それぞれ下記のJSONデータが表示されます。
http://www.mypress.jp:3090/path1
http://www.mypress.jp:3090/path2
{
"tags": [
"猫",
"犬",
"クジラ",
"ヤギ",
"タカ"
]
}
{
"articles": [
{
"title": "12月29日 晴れ",
"story": "今日は晴れでした。"
},
{
"title": "12月30日 曇り",
"story": "今日は大掃除をしました。エアコンの掃除は大変でした。"
},
{
"title": "12月31日 晴れのち雨",
"story": "今日は大晦日です。夜更かしします。"
}
]
}
以上を確認して、準備完了です。
2.Elmクライアントの動作
Elmクライアントは、「Get Tags」と「Get Articles」、「Get Tags and Articles」という3個のボタンを表示します。「Get Tags」をクリックするとタグ一覧のみを表示します。「Get Articles」は記事一覧のみを表示します。「Get Tags and Articles」はタグ一覧と記事一覧を表示します。
「Get Tags」をクリックするとタグ一覧のみ表示されます。
「Get Articles」をクリックすると記事一覧のみ表示されます。
「Get Tags and Articles」をクリックするとタグ一覧と記事一覧が表示されます。
2.Elmクライアントのコード
以下がソースコードになります。ポイントはHttpの使い方です。Json.Decodeと一緒に使います。また1画面を描画するのに、2回RestAPIを叩く必要がある場合に、2回Cmdを発行するのではなく1回のCmdの発行で済ます方法を示しています。2つのRequestをTaskに変換してから、2個のTaskをmap2で1個のTaskに束ねればOKです。以下に詳細を説明します。
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Http
import Json.Decode as Decode
import Task exposing (Task)
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- MODEL
type Tag
= Tag String
type alias Article =
{ story : String
, title : String
}
type alias Model =
{ tags : List Tag
, articles : List Article
}
init : (Model, Cmd Msg)
init =
( Model [] []
, Cmd.none
)
-- UPDATE
type Msg
= GetTags
| GetArticles
| GetTagsArticles
| NewTags (Result Http.Error (List Tag))
| NewArticles (Result Http.Error (List Article))
| NewTagsArticles (Result Http.Error Model)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
GetTags ->
(model, getTags)
GetArticles ->
(model, getArticles)
GetTagsArticles ->
(model, getTagsArticles)
NewTags (Ok newtags) ->
( Model newtags [] , Cmd.none)
NewTags (Err _) ->
(model, Cmd.none)
NewArticles (Ok newarticles) ->
( Model [] newarticles , Cmd.none)
NewArticles (Err _) ->
(model, Cmd.none)
NewTagsArticles (Ok newmodel) ->
( newmodel , Cmd.none)
NewTagsArticles (Err _) ->
(model, Cmd.none)
-- VIEW
view : Model -> Html Msg
view model =
div []
[ h2 [] [ text "ボタン一覧" ]
, button [ onClick GetTags ] [ text "Get Tags" ]
, br [] []
, button [ onClick GetArticles ] [ text "Get Articles" ]
, br [] []
, button [ onClick GetTagsArticles ] [ text "Get Tags and Articles" ]
, br [] []
, h2 [] [ text "タグ一覧" ]
, ul [] (List.map viewTags model.tags)
, br [] []
, h2 [] [ text "記事一覧" ]
, ul [] (List.map viewArticles model.articles)
]
viewTags (Tag t) =
li [] [ text t ]
viewArticles a =
li []
[ h3 [] [ text a.title]
, p [] [ text a.story]
]
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- HTTP
url_tags =
"http://www.mypress.jp:3090/path1"
url_articles =
"http://www.mypress.jp:3090/path2"
requestTags : Http.Request (List Tag)
requestTags =
Http.get url_tags ( Decode.field "tags" ( Decode.list ( Decode.map Tag Decode.string ) ) )
requestArticles : Http.Request (List Article)
requestArticles =
Http.get url_articles ( Decode.field "articles" ( Decode.list article ) )
article : Decode.Decoder Article
article =
Decode.map2 toArticle (Decode.field "title" Decode.string) (Decode.field "story" Decode.string)
toArticle : String -> String -> Article
toArticle t s =
{ title=t, story=s }
getTags : Cmd Msg
getTags =
Http.send NewTags requestTags
getArticles : Cmd Msg
getArticles =
Http.send NewArticles requestArticles
getTagsArticles : Cmd Msg
getTagsArticles =
Task.attempt NewTagsArticles ( Task.map2 toModel ( Http.toTask (requestTags) ) ( Http.toTask (requestArticles) ) )
toModel : List Tag -> List Article -> Model
toModel t a =
{ tags=t, articles=a }
まずはJson.Decodeの使い方に集中しましょう。Json.DecodeはHttpで取得したJson文字列を、Elmの値に変換するための関数群です。
http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Json-Decode
以下はタグ一覧を取得するためのRequestを作成しています。
requestTags : Http.Request (List Tag)
requestTags =
Http.get url_tags ( Decode.field "tags" ( Decode.list ( Decode.map Tag Decode.string ) ) )
Http.getの型を確認しましょう。2番目の引数は Decoder a となっています。
http://package.elm-lang.org/packages/elm-lang/http/latest/Http
get : String -> Decoder a -> Request a
上で Decorder a は Decode.field "tags" ( Decode.list ( Decode.map Tag Decode.string ) ) ですね。これは前に示したように以下のJson文字列をDecodeするためのものです。
{
"tags": [
"猫",
"犬",
"クジラ",
"ヤギ",
"タカ"
]
}
Decode.field "tags" でJson配列 ["猫","犬","クジラ","ヤギ","タカ"] を Decoder であるDecode.list ( Decode.map Tag Decode.string ) で Decodeします。これはElmのリストである[Tag "猫", Tag "犬",Tag "クジラ",Tag "ヤギ",Tag "タカ"] になります。
以下が Decode.map の型になります。
map : (a -> value) -> Decoder a -> Decoder value
次に記事一覧を取得するためのRequestを作成します。
requestArticles : Http.Request (List Article)
requestArticles =
Http.get url_articles ( Decode.field "articles" ( Decode.list article ) )
article : Decode.Decoder Article
article =
Decode.map2 toArticle (Decode.field "title" Decode.string) (Decode.field "story" Decode.string)
toArticle : String -> String -> Article
toArticle t s =
{ title=t, story=s }
このRequestは以下のJson文字列をDecodeします。articleとtoArticleという2つのサブ関数を用いています。JavaScriptではJsonはネイティブなデータ構造なので、そのまま読み込んで使えますが、ElmではJsonをパースして値一つ一つをElm値に変え、Elmのデータ構造に再構築する必要があります。ちょっと面倒ですし、可読性が著しく低下します。JavaScriptなら簡単のに、と思わずつぶやいてしまいますね。
{
"articles": [
{
"title": "12月29日 晴れ",
"story": "今日は晴れでした。"
},
{
"title": "12月30日 曇り",
"story": "今日は大掃除をしました。エアコンの掃除は大変でした。"
},
{
"title": "12月31日 晴れのち雨",
"story": "今日は大晦日です。夜更かしします。"
}
]
}
最後にタグ一覧と記事一覧を同時に取得するためのCmdの作り方です。Http.getではRequestを作成しますが、これを Http.toTask でTaskに変換します。2つのTaskはTask.map2で1つのTaskに合成できます。これは最初のTaskが終了してから2番目のTaskが走ります。いずれかのTaskが失敗すれば全体が失敗します。以下がそのコードになります。
http://package.elm-lang.org/packages/elm-lang/core/latest/Task
getTagsArticles : Cmd Msg
getTagsArticles =
Task.attempt NewTagsArticles ( Task.map2 toModel ( Http.toTask (requestTags) ) ( Http.toTask (requestArticles) ) )
toModel : List Tag -> List Article -> Model
toModel t a =
{ tags=t, articles=a }
この記事は2つの動機から書きました。ひとつはJson.Decodeが面倒だな、自分の頭の中でもう一度整理したいなと思ったからです。2つ目は、RequestとTaskの関係がもやもやしていて、これも整理したいと思ったからです。とりあえずは現状の理解をメモした次第です。