Подтвердить что ты не робот

Как извлечь результаты запросов Http в Elm

Используя пакет Elm html, вы можете сделать http-запросы:

https://api.github.com/users/nytimes/repos

Это все Нью-Йорк Таймс на Github. В основном есть два элемента, которые я хочу получить от ответа Github: id и имя

[ { "id": 5803599,  "name": "backbone.stickit"  , ... }, 
  { "id": 21172032, "name": "collectd-rabbitmq" , ... }, 
  { "id": 698445,   "name": "document-viewer"   , ... }, ... ]

Тип Elm для Http.get требует объекта Json Decoder

> Http.get
<function> : Json.Decode.Decoder a -> String -> Task.Task Http.Error a

Я еще не знаю, как открыть списки. Поэтому я поместил декодер Json.Decode.string и, по крайней мере, подобрал типы, но я понятия не имел, что делать с объектом task.

> tsk = Http.get (Json.Decode.list Json.Decode.string) url
{ tag = "AndThen", task = { tag = "Catch", task = { tag = "Async", asyncFunction = <function> }, callback = <function> }, callback = <function> }
    : Task.Task Http.Error (List String)

> Task.toResult tsk
{ tag = "Catch", task = { tag = "AndThen", task = { tag = "AndThen", task = { tag = "Catch", task = { tag = "Async", asyncFunction = <function> }, callback = <function> }, callback = <function> }, callback = <function> }, callback = <function> }
    : Task.Task a (Result.Result Http.Error (List String))

Мне просто нужен объект Elm имен репо, поэтому я могу отображать некоторые элементы div, но я даже не могу получить данные.


Может ли кто-то медленно пройти меня, как написать декодер и как получить данные с помощью Elm?

4b9b3361

Ответ 1

Обновление для Elm 0.17:

Я обновил суть этого ответа для работы с Elm 0.17. Вы можете увидеть полный исходный код здесь. Он будет работать на http://elm-lang.org/try.

В версии 0.17 было внесено несколько изменений в язык и API, что делает некоторые из следующих рекомендаций устаревшими. Вы можете прочитать о плане обновления 0.17 здесь.

Я оставлю исходный ответ для 0.16, нетронутого ниже, но вы можете сравнить финальные значения, чтобы просмотреть список изменений. Я считаю, что более новая версия 0.17 более понятна и понятна.

Оригинальный ответ для Elm 0.16:

Похоже, вы используете Elm REPL. Как указано здесь, вы не сможете выполнять задачи в REPL. Мы побольше узнаем, почему. Вместо этого создайте реальный проект Elm.

Я предполагаю, что вы загрузили стандартные инструменты Elm.

Сначала вам нужно создать папку проекта и открыть ее в терминале.

Обычный способ начать работу с проектом Elm - использовать StartApp. Позвольте использовать это как отправную точку. Сначала вы должны использовать инструмент командной строки для менеджера Elm для установки необходимых пакетов. Выполните следующее в терминале в корне проекта:

elm package install -y evancz/elm-html
elm package install -y evancz/elm-effects
elm package install -y evancz/elm-http
elm package install -y evancz/start-app

Теперь создайте файл в корне проекта с именем Main.elm. Вот вам какой-то шаблонный код StartApp, чтобы вы начали. Я не буду вдаваться в подробности здесь, потому что этот вопрос конкретно посвящен Задачам. Вы можете узнать больше, пройдя Elm Architecture Tutorial. На данный момент скопируйте это в Main.elm.

import Html exposing (..)
import Html.Events exposing (..)
import Html.Attributes exposing (..)
import Html.Attributes exposing (..)
import Http
import StartApp
import Task exposing (Task)
import Effects exposing (Effects, Never)
import Json.Decode as Json exposing ((:=))

type Action
  = NoOp

type alias Model =
  { message : String }

app = StartApp.start
  { init = init
  , update = update
  , view = view
  , inputs = [ ]
  }

main = app.html

port tasks : Signal (Task.Task Effects.Never ())
port tasks = app.tasks

init =
  ({ message = "Hello, Elm!" }, Effects.none)

update action model =
  case action of
    NoOp ->
      (model, Effects.none)

view : Signal.Address Action -> Model -> Html
view address model =
  div []
    [ div [] [ text model.message ]
    ]

Теперь вы можете запустить этот код, используя elm-реактор. Перейдите в терминал в папке проекта и введите

elm reactor

Это приведет к запуску веб-сервера на порту 8000 по умолчанию, и вы можете поднять http://localhost:8000 в своем браузере, а затем перейти к Main.elm, чтобы увидеть Пример "Привет, Elm".

