LoginSignup
15
7

More than 5 years have passed since last update.

ElmのHttpとJson.Decode、Taskの実践的な使い方

Last updated at Posted at 2017-12-29

 Elmの基礎的な勉強を2週間程度行ってきました。なんとなく感じがわかったところでもう少し実践的なコードを書いてみたいと思いました。Httpの使い方をもう少し掘り下げたいと思います。今回はRest APIを叩く簡単なクライアントを作成します。ちなみにここ数年人気の脳科学者(中野信子さん)によると、新しいことを学習してから3週間ぐらいで回路ができてくるみたいです。もう少しでElmの脳回路ができる予定です。頑張ります。

1.サーバ

 まずjson-serverをインストールします。

npm install -g json-server  

 json-serverはモック環境を提供してくれます。以下のようにdb.jsonファイルを作成します。

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

path1の表示結果
{
  "tags": [
    "猫",
    "犬",
    "クジラ",
    "ヤギ",
    "タカ"
  ]
}
path2の表示結果
{
  "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」はタグ一覧と記事一覧を表示します。

まずは初期画面です。タグ一覧も記事一覧も空です。
image.png

「Get Tags」をクリックするとタグ一覧のみ表示されます。
image.png

「Get Articles」をクリックすると記事一覧のみ表示されます。
image.png

「Get Tags and Articles」をクリックするとタグ一覧と記事一覧が表示されます。
image.png

2.Elmクライアントのコード

 以下がソースコードになります。ポイントはHttpの使い方です。Json.Decodeと一緒に使います。また1画面を描画するのに、2回RestAPIを叩く必要がある場合に、2回Cmdを発行するのではなく1回のCmdの発行で済ます方法を示しています。2つのRequestをTaskに変換してから、2個のTaskをmap2で1個のTaskに束ねればOKです。以下に詳細を説明します。

Main.elm
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
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

Http.get
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 の型になります。

Decoce.mapの型
map : (a -> value) -> Decoder a -> Decoder value

 次に記事一覧を取得するためのRequestを作成します。

requestArticles
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
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の関係がもやもやしていて、これも整理したいと思ったからです。とりあえずは現状の理解をメモした次第です。

15
7
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
15
7