Single-Page Web Apps in Elm: Part Four - Side Effects

Single-Page Web Apps in Elm: Part Four - Side Effects

Source on Github: Elm Tutorial - Part Four

Other Posts in this Series

  1. Single-Page Web Apps in Elm: Part One - Getting Started with a New Language
  2. Single-Page Web Apps in Elm: Part Two - Functional Routing
  3. Single-Page Web Apps in Elm: Part Three - Testing and Structure
  4. Single-Page Web Apps in Elm: Part Five - JavaScript Interoperability


Introduction

Well, given that I've already decided it's obvious I won't be completing this series in my originally-intended four parts, I'm going to make the next couple posts shorter and more focused. In this post we are going to look more deeply at generating and consuming side effects in our Elm code. In the next post we'll look at how to interoperate with JavaScript to do things that can't be done in Elm or how to reuse code we have (or someone else has) already written in JavaScript.


Introducing the Outside World to Our Application

When last we left off we had a few views and a few routes. However, we were only displaying hard-coded text. Hard-coded text is good. It's easy. It's nice that it's pure and we can count on it always being there. For a real-world application it is going to be rare that all of our data is pre-defined and even if it is we will more than likely need to deal with some sort of user interaction. We need to deal with the real world and all of the uncertainty that comes with that.

How then do we introduce the real world to our functional environment where every function must return the same result for the same input? Previously, we touched on this. We do so by representing side effects as data. The side effects our application produces and consumes start out an requests, or descriptions, of the side effects we expect. These descriptions is all our code deals with. Looking back at our initialization function from where we left off we find something like this:

Where the Cmd type is the data type for a request for some side effect. We command the Elm runtime to perform some side effect for us. Here our init function could make some request to get the initial data to populate our application. The Elm runtime will try to execute the side effects associated with these commands and on their completion will feed the message (Msg) back into our application. The side effects and nastiness have to happen somewhere, just not in our application. The Elm type system will ensure that we are always handling any potential errors, thus keeping runtime errors out of our code. Handling errors is usually going to take the form of providing our commands different messages to feed back into our system based on success or error/failure.

Let's start by taking our init function and having its initial command be to request a list of posts from the server. Well, this brings up an immediate question, how do we perform AJAX requests in Elm? Of course there's a module for that. Install "evancz/elm-http":

Aside: The documentation for elm-http: evancz/elm-http.

We're interested in performing a GET request. Not surprisingly there is a function for doing just that. The get function looks like this:

It takes something called a Json.Decoder, a String and gives us back something called a Task. Hopefully it is fairly obvious what each of these parts are even if we don't know exactly how they work. Because Elm is statically typed we can't just bring in whatever random JSON the server gives us. We have to assign a type to our JSON. We'll look at Decoders in more depth shortly, but for now Decoders can just be thought of as the piece that takes the response from the server and turns it into something useful for us. It is analogous to JSON.parse except we have to provide it with a type to expect. That type is then returned as the successful result of the Task. We know these are the same type because they use the same type variable in the type declaration of the function. The String is the url to hit. Nothing special there. The return type of Task is the actual thing our command (Cmd) will request be run. It is very much like a lazy Promise. When you create a Promise it immediately executes, but you can listen for (or get) the value of it later with the "then" method. Tasks describe their computation but don't try to execute those computations until they are commanded to by the Elm runtime. Like Promises, Tasks can have two branches, a success branch and a failure branch. You see this in the type declaration. The Task has two associated values, one for error and one for success.

Yes, but this will not satisfy what is expected of our init function because Task is not Cmd. Right, the command our init function is going to return is going to be a command to run the get Task. The Cmd type is essentially a wrapper type for a Task that knows what message (Msg) to return for success or failure of the Task. To turn a Task into a Cmd we will often use the "perform" function from the Task module.

Looking just at the type, let's piece together what we need to do. We need to provide two functions, one which takes the error value and returns a message and one that takes the success value and returns a message. The other thing we need to provide is our Task. We already have that covered. What then do we provide for those first two functions?

Let's look at our messages module (src/Messages.elm).