Конечной целью здесь является создание кнопки, которая при щелчке вытаскивает список репозиториев nytimes и перечисляет идентификаторы и имена каждого из них. Пусть сначала создайте эту кнопку. Мы сделаем это, используя стандартные функции генерации html. Обновите функцию view примерно следующим образом:

view address model =
  div []
    [ div [] [ text model.message ]
    , button [] [ text "Click to load nytimes repositories" ]
    ]

Сам по себе кнопка не делает ничего. Нам нужно создать действие, которое затем обрабатывается функцией update. Действие, инициируемое кнопкой, заключается в извлечении данных из конечной точки Github. Action теперь становится:

type Action
  = NoOp
  | FetchData

И теперь мы можем закончить обработку этого действия в функции update, как это. На данный момент позвольте изменить сообщение, чтобы показать, что нажата кнопка:

update action model =
  case action of
    NoOp ->
      (model, Effects.none)
    FetchData ->
      ({ model | message = "Initiating data fetch!" }, Effects.none)

Наконец, мы должны вызвать нажатия кнопок для запуска этого нового действия. Это делается с помощью функции onClick, которая генерирует обработчик события щелчка для этой кнопки. Строка генерации кнопок html теперь выглядит следующим образом:

button [ onClick address FetchData ] [ text "Click to load nytimes repositories" ]

Отлично! Теперь сообщение должно быть обновлено при нажатии на него. Перейдите в Задачи.

Как я упоминал ранее, REPL еще не поддерживает вызов задач. Это может показаться противоречивым, если вы исходите из императивного языка, такого как Javascript, где, когда вы пишете код, который говорит "go fetch data from this url", он сразу же создает HTTP-запрос. В чисто функциональном языке, таком как Elm, вы делаете что-то совсем по-другому. Когда вы создаете задачу в Elm, вы действительно просто указываете свои намерения, создавая своего рода "пакет", который вы можете передать во время выполнения, чтобы сделать что-то, что вызывает побочные эффекты; в этом случае обратитесь в внешний мир и удалите данные из URL.

Отпустите и создайте задачу, которая извлекает данные из URL-адреса. Во-первых, нам понадобится тип внутри Elm, чтобы представить форму данных, о которых мы заботимся. Вы указали, что вам просто нужны поля id и name.

type alias RepoInfo =
  { id : Int
  , name : String
  }

В качестве примечания о построении типа внутри Elm, остановитесь на минуту и ​​поговорите о том, как мы создаем экземпляры RepoInfo. Поскольку существует два поля, вы можете построить RepoInfo одним из двух способов. Следующие два утверждения эквивалентны:

-- This creates a record using record syntax construction
{ id = 123, name = "example" }

-- This creates an equivalent record using RepoInfo as a constructor with two args
RepoInfo 123 "example"

Вторая причина создания экземпляра станет более важной, когда мы поговорим о декодировании Json.

Позвольте также добавить их в модель. Мы также должны изменить функцию init, чтобы начать с пустого списка.

type alias Model =
  { message : String
  , repos : List RepoInfo
  }

init =
  let
    model =
      { message = "Hello, Elm!"
      , repos = []
      }
  in
    (model, Effects.none)

Поскольку данные из URL-адреса возвращаются в формате JSON, нам понадобится Json Decoder для перевода исходного JSON в наш класс Elm, безопасный для типов. Создайте следующий декодер.

repoInfoDecoder : Json.Decoder RepoInfo
repoInfoDecoder =
  Json.object2
    RepoInfo
    ("id" := Json.int) 
    ("name" := Json.string) 

Отбросьте это отдельно. Декодер - это то, что отображает необработанный JSON в форму типа, к которому мы привязаны. В этом случае наш тип - простой псевдоним записи с двумя полями. Помните, что я упомянул несколько шагов назад, что мы можем создать экземпляр RepoInfo, используя RepoInfo как функцию, которая принимает два параметра? Вот почему мы используем Json.object2 для создания декодера. Первый аргумент object - это функция, которая сама берет два аргумента и поэтому мы проходим в RepoInfo. Это эквивалентно функции с arity two.

Остальные аргументы описывают форму типа. Поскольку наша модель RepoInfo перечисляет id first и name second, то порядок, в котором декодер ожидает аргументы.

Нам понадобится еще один декодер для декодирования списка экземпляров RepoInfo.

repoInfoListDecoder : Json.Decoder (List RepoInfo)
repoInfoListDecoder =
  Json.list repoInfoDecoder

Теперь, когда у нас есть модель и декодер, мы можем создать функцию, возвращающую задачу для извлечения данных. Помните, что на самом деле это не выборка данных, а просто создание функции, которую мы можем передать позже.

fetchData : Task Http.Error (List RepoInfo)
fetchData =
  Http.get repoInfoListDecoder "https://api.github.com/users/nytimes/repos"

