Elm III: Adding Effects

bridge.jpg

Last week we dug deeper into using Elm. We saw how to build a more complicated web page forming a Todo list application. We learned about the Elm architecture and saw how we could use a couple simple functions to build our page. We laid the groundwork for bringing effects into our system, but didn't use any of these.

This week, we'll add some useful pieces to our Elm skill set. We'll see how to include more effects in our system, specifically randomness and HTTP requests.

To learn more about constructing a backend for your system, you should read up on our Haskell Web Series. It'll teach you things like connecting to a database and making an HTTP server.

Incorporating Effects

Last week, we explored using the element expression to build our application. Unlike sandbox, this allowed us to add commands, which enable side effects. But we didn't use any of commands. Let's examine a couple different effects we can use in our application.

One simple effect we can cause is to get a random number. It might not be obvious from the code we have so far, but we can't actually do it in our Todo application at the moment! Our update function is pure! This means it doesn't have access to IO. What it can do is send commands as part of its output. Commands can trigger messages, and incorporate effects along the way.

Making a Random Task

We're going to add a button to our application. This button will generate a random task name and add it to our list. To start with, we'll add a new message type to process:

type TodoListMessage =
  AddedTodo Todo |
  FinishedTodo Todo |
  UpdatedNewTodo (Maybe Todo) |
  AddRandomTodo

Now here's the HTML element that will send the new message. We can add it to the list of elements in our view:

randomTaskButton : Html TodoListMessage
randomTaskButton = button [onClick AddRandomTodo] [text "Random"]

Now we need to add our new message to our update function. We need a case for it:

todoUpdate : TodoListMessage -> TodoListState -> (TodoListState, Cmd TodoListMessage)
todoUpdate msg (TodoListState { todoList, newTodo}) = case msg of
  …
  AddRandomTodo ->
    (TodoListState { todoList = todoList, newTodo = newTodo}, …)

So for the first time, we're going to fill in the Cmd element! To generate randomness, we need the generate function from the Random module.

generate : (a -> msg) -> Generator a -> Cmd msg

We need two arguments to use this. The second argument is a random generator on a particular type a. The first argument then is a function from this type to our message. In our case, we'll want to generate a String. We'll use some functionality from the package elm-community/random-extra. See Random.String and Random.Char for details. Our strings will be 10 letters long and use only lowercase.

genString : Generator String
genString = string 10 lowerCaseLatin

Now we can easily convert this to a new message. We generate the string, and then add it as a Todo:

addTaskMsg : String -> TodoListMessage
addTaskMsg name = AddedTodo (Todo {todoName = name})

Now we can plug these into our update function, and we have our functioning random command!

todoUpdate : TodoListMessage -> TodoListState -> (TodoListState, Cmd TodoListMessage)
todoUpdate msg (TodoListState { todoList, newTodo}) = case msg of
  …
  AddRandomTodo ->
    (..., generate addTaskMsg genString)

Now clicking the random button will make a random task and add it to our list!

Sending an HTTP Request

A more complicated effect we can add is to send an HTTP request. We'll be using the Http library from Elm. Whenever we complete a task, we'll send a request to some endpoint that contains the task's name within its body.

We'll hook into our current action for FinishedTodo. Currently, this returns the None command along with its update. We'll make it send a command that will trigger a post request. This post request will, in turn, hook into another message type we'll make for handling the response.

todoUpdate : TodoListMessage -> TodoListState -> (TodoListState, Cmd TodoListMessage)
todoUpdate msg (TodoListState { todoList, newTodo}) = case msg of
  …
  (FinishedTodo doneTodo) ->
    (..., postFinishedTodo doneTodo)
  ReceivedFinishedResponse -> ...

postFinishedTodo : Todo -> Cmd TodoListMessage
postFinishedTodo = ...

We create HTTP commands using the send function. It takes two parameters:

send : (Result Error a -> msg) -> Request a -> Cmd Msg

The first of these is a function interpreting the server response and giving us a new message to send. The second is a request expecting a result of some type a. Let's plot out our code skeleton a little more for these parameters. We'll imagine we're getting back a String for our response, but it doesn't matter. We'll send the same message regardless:

postFinishedTodo : Todo -> Cmd TodoListMessage
postFinishedTodo todo = send interpretResponse (postRequest todo)

interpretResponse : Result Error String -> TodoListMessage
interpretResposne _ = ReceivedFinishedResponse

postRequest : Todo -> Request String
postRequest = ...

Now all we need is to create our post request using the post function:

post : String -> Body -> Decoder a -> Request a

Now we've got three more parameters to fill in. The first of these is the URL we're sending the request to. The second is our body. The third is a decoder for the response. Our decoder will be Json.Decode.string, a library function. We'll imagine we are running a local server for the URL.

postRequest : Todo -> Request String
postRequest todo = post "localhost:8081/api/finish" … Json.Decode.string

All we need to do now is encode the Todo in the post request body. This is straightforward. We'll use Json.Encode.object, which takes a list of tuples. Then we'll use the string encoder on the todo name.

jsonEncTodo : Todo -> Value
jsonEncTodo (Todo todo) = Json.Encode.object
  [ ("todoName", Json.encode.string todo.todoName) ]

Now all we have to do is use this encoder together with the jsonBody function. And then we're done!

postRequest : Todo -> Request String
postRequest todo = post
  "localhost:8081/api/finish"
  (jsonBody (jsonEncTodo todo))
  Json.Decode.string

As a reminder, most of the types and helper functions from this last part come from the HTTP Library for Elm. We could then further process the response in our interpretResponse function. If we get an error, we could send a different message. Either way, we can ultimately do more updates in our update function.

Conclusion

This concludes part 3 of our series on Elm! We took a look at a few nifty ways to add extra effects to our Elm projects. We saw how to introduce randomness into our Elm project, and then how to send HTTP requests. Next week, we'll explore navigation, a vital part of any web application. We'll see how the Elm architecture supports a multi-page application. Then we'll see how to move between the different pages efficiently, without needing to reload every bit of our Elm code each time.

Now that you know how to write a functional frontend, you should learn more about the backend! Read our Haskell Web Series for some tutorials on how to do this. You can also download our Production Checklist for some more ideas!

Previous
Previous

Elm IV: Navigation!

Next
Next

Elm II: Making a Todo List!