Skip to content

Instantly share code, notes, and snippets.

@napcs
Last active April 15, 2017 18:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save napcs/53f2aa966c1583e4c3b5b99249feb32a to your computer and use it in GitHub Desktop.
Save napcs/53f2aa966c1583e4c3b5b99249feb32a to your computer and use it in GitHub Desktop.
EFP #47

EFP 47

This is the solution to the "Who's In Space" problem in Exercises For Programmers, implemented in Elm v0.18

This exercise uses the OpenNotify API and that API doesn't support CORS, so you'll need to use a local proxy if you want to make this work in a browser.

But you can use my app QEDProxy as a simple local proxy.

Install it with npm:

npm install -g qedproxy

Then run it like this:

qedproxy --api http://api.open-notify.org

This'll make http://localhost:4242/api/astros.json proxy to the real endpoint. QEDProxy has CORS headers set appropriately so now the example app will work.

So now you can paste the below code into http://elm-lang.org/try and see it work.

Or do it locally:

$ mkdir space
$ cd space
$ npm install -g elm
$ elm-package install
$ elm-package install elm-lang/http

Then create app.elm and save the contents.

Make it with

$ elm make app.elm

And open the generated index.html file

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Http
import Task
import Json.Decode as Decoder exposing(Decoder, field)
-- Every elm program starts with main. We'll use the `App.program` function
-- to kick things off. It needs some functions mapped.
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- MODEL
-- Describe a Person - a name and the craft they're on.
-- Here's what the data looks like:
--
-- {
-- "message": "success",
-- "number": 3,
-- "people": [
-- {
-- "craft": "ISS",
-- "name": "Anatoly Ivanishin"
-- },
-- . . .
-- ]
-- }
--
-- So, each item under "people" has a name and a craft. That's the PersonRecord we're defining:
type alias PersonRecord =
{ name: String
, craft: String
}
-- Describe our model - the representation of the state of the app
-- In this app, it's a list of PersonRecords and an error message we can display if something goes wrong.
type alias Model =
{ people : List PersonRecord
, errorMessage : String
}
-- This function returns an initialized model with an empty list. This is
-- what our app will start with, and any other time we need to reset
-- the model.
initialModel: Model
initialModel =
{ people = [], errorMessage = "" }
-- init is what runs when the app starts - it's mapped by Html.program. It returns a model and the function that kicks it all off.
init : (Model, Cmd Msg)
init =
( initialModel
, getPeopleInSpace
)
-- UPDATE
-- Normally we'd have messages for button clicks and other events.
-- But we can have messages to represent other events, like successful API results.
-- In this app the only thing we do is fetch records from the API.
-- So the Msg is the result from our API call.
-- The successful request is going to return a list of PersonRecords
-- A failure? It'll return an HTTP error. We'll pattern match these values
-- in the update function.
type Msg
= GotResultsFromJson (Result Http.Error (List PersonRecord))
-- update is fired when one of those events fires. Did the request succeed or not?
-- if it succeeds, create a new model with the data we got back.
-- If it fails? Just display the default model.
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
GotResultsFromJson (Ok data) ->
( {model | people = data}, Cmd.none)
GotResultsFromJson (Err message) ->
( {model | errorMessage = ("Can't get data because " ++ toString message)}, Cmd.none)
-- VIEW
-- The display of the data. Show a div with an h1 and an unordered list.
-- We use List.map to iterate over the list of PersonRecords, generating an array of
-- list items. We put the name and the craft in the text of each listitem
-- Weird to read at first, but a lot nicer than a ForEach loop.
view : Model -> Html Msg
view model =
let
content =
(List.map (\p -> li [] [text (p.name ++ " - " ++ p.craft) ]) model.people)
in
div []
[ div [] [text model.errorMessage ]
, h1 [] [text "Who's in space"]
, ul [] content
]
-- SUBSCRIPTIONS
-- We need to define subscriptions for this app because we are using the Html.program function to kick
-- off the app, and it requires that we provide it some subscriptions. But we don't need any subscriptions
-- so we just state that. One of the drawbacks to programming to an interface sometimes.
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- HTTP
-- To get data from the API, we set the URL variable, and use a Task to make the call.
-- A Task will execute and then emit a message that our Update will respond to.
-- We perform the task, passing the messages it will return, followed by the function
-- the task should call.
-- The Http.get function needs to know how to decode the results it gets back, so
-- we have to pass a JSON decoder in, along with the URL. This is not JS - Elm is staticly
-- typed, so we have to map data types to Elm data types. That's what a decoder does.
getPeopleInSpace : Cmd Msg
getPeopleInSpace =
let
url =
"http://localhost:4242/api/astros.json"
in
decodePeopleInSpace
|> Http.get(url)
|> Http.send GotResultsFromJson
-- Here's the decoder. It needs to return a List of PersonRecords
-- First we define the decoder for the PersonRecord - the name and craft.
-- Once we have that we can use `at` to fetch all the data from the "people" field and
-- decode it to a list.
decodePeopleInSpace : Decoder (List PersonRecord)
decodePeopleInSpace =
let
decoder = Decoder.map2 PersonRecord
(field "name" Decoder.string)
(field "craft" Decoder.string)
in
Decoder.at ["people"] (Decoder.list decoder)
-- That's the end of our program!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment