Simple code is different from simplistic code: Elm vs JavaScript

Marcio Frayze - Jan 7 '22 - - Dev Community

There are languages, frameworks, and libraries that strive to enable you to accomplish relatively complex tasks by writing a few lines of code. JavaScript is a good example. To make an http call to a page of my site using this language, you just have to write a single line:

await fetch("https://segunda.tech/about")
Enter fullscreen mode Exit fullscreen mode

Most people probably don't consider this code to be difficult or complex, but there may be hidden error scenarios that are not trivial to handle. To analyze this, I'll show you a small-page implementation using pure JavaScript and discuss potential issues. Next I will show you how to implement the same solution using the Elm programming language and analyze the same points.

Exercise: Retrieving a list of Pokémon names

To exemplify the problem I want to discuss in this article, I implemented in html and pure JavaScript (using Ajax) the minimum necessary to display a list with Pokémon names. I used a service from PokéAPI for this. The endpoint for retrieving the list of the first 5 Pokémon is quite simple: just call the URL https://pokeapi.co/api/v2/pokemon?limit=5 and the return will be a json containing the result below.

{
  "count": 1118,
  "next": "https://pokeapi.co/api/v2/pokemon?offset=5&limit=5",
  "previous": null,
  "results": [
    {
      "name": "bulbasaur",
      "url": "https://pokeapi.co/api/v2/pokemon/1/"
    },
    {
      "name": "ivysaur",
      "url": "https://pokeapi.co/api/v2/pokemon/2/"
    },
    {
      "name": "venusaur",
      "url": "https://pokeapi.co/api/v2/pokemon/3/"
    },
    {
      "name": "charmander",
      "url": "https://pokeapi.co/api/v2/pokemon/4/"
    },
    {
      "name": "charmeleon",
      "url": "https://pokeapi.co/api/v2/pokemon/5/"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

In this exercise the goal is to retrieve this data asynchronously and list on the html page only the contents of the name field (which is within result).

Implementing a solution using pure html and JavaScript

There are several ways to solve this problem using these technologies. Below I present my implementation.

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>List of Pokémons using HTML and JavaScript</title>
  <meta name="author" content="Marcio Frayze David">
</head>

<body>
  <p id="loading-message">
    Loading Pokémons names, please wait...
  </p>

  <ul id="pokemon-names-list">
  </ul>

  <script>

    (async function() {

      await fetch("https://pokeapi.co/api/v2/pokemon?limit=5")
        .then(data => data.json())
        .then(dataJson => dataJson.results)
        .then(results => results.map(pokemon => pokemon.name))
        .then(names => addNamesToDOM(names))

      hideLoadingMessage()

    })();

    function addNamesToDOM(names) {
      let pokemonNamesListElement = document.getElementById('pokemon-names-list')
      names.forEach(name => addNameToDOM(pokemonNamesListElement, name))
    }

    function addNameToDOM(pokemonNamesListElement, name) {
      let newListElement = document.createElement('li')
      newListElement.innerHTML = name
      pokemonNamesListElement.append(newListElement)
    }

    function hideLoadingMessage() {
      document.getElementById('loading-message').style.visibility = 'hidden'
    }

  </script>

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The idea is that at the end of the Ajax call, the loading message no longer appears and the list containing the Pokémon names is loaded within the tag with the id pokemons-names-list. I published this page on-line with JSFiddle so you can see the expected behavior.

I know hardly anyone would write a code like that. I didn't use any framework or external library and did some things that many would consider bad practices (such as putting JavaScript code right in html). But even if i had implemented this solution with popular technologies like React, JSX and Axios, the potential problems I want to discuss here would probably still exist.

Looking at the code above, the questions I'd like you to try to answer are:

  • What will happen if a timeout occurs in the Ajax call?
  • If the server returns a status http of failure, what will happen?
  • If the server returns a valid status http but the format of the returned content is different than expected, what will happen?

The above code does not answer any of these questions clearly. It is easy to visualize the "happy path", but any unexpected situation is not being treated explicitly. And while we should never put code into production that doesn't treat these scenarios, the JavaScript language doesn't force us to deal with them. If someone on your team forgets to do the right treatment for one of these potential problems, the result will be a runtime error.

If your team is unlucky, these scenarios may appear when the code is already in production. And when that inevitably happens, it's likely to blame the developer who implemented that part of the system.

But if we know that this type of situation must be addressed, why do languages, frameworks and libraries allow this type of code to be written?

What is a simple solution?

There is a big difference between a solution being simple and being simplistic. This solution I wrote in JavaScript is not simple. It's simplistic, as it ignores fundamental aspects of the problem in question.

Languages such as Elm tend to force us to think and implement the solution for all potential problems. The final code will probably be larger, but it will provide assurance that we will have no errors at runtime, as the compiler checks and enforces the developer to handle all possible paths, leaving no room for predictable failures.

Another advantage of this approach is that we have a self-documented code. For example, the format of the expected return should be very clear, including which fields are required and which are optional.

Implementing the same solution in Elm

Now let's look at a solution written in Elm for this same problem. If you don't know this language (or some similar language, such as Haskell or PureScript), you'll probably find its syntax a little strange. But don't worry, you don't need to fully understand this code to understand the proposal of this article.

First we need a simple html file, which will host our page. This approach is quite similar to what is done when we use tools such as React or Vue.

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>List of Pokémons using HTML and JavaScript</title>
  <meta name="author" content="Marcio Frayze David">
</head>

<body>
  <main></main>
  <script>
    Elm.Main.init({ node: document.querySelector('main') })
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

This time our html is just a shell. It will only load the application written in Elm (previously compiled) and place its contents within the tag main.

And finaly the interesting part: the code written in Elm. I will first list the code completely and then highlight and comment on some more relevant parts to the topic of this article.

module Main exposing (..)

import Browser
import Html exposing (..)
import Http
import Json.Decode exposing (Decoder)


-- MAIN


main =
  Browser.element
    { init = init
    , update = update
    , subscriptions = subscriptions
    , view = view
    }


-- MODEL


type alias PokemonInfo = { name : String }

type Model
  = Failure
  | Loading
  | Success (List PokemonInfo)


init : () -> (Model, Cmd Msg)
init _ =
  (Loading, fetchPokemonNames)


-- UPDATE


type Msg
  = FetchedPokemonNames (Result Http.Error (List PokemonInfo))


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of

    FetchedPokemonNames result ->
      case result of
        Ok pokemonsInfo ->
          (Success pokemonsInfo, Cmd.none)

        Err _ ->
          (Failure, Cmd.none)


-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
  Sub.none


-- VIEW


view : Model -> Html Msg
view model =
  case model of
    Failure ->
        text "For some reason, the Pokémon name list could not be loaded. 😧"

    Loading ->
      text "Loading Pokémons names, please wait..."

    Success pokemonsInfo ->
      ul []
        (List.map viewPokemonInfo pokemonsInfo) 


viewPokemonInfo : PokemonInfo -> Html Msg
viewPokemonInfo pokemonInfo =
  li [] [ text pokemonInfo.name ]


-- HTTP


fetchPokemonNames : Cmd Msg
fetchPokemonNames =
  Http.get
    { url = "https://pokeapi.co/api/v2/pokemon?limit=5"
    , expect = Http.expectJson FetchedPokemonNames decoder
    }


pokemonInfoDecoder : Decoder PokemonInfo
pokemonInfoDecoder =
  Json.Decode.map PokemonInfo
    (Json.Decode.field "name" Json.Decode.string)

decoder : Decoder (List PokemonInfo)    
decoder =
  Json.Decode.field "results" (Json.Decode.list pokemonInfoDecoder)
Enter fullscreen mode Exit fullscreen mode

I've published this page in the online editor Ellie so you can see this webapp up and running. I recommend you try to change the code and see what happens. It's a great way to start experimenting with the Elm language.

Analyzing the implementation in Elm

I will not explain in this article all this code and the architecture behind the Elm language. But I wanted to highlight some important parts for the context of the discussion of this article, starting with the definition of our types.

Type definitions

type alias PokemonInfo = { name : String }

Model type
  = Loading
  | Failure
  | Success (PokemonInfo List)
Enter fullscreen mode Exit fullscreen mode

In the code above is set a type alias, making it clearer to the person reading the code what is a PokemonInfo (in this case, a structure with a field called name of type String). This will also make life easier for our compiler by allowing you to handle the appropriate error when necessary and, during the build phase, be able to send more informative error messages.

Next, we define a type named Model that will be used to represent the current state of our application. In this example, our webapp can be in one (and only one) of the 3 possible states:

  • Loading: initial application state, indicating that the http request is still being processed.
  • Failure: represents a state of failure, indicating that there was a problem making the http call to the server (which may be timeout, a parsing failure of the return message, etc.).
  • Success: indicates that the request was performed and its return successfully converted.

Of the three defined states, only Success has extra information associated with it: a list containing elements of type PokemonInfo. Note that this leaves no room for ambiguity. If we have a state of success, it's mandatory we have a list of PokemonInfo defined and with a valid structure. And the opposite is also true: in case of failure, the list with the names of Pokémon will not be defined.

The construction of the html page

Elm was one of the pioneers in using the concept of virtual DOM and declarative programming in the development of webapps.

In the architecture of Elm, there is a very clear separation between the state of our application and what should be displayed on the screen. It is the responsibility of the view function to mount, from the current state of our application, a representation of our virtual DOM. And every time the state changes (when, for example, you finish loading the data with Pokémon names) this function will be reevaluated and a new virtual DOM created.

In our example, this occurs in the following code snippet:

view : Model -> Html Msg
view model =
  case model of
    Failure ->
        text "For some reason, the Pokémon name list could not be loaded. 😧"

    Loading ->
      text "Loading Pokémons names, please wait..."

    Success pokemonsInfo ->
      ul []
        (List.map viewPokemonInfo pokemonsInfo) 


viewPokemonInfo : PokemonInfo -> Html Msg
viewPokemonInfo pokemonInfo =
  li [] [ text pokemonInfo.name ]
Enter fullscreen mode Exit fullscreen mode

Here we have the declaration of 2 functions: the view and a helper function called viewPokemonInfo.

One advantage of using types to represent the state of our application is that always that a piece of code is to use this type, the compiler will compel the developer to handle all possible states. In this case: Loading, Failure and Success. If you remove the Loading treatment from the view function of our example, you will receive an error message similar to this when you try to compile the application:

Line 70, Column 3
This `case` does not have branches for all possibilities:

70|>  case model of
71|>    Failure ->
72|>        text "For some reason, the Pokémon name list could not be loaded. 😧"
73|>
74|>    Success pokemonsInfo ->
75|>      ul []
76|>        (List.map viewPokemonInfo pokemonsInfo) 

Missing possibilities include:

    Loading

I would have to crash if I saw one of those. Add branches for them!

Hint: If you want to write the code for each branch later, use `Debug.todo` as a
placeholder. Read <https://elm-lang.org/0.19.1/missing-patterns> for more
guidance on this workflow.
Enter fullscreen mode Exit fullscreen mode

This brings more protection for the developer person to refactor the code and include or remove states from the application, making sure it won't fail to address some obscure case.

Making a http call

The code snippet below is responsible for making the http call asynchronously and performing the parse of the return, turning it into a list of PokemonInfo.

fetchPokemonNames : Cmd Msg
fetchPokemonNames =
  Http.get
    { url = "https://pokeapi.co/api/v2/pokemon?limit=5"
    , expect = Http.expectJson FetchedPokemonNames decoder
    }


pokemonInfoDecoder : Decoder PokemonInfo
pokemonInfoDecoder =
  Json.Decode.map PokemonInfo
    (Json.Decode.field "name" Json.Decode.string)


decoder : Decoder (List PokemonInfo)    
decoder =
  Json.Decode.field "results" (Json.Decode.list pokemonInfoDecoder)
Enter fullscreen mode Exit fullscreen mode

It's impossible to deny that this code is longer than a call to a fetch function. But note that, in addition to making the call asynchronously, also validates and transforms the return into a List PokemonInfo, eliminating the need for any validation on our part.

At the end of the execution, a FetchedPokemonNames message will be issued along with the result of the operation: either a list with names of Pokémon already decoded or a result representing that an error occurred.

It will be the responsibility of the update function to receive this message and create a new state for the application.

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of

    FetchedPokemonNames result ->
      case result of
        Ok pokemonsInfo ->
          (Success pokemonsInfo, Cmd.none)

        Err _ ->
          (Failure, Cmd.none)
Enter fullscreen mode Exit fullscreen mode

Once again, we must deal with all possible scenarios. In this example, there are two:

  • if result is Ok, it means that our request has been successfully processed. A new state is then returned to our application, changing to Success, along with the list containing the Pokémon names.
  • if the result is Err, then we know that there was a problem during the request or when performing the json parsing. A new application state is returned, changing it to Failure.

Whenever the return of the update function is different from the previous state, the view function will automatically be triggered again, then a new virtual DOM is created and any changes will be applied to the screen. To better understand this process, you can read about The Elm Architecture on this page.

Conclusions

Although this article focused exclusively on http requests and JavaScript, the same concepts are applied in many other scenarios, libraries, frameworks and languages.

My intention is not to discourage the use of JavaScript. Elm is a wonderful language, but I still use JavaScript and TypeScript in some webapps and this is not the focal point of the problem. What I would like is that when you are consuming a function of your preferred language (regardless if it is a native function or from third-party libraries), that you always reflect: is there any scenario that this code is ignoring? Or, in other words, is this a simple or a simplistic solution?

Most importantly, when writing a new function, use a communication interface that encourages the person who consumes it to follow best practices. Even if she is following the path of minimal effort, she should be able to take care of all possible scenarios. Or, in other words, always follow the Principle of least astonishment.


Did you like this text? Checkout my other articles at: https://segunda.tech/tags/english

. . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player