Yes, I've added a bit. We have two new messages, one for successful fetch, one for failed fetch. If we remember our discussions of Union types we know these are really type constructors, each of which take one value. These will fill out everything we need for the perform function to give us a Cmd.

You'll notice on FetchSucceed we are expecting a List of Posts. What is a Post? For now we'll use this type I'm adding to src/Models.elm:

Back in Main.elm I've updated our init function with our new discoveries:

We also need to update our imports:

Reading through that code the only thing we haven't covered yet is the decodePosts function I'm passing to Http.get. We've got a whole section on JSON decoders coming up. There is another piece to this missing though. If we try to compile this, not only will Elm complain about this missing decodePosts function it will also complain that we are not handling FetchFail and FetchSucceed in our update function. It's not possible for us to compile a function in which we do not exhaustively handle the potential input.

For now let's handle our two new messages like this (src/Update.elm):

We'll just log out the data we receive and leave the state alone for now.

Aside: For those who like their learning in video form, Richard Feldman of NoRedInk gave a very good talk about representing side effects as data: Effects as Data.


JSON in a Type-Safe World

When working with REST APIs in JavaScript you're probably very used to how easy it is to deal with those JSON responses. You've got a response object. You can just start referencing the keys on that object. In order to maintain type safety this is going to take a little more work in Elm. As a reward for this work once a response makes it into our application we are guaranteed that everything will work as expected. We will be forced to handle all error states. If the JSON response changes and we update our code in one place there is no chance we will forget to update it in another place.

Decoding JSON in Elm really takes place in three steps. The first step is to define a type to translate the JSON response into. This can be any Elm type, but will usually be a List or Record. The second step is to define a Decoder. A Decoder is a value that describes the form of the expected JSON. The third step is to apply the Decoder to the raw JSON. Very often the third step will be handled for us by the library functions we are using.

The best way to see how this works is probably just to jump into examples. Let's take a look at decoding a response for our fetchPosts request. We know that eventually we want the data to take the form of our Post type:

Just a Record with four fields, simple for now, we'll expand on this later. What then does the JSON response from our server look like? Let's take a quick detour and look at the mock data I've set up the server to return.

In ./server.js our imports and mock data look like this:

And our routes looks like this:

Our mock data mimics exactly the Post type we set up in our Elm code. That makes things easy. This doesn't have to be the case. The JSON response can have more fields than you want to bring into Elm. It can have nested values you want to flatten. We'll look at some of that in a minute.

Back in Main.elm I add a Decoder for our Post type:

You can probably piece this together some just by reading through it, but there is a lot going on. Elm's Json.Decode module gives us a few Decoders out of the box. Since it is simpler, let's break down the decodePosts Decoder first. This Decoder is for decoding a List of Posts. I think we can assume Json.list decodes lists for us. To verify let's look at the source:

Yep, Json.list takes a Decoder for some type "a" and returns a Decoder for Lists of that type. You'll notice, as we mentioned, that a Decoder is just a type. It is just data. It is a description of how to decode something. It doesn't actually do anything by itself. In our case the application of this Decoder will be handled for us by Http.get. If you do want to apply the Decoders yourself there are functions to do that. Our Decoders for example would be something we would probably want to unit test. Later we will be pulling JSON from localStorage and we will need to apply the Decoders ourselves. Usually when we are decoding JSON it is going to be from a String. So we need a function that takes our Decoder, a String and returns a Result. Yes, I capitalize Result intentionally. We are going to use the Result type we've seen before. If a Decoder for our given JSON String is successful we will get a Result of type Ok (List Post). If it is not successful the Result will be Err String where the String is a description of how it failed. Http.get will the return the FetchFail message (Msg). The function that does this application is called decodeString from the Json.Decode module:

It does exactly what we said we needed it to do. No surprises.

The more interesting bit of decoding for us is the object. For common types (List, String, Int, Boolean... etc) Elm provides Decoders for us. Objects will be of unique shape and we need a way to describe that to Elm. Looking back at our Decoder:

Json.object4 is a Decoder for an object. The 4 appended to the end of object is the number of fields the resulting object is expected to have. Such helpers are provided by Elm for objects with one field up to eight fields (object1...object8). If you need more fields than that you will need to roll your own. Json.object4 takes a function that takes four values, in this case our Post type constructor, and four Decoders, one for each of the expected fields.

