In today's episode, we're going to look at Tasks and Cmd. To do this, we'll be implementing the RandomGif example from the elm gitbook.

This example loads a random gif for a given topic from giphy, and provides a button to request a new random gif in that topic. Let's get started

Project

We'll start a new project and bring in:

  • elm-lang/html
  • elm-lang/http
mkdir random_gif
cd random_gif
elm package install -y elm-lang/html
elm package install -y elm-lang/http

Next, let's model the primary component:

vim Main.elm
module Main  exposing (..)

-- We'll need to import some modules for later
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Http
import Json.Decode as Decode

-- We'll define the model.  Our RandomGif component just takes a topic and the
-- url for the gif we're displaying.
type alias Model =
    { topic : String
    , gifUrl : String
    }

-- We'll define an `init` function to create our initial data - we're
-- introducing a new concept here.
-- We'll also reference a gif on the internet for our `waiting` state, which
-- happens initially before we've requested anything.
init : String -> ( Model, Cmd Msg )
init topic =
    let
        waitingUrl =
            "https://i.imgur.com/i6eXrfS.gif"
    in
        ( Model topic waitingUrl
        , getRandomGif topic
        )

For this our initial data consists of a 2-tuple containing our model and a Cmd that produces a Msg - the Cmd we'll start out with is a function called getRandomGif that accepts our topic as an argument. We'll define that a bit later.

-- UPDATE

-- Our messages will be either to `RequestMore`, which asks for a new gif,
-- or `NewGif`, which is what we get when the http request completes.
-- It's a Result type, which I don't think we've covered before, so I've linked
-- to the documentation for Result in the resources.

-- Just to reiterate - when we send a Cmd, we're asking the Elm runtime to do
-- some work for us.  When that work is completed, we'll be sent a new Msg that
-- tells us what happened, and that's what our NewGif type describes.
type Msg
    = RequestMore
    | NewGif (Result Http.Error String)


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
-- When we request another gif, we won't update the model but we'll add a new
-- Cmd that we want to take place - another call to `getRandomGif`
        RequestMore ->
            ( model, getRandomGif model.topic )

-- We don't really want to discuss how to handle the incoming Result yet, so
-- let's just stub it out for now.
        NewGif _ ->
            ( model, Cmd.none )

For now, we'll move on to the view. We'll come back to this in a bit.

view : Model -> Html Msg
view model =
    div []
        [ h2 [] [ text model.topic ]
        , div [] [ img [ src model.gifUrl ] [] ]
        , button [ onClick RequestMore ] [ text "More, better..." ]
        ]

And now we can wire it all together, with Html.program, which is like beginnerProgram but with subscriptions, which we'll ignore for now, and with the ability to send out Cmd so we can perform stuff in the outside world, which we'll use for Http calls.

main =
    Html.program
        { init = init "cats"
        , view = view
        , update = update
        , subscriptions = always Sub.none
        }

If you try to run this now, it will fail because there's no getRandomGif. Let's just implement it as Cmd.none for now, which returns a Cmd that doesn't do anything:

getRandomGif : String -> Cmd Msg
getRandomGif topic =
    Cmd.none

With that, you should be able to load it up in the reactor and see it working, but not terribly exciting.

Next, let's implement getRandomGif using the Http package:

getRandomGif : String -> Cmd Msg
getRandomGif topic =
    let
        -- We need a URL to get - we'll use the topic to construct a Giphy API
        -- request.
        -- NOTE: This includes a public, shared beta API key
        url =
            "https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=" ++ topic

        -- Then we'll create an Http request with `Http.get`, passing it our URL
        -- as well as a Json.Decoder.  I'll explain Decoders a bit more
        -- momentarily.
        request =
            Http.get url decodeGifUrl
    in
        -- Finally, we `send` the request, along with a function that will
        -- handle the returned data - in this case, that function is the
        -- function that builds a `NewGif` message.  This implies that the
        -- return value of the `Http.send` function is a `Result`.
        Http.send NewGif request


-- Now we get to the `decodeGifUrl` function.  This returns a Json.Decoder for a
-- String.  We use a few functions from the `Json.Decode` module, which let us dig
-- into a Json value and extract a string from it.  This results in a decoder
-- that, when provided with a JSON value representing an object with a `data` key
-- which contains an object with an `image_url` key, will return the value in
-- the `image_url` key as a String type.
decodeGifUrl : Decode.Decoder String
decodeGifUrl =
    Decode.at [ "data", "image_url" ] Decode.string

If you refresh the reactor now, you can see that an HTTP request is made each time you click the button. We're also getting that sent back as a Msg, so let's handle the Msg in our update.

First, let's talk about the NewGif type. It's a Result. We defined ours as a Result where the error cases is an Http.Error and the success case is a String. Our Result, then, is a union type of Ok String and Err Http.Error.

Now that we know that, let's write the update:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        RequestMore ->
            ( model, getRandomGif model.topic )

        NewGif result ->
            -- We'll pattern match on the result to handle failure and success
            -- differently.
            case result of
                Ok url ->
                    ( { model | gifUrl = url }, Cmd.none )

                Err _ ->
                    ( model, Cmd.none )

If you refresh the page now, you'll find that our random gif fetcher essentially works. Before moving on, let's look at something else.

Our update right now handles NewGif in a single branch, pattern matching inside of that branch. We can actually pattern match a bit deeper into the NewGif type, and avoid the nested case statement though!

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        RequestMore ->
            ( model, getRandomGif model.topic )

        NewGif (Ok url) ->
            ( { model | gifUrl = url }, Cmd.none )

        NewGif (Err _) ->
            ( model, Cmd.none )

That's a lot cleaner, right?

Summary

So that's it. We just implemented the gitbook's RandomGif example, with a lot of exposition along the way. I hope you learned something useful. See you soon!

Resources

Code Listing

Here's the whole code listing for today's program, if you had any issues:

module Main exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Http
import Json.Decode as Decode


main =
    Html.program
        { init = init "cats"
        , view = view
        , update = update
        , subscriptions = always Sub.none
        }


getRandomGif : String -> Cmd Msg
getRandomGif topic =
    let
        url =
            "https://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=" ++ topic

        request =
            Http.get url decodeGifUrl
    in
        Http.send NewGif request


decodeGifUrl : Decode.Decoder String
decodeGifUrl =
    Decode.at [ "data", "image_url" ] Decode.string


type alias Model =
    { topic : String
    , gifUrl : String
    }


init : String -> ( Model, Cmd Msg )
init topic =
    let
        waitingUrl =
            "https://i.imgur.com/i6eXrfS.gif"
    in
        ( Model topic waitingUrl
        , getRandomGif topic
        )


type Msg
    = RequestMore
    | NewGif (Result Http.Error String)


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        RequestMore ->
            ( model, getRandomGif model.topic )

        NewGif (Ok url) ->
            ( { model | gifUrl = url }, Cmd.none )

        NewGif (Err _) ->
            ( model, Cmd.none )


view : Model -> Html Msg
view model =
    div []
        [ h2 [] [ text model.topic ]
        , div [] [ img [ src model.gifUrl ] [] ]
        , button [ onClick RequestMore ] [ text "More, better..." ]
        ]