DEV Community

Cover image for Simple code is different from simplistic code: Elm vs JavaScript
Marcio Frayze
Marcio Frayze

Posted on • Updated on

Simple code is different from simplistic code: Elm vs JavaScript

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. It should be very clear, for example, what format of the expected return, which fields are required and which are optional, etc.

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

Discussion (12)

Collapse
romeerez profile image
Roman Kushyn

The title is just a bit... misleading. is ELM really simplistic?

Simplistic - treating complex issues and problems as if they were much simpler than they really are.

I think simplistic solution is to wrap await fetch into try/catch. Better but less simplistic solution is to have own wrapper for fetch (or axios) with default error handling.

2-3 times more lines count, all native APIs are wrapped into ELMs, Result type is used but not obvious where is it coming from, somehow you need to know and write return type of functions, like HTTP.get returns Cmd Msg. And in the end, only small community of people will be able to support your code.

Collapse
marciofrayze profile image
Marcio Frayze Author

Hey Roman, thanks for your comment.

This article is a translation from my original post (in brazilian portuguese, my native language) so maybe that's causing some noise in the translation.

But to better understand what I meant as being "simplistic", think about this definition (taken from wordnik.com/words/simplistic):

"Simplistic. Adjective. In a manner that simplifies a concept or issue so that its nuance and complexity are lost or important details are overlooked."

So the goal is not to say that we all should use Elm (as I tried to say in the conclusions, I still use JS/TS on some projects, and that's fine), but to always try to find the simplest solution that do not overlook the important details.

Your idea of using a wrapper around fetch to make it safer is exactly the kind of thought I was hoping to rise. And using the definition of "simplistic" above, your idea (if well implemented) could be a nice and simple (not simplistic) solution.

But it may be a little tricky to create a wrapper that also handles the situation of a return in an invalid format, for example. Or a return where a field is optional, and so on.

Languages like TypeScript take a step toward trying to help in these scenarios. Elm takes many other steps in this direction. But which language to choose is not the central idea of this post. JavaScript is a good technology, but we need to act even more proactively on these potential problems in this language.

Collapse
romeerez profile image
Roman Kushyn

Thanks for explaining, so you wanted to show that ELM takes care of data integrity so developer can keep calm and this is why it's "simple". And if it makes sense to always validate JSON data from server instead of trusting it - that's another question, for developer to decide, depends on situation

Thread Thread
marciofrayze profile image
Marcio Frayze Author

Yes, but not just data integrity. That was just the main example, but Elm takes care of all possible runtime exceptions. All those errors on the browser console? They never happen when you are using Elm. A little too extreme? Maybe. But Elm makes it so easy that I believe the benefits compensate the efforts.

But as you said, it's for the developer to decide, and I agree. But it's important we make an informed and well thought decision.

Collapse
mindplay profile image
Rasmus Schultz

You might be interested in Hegel:

hegel.js.org/

It's a sound type checker for JS - enforces exhaustive checking for type unions and more of that Elm goodness.

I do wish this got more traction. I wish any language with sound typing got more traction and interest for that matter. Next level stuff for sure. 🙂

Collapse
marciofrayze profile image
Marcio Frayze Author • Edited on

Hello Rasmus.

I wasn’t aware of it, looks really interesting! I will definitely experiment with it. Thanks for sharing.

Collapse
scherrey profile image
Ben Scherrey

To bring the point home, I would suggest showing version 2 of the Javascript code that fully implements all the protections and use case of the Elm version at the end. I don't think people fully appreciate what strong typing does for you if they've only ever coded in Javascript or. other dynamic languages. Excellent article. The point of simple vs simplistic was spot on.

Collapse
lil5 profile image
Lucian I. Last

TLDR; MVC helps structure code

Collapse
marciofrayze profile image
Marcio Frayze Author • Edited on

I’m not a big fan of MVC for front end development. I prefer MVU (Model-View-Update, which is the same as The Elm Architecture). But yes, a good architecture helps a lot :)

guide.elm-lang.org/architecture

But the main focus on the article is not architecture per se, but the lack of care with runtime errors and such. I’ve seen pages created in js/react/redux with nice architecture and near 100% test coverage, and yet it was giving abnormal runtime errors during a presentation due the lack of care with http timeout issues. My guess is that that happens because most libs do not force (or even encourages) the handling of this kind of exceptions. And I think Elm does a great job avoiding this kind of pitfalls.

Collapse
lil5 profile image
Lucian I. Last

As long as you separate View, Storage(Models Types), Events(Controllers, Update), (maybe even separate business logic if necessary) a project is so much easier to read.

Collapse
xem0n profile image
Xem0n

I know it's not really topic-related but why did you use a promise chaining in an async function? Seems redundant

Collapse
marciofrayze profile image
Marcio Frayze Author • Edited on

I'm not sure if I understood what you said, but the (async function() { is necessary otherwise I'll get a Uncaught SyntaxError: await is only valid in async functions, async generators and modules.

And I used a then chaining just because I find it more declarative (a more functional style code).

And if I take off the async and the await, then the hideLoadingMessage function will run before the fetch finishes. But maybe I could chain it as well, or something, and then the async/await could be taken off.