For our four Decoders we are using the Json.at function. This function gets the value of a field from our JSON. The Json.at function takes two arguments. The first is a List of Strings describing the path to the value we want. In this case all of the fields are top-level so each List has only one value in it, but if some field were nested we would add the keys for each level of the nesting. The second argument is a Decoder to decode the value found at the given location in the object.

So Json.at uses the List of Strings to find a value in an object and then a Decoder to interpret that value.

Let's say instead of a flat data structure our API was returning a response of this form:

How would this change our object decoder?

This would decode the new response type into our previously defined Post type.

At this point things should compile. We really haven't changed our application because we aren't doing anything with the response. Let's start up the node server:

Build the project:

Then when we navigate to http://localhost:3000 we should see this in the console:

Nice, things are working.

A Little Cleanup and Tests

On the top level of our project I am going to add a new module called Decoders. The Decoders we've written so far don't really belong in Main.elm. I'm going to put them into a file at src/Decoders/Posts.elm.

You'll need to update import in Main.elm, but everything else should still work as is. Now we can go over to our tests directory and add a module for testing our Decoders Test/Decoders/Posts.elm.

The tests we are going to run:

And the supporting data:

The triple quotes (""") are for multiline String literals. Finally, update tests/Test.elm to run our new tests:

Now as we add more fields to our Post type and our JSON responses we can make sure we are handling everything properly.


Updating the UI with Our New Data

Now all that is left is to feed our new found data down to our views. The first thing we need is a place to save the data we get from the server. That's going to go on our state. Back in Models.elm we're going to update our State type to be:

What is a Maybe? A Maybe is a wrapper for values that may be empty. Maybe we will have some Posts and maybe we won't. Maybe is defined as a Union type and is one of the default imports available in every module. It is very similar to the Result type we have seen before.

When a Maybe has a value we use the type constructor Just, otherwise we have a Nothing. Then we can update our newState function:

The newState function is used by our init function to create our initial application state. Initially we have no posts so we use the Nothing type constructor. We've already updated the init function to request posts. We need to change the update function to add the received posts to the State.

We set the posts property of the state to be "Just posts", meaning we have some posts. We leave the FetchFail branch as is for now. We could update the state with the current error or something of this nature, but we'll save that for later. Now we have our posts saved to the state.

The List of Posts is something we need to display on the home page. We'll find the view logic for that in src/View/Body.elm.

The first thing we need to update is our bodyContent function:

It just needs to feed our posts down to the postListView function. This is where things start to change. As a refresher, this is what the postListView function looked like at the end of part three:

That is going to become this:

Our function branches depending on if we have posts or we don't. If we have posts the rendering of those is delegated to another function postList:

This function constructs an unordered list container for us and maps our posts to list items with the postEntry function:

There now we've gotten data from a server, saved it to memory and rendered the data to the screen. Now, when the links in these post entries are clicked we need to go through and handle fetching single posts and rendering those. That will be much the same flow so I won't go over it here.


Conclusion

Hopefully from here you can work out fetching single posts and rendering them. Before I publish the next article in this series I'll update the Github repo with a solution for that.

Next time we will focus on JavaScript interoperability. We'll be doing that in the context of using localStorage to do autosaves. The is currently an in-development module for handling localStorage in Elm (elm-lang/local-storage), but it isn't ready yet. To use localStorage now we need to communicate with JavaScript somehow. This is of course a good opportunity for us to learn something new. Until next time, some wise man said some wise thing, but I forget what it was.

Other Articles About Elm

Programming in Elm: Modals in a Pure Environment


Victor Almeida

Fullstack Developer | Certsys Tecnologia da Informação | Chatbot Consultant

3y

Hi Kevin! Thank you for your contribution regarding this topic! Unfortunately I can't read the third chapter due to an error - https://www.linkedin.com/pulse/single-page-web-apps-elm-part-three-testing-structure-kevin-greene/ Do you know what would be the problem? Thanks again!

Like
Reply

To view or add a comment, sign in

Insights from the community

Explore topics