如何在Elm

时间:2016-01-27 03:38:05

标签: json http io elm

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

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

这些都是Github上的New York Times回购。基本上我从Github回复中有两个项目, id 名称

[ { "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))

我只想要一个repo名称的Elm对象,这样我就可以在一些div元素中显示,但我甚至无法获取数据。

有人可以慢慢地指导我如何编写解码器以及如何使用Elm获取数据?

1 个答案:

答案 0 :(得分:25)

更新Elm 0.17:

我已经更新了这个答案的完整要点,与Elm 0.17一起工作。你可以see the full source code here。它将在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版本更清晰,更易于理解。

榆树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以查看&#34; Hello,Elm&#34;示例

这里的最终目标是创建一个按钮,当单击该按钮时,会拉入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这样的命令式语言,这可能看起来有悖常理,当你编写代码表示&#34;从这个网址获取数据时,&#34;它立即创建一个HTTP请求。在像Elm这样纯粹的功能性语言中,你做的事情有点不同。当您在Elm中创建任务时,您实际上只是表明了您的意图,创建了一个&#34;包&#34;您可以将其移交给运行时以执行导致副作用的操作;在这种情况下,请联系外部世界并从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转换为我们的类型安全的Elm类。创建以下解码器。

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

让我们挑选一下。解码器将原始JSON映射到我们要映射的类型的形状。在这种情况下,我们的类型是一个带有两个字段的简单记录别名。请记住,我前面提到我们可以使用RepoInfo作为一个带两个参数的函数来创建一个RepoInfo实例吗?这就是我们使用Json.object2创建解码器的原因。 object的第一个arg是一个函数,它本身带有两个参数,这就是我们传递RepoInfo的原因。它等同于具有arity 2的函数。

其余参数说明了该类型的形状。由于我们的RepoInfo模型首先列出id,然后列出name秒,这就是解码器期望参数的顺序。

我们需要另一个解码器来解码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来获取Task的错误值。这只是因为,通过映射到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)

这为我们提供了一种将永不失败的任务映射到Action的方法。但是,StartApp处理的是Effects,它是一个关于Tasks(以及其他一些东西)的薄层。在我们将它们组合在一起之前,我们还需要一个部分,这是将永不失败的HTTP任务映射到我们的类型Action的效果的方法。

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

你可能已经注意到我打过这个东西,&#34;永远不会失败。&#34;起初这让我很困惑,所以让我试着解释一下。当我们创建任务时,我们会保证一个结果,但它是成功还是失败。为了使Elm应用程序尽可能健壮,我们实质上通过显式处理每个案例来消除失败的可能性(我主要是指一个未处理的Javascript异常)。这就是为什么我们经历了首先映射到Result然后映射到我们Action的问题的原因,view显式处理错误消息。说它永远不会失败并不是说HTTP问题不会发生,而是说我们正在处理所有可能的结果,并且错误被映射到&#34;成功&#34;通过将它们映射到有效的动作。

在最后一步之前,请确保我们的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函数的FetchData -> ({ model | message = "Initiating data fetch!" }, fetchDataAsEffects) 大小写返回启动我们任务的效果。像这样更新case语句:

elm reactor

那就是它!您现在可以运行Http.get并单击按钮以获取存储库列表。如果您想测试错误处理,可以直接修改{{1}}请求的URL,看看会发生什么。

我已发布了此as a gist的完整工作示例。如果您不想在本地运行它,可以通过将代码粘贴到http://elm-lang.org/try来查看最终结果。

我试图在整个过程中的每一步都非常明确和简明扼要。在典型的Elm应用程序中,很多这些步骤将简化为几行,并且将使用更多惯用的简写。我试图通过尽可能小而明确的方式来避免这些障碍。我希望这有帮助!