Elm vs HyperScript - A Wordle implementation
Tags: elm, hyperscript, webdev
Published: 2022-01-30 03:13:33

---
title: Elm vs HyperScript - A Wordle implementation
published: true
description: A comparison between the Elm language and the HyperScript through a simple implementation of the Wordle game.
tags: elm, hyperscript, webdev
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yre651oo0h8rf503m6v3.jpeg
---

> HyperScript: [Demo](https://arhamjain.com/hyperwordle/), The source code is in the HTML Page Source.

> Elm: [Demo](https://elmwordle.guupa.com/), [Source Code](https://github.com/lucamug/elmwordle/blob/master/src/Main.elm), [Playground](https://ellie-app.com/gxSyb8nrgSCa1)

    
----


A few days ago I came across a simple Wordle implementation in HyperScript, a new event-oriented front-end scripting language, inspired by [HyperTalk](https://en.wikipedia.org/wiki/HyperTalk) and a companion of [htmx](https://htmx.org/).

Not sure exactly what a "companion of htmx" stands for here. It could be that both are sponsored by the [same company](https://github.com/sponsors/bigskysoftware) or that they complement each other on the same page with the common goal of avoiding JavaScript.

The HyperScript implementation of Wordle is written in [57 total lines of code](https://twitter.com/htmx_org/status/1486337732888588289).

The code is in the source of the HTML page and it looks quite **impressive that the entire implementation can be so compact**.

So I decided to rewrite it in Elm and see how it compares:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o0r3fcfl6ay9x2lfmmlh.gif)

# The DOM is the state

The HyperScript version is built around the DOM. **The DOM is used as a global mutable variable that stores the state of the application**, similar to how web applications were written using JQuery.

In detail, there are three classes `.bg-muted`, `.bg-warning`, and `.bg-success` which are written and read throughout the life of the app, to change the state of blocks, both in the gameboard and on the keyboard.

Then there is the class `.guess` that is used to mark the latest row typed by the user.

The HyperScript is partially written within the `<script>` element and partially inlined inside HTML, using the `_` attribute:

```html
<script type="text/hyperscript">
    init fetch words.json as json then set $word to (random in it).toUpperCase() then log $word

    def submit()
        set remaining to $word
        repeat for ch in children of first .guess index i
            set key to <[letter=${ch.innerText}]/>
            add .bg-muted to key
            if ch.innerText == $word[i] then
                add .bg-success to ch add .bg-success to key
                set remaining to remaining.replace(ch.innerText, '')
            end
        end
        repeat for ch in children of first .guess
            set key to <[letter=${ch.innerText}]/>
            if remaining.includes(ch.innerText) then add .bg-warning to ch add .bg-warning to key end
            set remaining to remaining.replace(ch.innerText, '')
        end
        if (.bg-success in first .guess).length == 5 then remove .guess from .guess
        otherwise remove .guess from first .guess
    end

    behavior kbdRow(keys) 
        on click[target[@letter]] send keyup(keyCode: (target[@letter]).charCodeAt(0)) to <body/>
        init repeat for ch in keys 
            append `<button letter='${ch}' class='secondary ma1 pa2 w2'>${ch}</button>` to me
    end
</script>

<body _="
    on keyup(keyCode)[keyCode >= 65 and keyCode <= 90] put String.fromCharCode(keyCode) into first <:empty/> in first .guess
    on keyup(keyCode)[keyCode == 8] put '' into last <.guess > :not(:empty)/>
    on keyup(keyCode)[keyCode == 13 and (<:not(:empty)/> in first .guess).length == 5] call submit()
    ">
    <div class="container">
        <h1 class="tc">Hyperwordle</h1>
        <div _="on load set h to my innerHTML then repeat 5 times put h at the end of me">
            <div class="guess contrast flex ttu tc b f3 w-100 justify-center">
                <span class="w3 h3 ma1 pa3 bg-muted"></span>
                <span class="w3 h3 ma1 pa3 bg-muted"></span>
                <span class="w3 h3 ma1 pa3 bg-muted"></span>
                <span class="w3 h3 ma1 pa3 bg-muted"></span>
                <span class="w3 h3 ma1 pa3 bg-muted"></span>
            </div>
        </div>
        <div class="flex justify-center ttu tc" _="install kbdRow(keys: 'QWERTYUIOP')"></div>
        <div class="flex justify-center ttu tc" _="install kbdRow(keys: 'ASDFGHJKL')"></div>
        <div class="flex justify-center ttu tc">
            <button class="secondary ma1 pa2 w-auto" _="on click send keyup(keyCode: 13) to <body/>">Enter</button>
            <div class="flex" _="install kbdRow(keys: 'ZXCVBNM')"></div>
            <button class="secondary ma1 pa2 w-auto" _="on click send keyup(keyCode: 8) to <body/>">⌫</button>
        </div>
    </div>
</body>
```

The Elm version uses instead [The Elm Architecture](https://guide.elm-lang.org/architecture/) where the state of the app is stored in an immutable data structure called `Model`. What shows up on the page is then just a representation of this `Model` converted to HTML by the `view` function.

In this implementation, I adopted the idea that **if some data can be calculated from others, it should not be stored in the `Model`**. This is usually a good approach as it assure that the model doesn't contain any stale calculation. For example, all the information about which letter has been guessed or not, and also the notion that the game is over or player won the game, is calculated just-in-time, when is needed.

This is the model:

```Elm
type alias Model =
    { currentGuess : List Char
    , pastGuesses : List (List Char)
    , wordToGuess : List Char
    , error : Maybe String
    }
```

It only stores what the user typed and what is the secret word to guess, plus a possible error from loading asynchronously the list of words to guess.

This is the entire [code](https://github.com/lucamug/elmwordle/blob/master/src/Main.elm) of the Elm version. This is an [Ellie](https://ellie-app.com/gxSyb8nrgSCa1) version of it.
    
    
# Compactness

**The HyperScript compactness is a combination of several things, but mainly the idea of DOM-oriented syntax**. The language is deeply integrated with the DOM. Adding/removing classes is a breeze. For example `on click toggle .red on me`. How can it get more direct and shorter than this?

Let's analyze another line of HyperScript code:
    
```html
init fetch words.json as json then set $word to (random in it).toUpperCase() then log $word
```

This is a remarkable small piece of code that does many things:
    
* On initialization of the app, send a GET HTTP request to fetch "words.json"
* Interpret it as JSON data
* Extract a random element from the JSON list (The secret word to guess) and assign it to `$word`
* Log this value into the console

This is something that in Elm require more structured work:
    
* In the `init` function we start sending out the command `Random.generate NewRandom (Random.float 0 1)` instructing the Elm runtime that we would like to have a random number. This is pure functional code, so no impure functions are allowed, this is one of these things that require some [mindset-shift](https://elm.dmy.fr/packages/elm/random/latest/#mindset-shift). Consider Elm as your assistant that does things for you. You just need to ask.
* Once the random number is ready we will get a message. At that point we can ask the Elm runtime to send out a GET request to get `words.json` and that we expect it to have a certain structure.
* Once `words.json` data is ready, we will get another message informing us of the result of the operation, including all possible sort of failure (network down, not a JSON file, not a data structure we expected, etc.)
* At this point, we can use the random number and the list of words to peak what secret word and save in the `Model`.
        
What happens in the HyperScript version if something goes wrong with the JSON file as described above?
    
The app just keeps running and lets you type the first row of characters. After that, **it stops working**.
    
Note that the HyperScript version **generates errors in the console also during the normal execution**. For example, continuing to type after reaching the five characters in a row, generates errors. The same when continuing to type after a solution is found. Probably this has been done on purpose, to make the code shorter. To avoid these errors, some extra code would be necessary.
    
Elm instead forces us to consider all the edge cases so that these types of errors became impossible. So, in case something goes wrong with the GET request, I display a message on the screen.
    
# Typos

In my projects, I try to repeat only once all the strings. This is because **the content of strings is not checked by the compiler** and typos can be deadly.
    
For example, there are three possible state for a block: `.bg-muted`, `.bg-warning`, and `.bg-success`. These are classes added to the DOM and have these meanings:

* `.bg-muted` (light gray background) is used for when a key in the keyboard has been used, 
* `.bg-warning` (light yellow background) is for when a character is used but it is in the wrong position 
* `.bg-success` (green background) when the character is a perfect match

In Elm I replaced them with a custom type:
    
```elm
type CharState
    = Muted
    | Warning
    | Success
```

This guarantees that no typos can go into the code. Then I have a conversion function, used to inject the classes in the DOM:
    
```elm
charStateToClass : CharState -> String
charStateToClass charState =
    case charState of
        Muted ->
            "bg-muted"

        Warning ->
            "bg-warning"

        Success ->
            "bg-success"
```

These strings appear only here in the entire code, so the possibility of introducing errors is minimized.

The HyperScript code instead has these strings used multiple times, for example:

```html
add .bg-success to ch add .bg-success to key
```

**Any typo in the string `bg-success` will go undetected and just causes a malfunction in the app**, without any apparent error notification. So only ad-hoc tests can detect these issues.

# Code and bundle sizes

The Elm implementation, which tries to mimic the original HyperScript implementation (there is probably a more "functional" way to implement the Wordle game itself), is around 260 lines of code. **Around five times the original HyperScript version**.

I would argue that the Elm version is more readable and simpler to be expanded with additional features but here we enter into a biased territory caused by my knowledge of Elm.

The HyperScript version, in addition to what is in the source of the page, requires an HyperScript runtime library that is 86KB minified and 24KB zipped.

The Elm language compiles to JavaScript and it doesn't need any separated runtime library as it is already included in the compiled JavaScript. The output of this Wordle implementation written in Elm is 33KB minified and 14KB zipped.

So this means that **concerning the sizes of stuff to load, Elm is around half the size of HyperScript**.

---------------

There are many more differences, but this is all that I have for this article. It has been a fun exercise. Have a look at the respective codes to learn more about these two languages.

There are also alternative implementation of Wordle in Elm [here](https://openprojectrepo.com/project/n1k0-wordlem-javascript-misc), [here](https://twitter.com/FlavioCorpa/status/1487537170709684235) and [here](https://twitter.com/janiczek/status/1487235356579803139).

❤️

---------------

> HyperScript: [Demo](https://arhamjain.com/hyperwordle/), The source code is in the HTML Page Source.

> Elm: [Demo](https://elmwordle.guupa.com/), [Source Code](https://github.com/lucamug/elmwordle/blob/master/src/Main.elm), [Playground](https://ellie-app.com/gxSyb8nrgSCa1)



----

*Note: In the past, there has been some disagreement about the choice of "vs" as the title in this series of articles. I agree with the criticism: there is ["plenty of room for everyone"](https://twitter.com/Rich_Harris/status/1226592707990163465). So please consider "vs" in the sense of a "comparison" rather than a "deathmatch".*