如何在 Elm 中提取 Http Requests 的结果

How to extract the results of Http Requests in Elm

使用 Elm 的 html 包可以发出 http 请求:

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

这些是 Github 上的所有 New York Times 回购。基本上我想从 Github 响应中得到两个项目,idname

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

Http.get 的 Elm 类型需要 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 获取数据吗?

Elm 0.17 更新:

我已经更新了这个答案的完整要点以适用于 Elm 0.17。你可以 see the full source code here. It will run on http://elm-lang.org/try.

0.17 中进行了一些语言和 API 更改,使得以下一些建议已过时。你可以 read about the 0.17 upgrade plan here.

我将在下面保留 0.16 的原始答案不变,但您可以 compare the final gists to see a list of what has changed。我相信较新的 0.17 版本更清晰,更容易理解。

Elm 0.16 的原始答案:

您似乎在使用 Elm REPL。 As noted here,您将无法在 REPL 中执行任务。我们稍后会详细介绍 为什么 。相反,让我们创建一个实际的 Elm 项目。

我假设您已经下载了标准 Elm tools

您首先需要创建一个项目文件夹并在终端中打开它。

开始 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-reactor 运行 此代码。转到项目文件夹中的终端并输入

elm reactor

这将 运行 默认情况下在端口 8000 上的 Web 服务器,您可以在浏览器中打开 http://localhost:8000,然后导航至 Main.elm 以查看 "Hello, Elm" 例子。

这里的最终目标是创建一个按钮,单击该按钮会拉入 nytimes 存储库列表并列出每个存储库的 ID 和名称。让我们首先创建那个按钮。我们将使用标准的 html 生成函数来实现。用这样的东西更新 view 函数:

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

就其本身而言,单击按钮不会执行任何操作。我们需要创建一个 Action,然后由 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 中创建一个任务时,你实际上只是在表明你的意图,创建一种你可以交给 运行 时间的 "package" 来做一些会产生副作用的事情;在这种情况下,联系外界并从 URL.

中拉取数据

让我们继续创建一个从 url 获取数据的任务。首先,我们需要在 Elm 中使用一个类型来表示我们关心的数据的形状。您表示您只需要 idname 字段。

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 解码器将原始 JSON 转换为我们的 type-safe榆树class。创建以下解码器。

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

让我们把它分开。解码器将原始 JSON 映射到我们要映射到的类型的形状。在这种情况下,我们的类型是一个带有两个字段的简单记录别名。还记得我在几步之前提到过,我们可以通过使用 RepoInfo 作为带有两个参数的函数来创建 RepoInfo 实例吗?这就是我们使用 Json.object2 来创建解码器的原因。 object 的第一个参数是一个本身有两个参数的函数,这就是我们传入 RepoInfo 的原因。相当于一个二元数的函数。

剩下的g 参数拼写出类型的形状。由于我们的 RepoInfo 模型首先列出 id,然后列出 name,这就是解码器期望参数的顺序。

我们需要另一个解码器来解码 RepoInfo 个实例的列表。

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

现在我们有了模型和解码器,我们可以创建一个函数来完成 return 获取数据的任务。请记住,这实际上并没有获取任何数据,它只是创建了一个函数,我们可以稍后将其交给 运行。

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

有多种方法可以处理可能发生的各种错误。让我们选择 Task.toResult, which maps the result of the request to a 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)

这为我们提供了一种将 never-failing 任务映射到操作的方法。然而,StartApp 处理 Effects,它是 Tasks(以及其他一些东西)之上的一个薄层。在我们将它们结合在一起之前,我们还需要一个部分,这是一种将 never-failing HTTP 任务映射到我们类型 Action 的效果的方法。

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

你可能已经注意到我称这个东西为 "never failing." 一开始我很困惑所以让我试着解释一下。当我们创建一个任务时,我们保证有一个结果,但它是成功还是失败。为了使 Elm 应用程序尽可能健壮,我们实质上通过显式处理每种情况来消除失败的可能性(我主要指的是未处理的 Javascript 异常)。这就是为什么我们经历了首先映射到 Result 然后映射到我们的 Action 的麻烦,后者显式处理错误消息。说它永不失败并不是说 HTTP 问题不会发生,而是说我们正在处理每一种可能的结果,并且通过将错误映射到有效操作来将错误映射到 "successes"。

在我们的最后一步之前,让我们确保我们的 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)
      ]

最后,将这一切联系在一起的部分是使 update 函数的 FetchData 案例成为 return 启动我们任务的效果。像这样更新 case 语句:

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

就是这样!您现在可以 运行 elm reactor 并单击按钮来获取存储库列表。如果你想测试错误处理,你可以为 Http.get 请求修改 URL 看看会发生什么。

我已经发布了这个 as a gist. If you don't want to run it locally, you can see the final result by pasting that code into http://elm-lang.org/try 的整个工作示例。

在此过程中,我尽量对每一步都做到非常明确和简洁。在一个典型的 Elm 应用程序中,很多这些步骤将被压缩成几行,并且将使用更惯用的 shorthand。我试图通过使事情尽可能小和明确来为您免除这些障碍。希望对您有所帮助!