Существует несколько способов обработки множества ошибок, которые могут возникнуть. Позвольте выбрать Task.toResult, который отображает результат запроса в тип Result. Это немного облегчит нам задачу, и этого достаточно для этого примера. Позвольте изменить, что fetchData подпись:

fetchData : Task x (Result Http.Error (List RepoInfo))
fetchData =
  Http.get repoInfoListDecoder "https://api.github.com/users/nytimes/repos"
    |> Task.toResult

Обратите внимание, что я использую x в аннотации типа для значения ошибки задачи. Это просто потому, что при сопоставлении с Result мне никогда не придется заботиться об ошибке из задачи.

Теперь нам понадобятся некоторые действия для обработки двух возможных результатов: ошибка HTTP или успешный результат. Обновите Action следующим образом:

type Action
  = NoOp
  | FetchData
  | ErrorOccurred String
  | DataFetched (List RepoInfo)

Теперь ваша функция обновления должна установить эти значения в модели.

update action model =
  case action of
    NoOp ->
      (model, Effects.none)
    FetchData ->
      ({ model | message = "Initiating data fetch!" }, Effects.none)
    ErrorOccurred errorMessage ->
      ({ model | message = "Oops! An error occurred: " ++ errorMessage }, Effects.none)
    DataFetched repos ->
      ({ model | repos = repos, message = "The data has been fetched!" }, Effects.none)

Теперь нам нужен способ сопоставить задачу Result с одним из этих новых действий. Поскольку я не хочу увязнуть в обработке ошибок, я просто собираюсь использовать toString для изменения объекта ошибки в строку для целей отладки

httpResultToAction : Result Http.Error (List RepoInfo) -> Action
httpResultToAction result =
  case result of
    Ok repos ->
      DataFetched repos
    Err err ->
      ErrorOccurred (toString err)

Это дает нам способ сопоставить бесконечную задачу с действием. Однако StartApp имеет дело с эффектами, которые являются тонким слоем над задачами (а также несколькими другими вещами). Нам понадобится еще одна часть, прежде чем мы сможем связать все это вместе, и это способ сопоставить никогда не выполняющуюся HTTP-задачу с эффектами нашего действия типа.

fetchDataAsEffects : Effects Action
fetchDataAsEffects =
  fetchData
    |> Task.map httpResultToAction
    |> Effects.task

Возможно, вы заметили, что я назвал это, "никогда не терпеть неудачу". Сначала это меня сбивало с толку, поэтому позвольте мне попытаться объяснить. Когда мы создаем задачу, мы гарантируем результат, но это успех или неудача. Чтобы сделать приложения Elm максимально надежными, мы по существу исключаем возможность сбоя (главным образом, это означает, что это необработанное исключение Javascript), явно обрабатывая каждый случай. Вот почему мы столкнулись с проблемой сопоставления сначала с Result, а затем с нашим Action, который явно обрабатывает сообщения об ошибках. Сказать, что это никогда не терпит неудачу, не означает, что HTTP-проблемы не могут произойти, это означает, что мы обрабатываем каждый возможный результат, а ошибки сопоставляются с "успехами", сопоставляя их с действительным действием.

До нашего последнего шага, давайте удостовериться, что наш view может отображать список репозиториев.

view : Signal.Address Action -> Model -> Html
view address model =
  let
    showRepo repo =
      li []
        [ text ("Repository ID: " ++ (toString repo.id) ++ "; ")
        , text ("Repository Name: " ++ repo.name)
        ]
  in
    div []
      [ div [] [ text model.message ]
      , button [ onClick address FetchData ] [ text "Click to load nytimes repositories" ]
      , ul [] (List.map showRepo model.repos)
      ]

Наконец, часть, которая связывает это все вместе, должна сделать случай fetchData нашей update функции возвратом Эффекта, который инициирует нашу задачу. Обновите оператор case следующим образом:

FetchData ->
  ({ model | message = "Initiating data fetch!" }, fetchDataAsEffects)

Что это! Теперь вы можете запустить elm reactor и нажать кнопку, чтобы получить список репозиториев. Если вы хотите протестировать обработку ошибок, вы можете просто изменить URL-адрес для запроса Http.get, чтобы узнать, что произойдет.

Я опубликовал весь рабочий пример этого как сущность. Если вы не хотите запускать его локально, вы можете увидеть конечный результат, вставив этот код в http://elm-lang.org/try.

Я старался быть очень ясным и кратким в каждом шаге на этом пути. В типичном приложении Elm многие из этих шагов будут сведены к нескольким строкам, и будет использоваться более идиоматическая стенография. Я попытался сэкономить вам эти препятствия, сделав все как можно меньше и ясным. Надеюсь, это поможет!