Constructing HTML with Functional Functions

I heard that learning Elm is a good way to approach learning Haskell, so I gave it a go and was surprised early on about an approach to writing abstracted HTML. In this post I compare the way that R and Elm generate HTML and the differences between their approaches.

I was listening to the Linux Dev Time podcast based on a recommendation and this episode (episode 100) was about “how many different programming languages should you learn?”

One suggestion in amongst the interesting discussions was that “if you want to learn Haskell, start by learning Elm, because Elm uses Haskell syntax but it removes most of the most difficult concepts in Haskell.”

I’ve been trying to learn Haskell this year, building on some intermittent exposure to it in the last couple of years and partly because I joined a local functional programming meetup group which is fortunate enough to have some Haskell developers. I’ve read the first few chapters of ‘Learn You a Haskel for Great Good!’ and Haskell is a supported track on Exercism. As you can see from my summary of complete exercises Haskell is the language I’ve solved the most exercises in, followed very closely by R.

Haskell definitely feels like an academic language - even reading other people’s solutions to puzzles and code problems I find myself struggling to understand both the somewhat unique syntax and the approach at the same time. The fact that there is no if-else construct in Haskell means a lot of pattern matching, and the fact that there are no loops means a lot of recursion. All arguments to functions are whitespace separated, so the expression add 1 2 is a call of the function add with arguments 1 and 2 meaning that parentheses are frequently required, or other specific syntax quirks.

Rust (another language I’ve spent quite a bit of time learning) lists Haskell as one of its influences and that becomes quite apparent once you see the keyword deriving (for typeclasses) in both languages, e.g.

But Rust doesn’t use Haskell syntax, and isn’t nearly as ‘pure’ (lacking side-effects).

The advice to start with Elm sounded like it came from a good place, and seemed reasonable.

Elm is also one of the languages offered on Exercism but again referring to my summary, I haven’t done much at all with it - the introduction ‘hello, world’ exercise and one other. That at least means I installed it locally and got the toolchain working, but I certainly wasn’t familiar at all. My solution to the problem that I did solve involved a big if-else block.

I like learning languages (can you tell?) so I figured I’d give it a fair go and started reading Elm in Action by Richard Feldman. As well as an Elm core member, Richard is the author of the Roc language which is built in Rust and aims to be better than both Elm and Rust in the sense of being a “fast, friendly, functional language” with specific emphasis on each of the words in that phrase. I do plan to learn more Roc as well, but for now I’m focussed on gaining a better understanding of Haskell via Elm, so back to Elm…

Elm isn’t a new language - it first appeared in 2012 - but it’s also considered somewhat “stable” in that there haven’t been any big changes to it for a few years. Some people seem to rush to the conclusion that a language not in a constant state of change is “dead” or “stale”, but the author (of the language/compiler) makes it clear that

If you like what you see now, that’s pretty much what Elm is going to be for a while.

I have no problem with that.

Elm comes with a REPL, something I find extremely helpful for playing with a new language. One of the big surprises was how good the error messages are. Trying to do something like adding a number and string produces an explanation of what is wrong

> 1 + "a"
-- TYPE MISMATCH ---------------------------------------------------------- REPL

I cannot do addition with String values like this one:

3|   1 + "a"
The (+) operator only works with Int and Float values.

Hint: Switch to the (++) operator to append strings!

not a screen full of stack traces (I’m looking at you, JVM languages).

I think of Rust’s error messages as the most helpful; pointing out not only what went wrong, but a link to documentation explaining that pattern, and often a code suggestion for how to fix it. The tooling can even ‘auto fix’ some of those errors when it’s clear what the code should have been. Roc aims to have even better error messages than both of these - a high bar, but fantastic to aim for.

I have tended towards dynamic languages (R being one of them) where classes of variables might be coerced, promoted, or combined sensibly. Not every language supports that, and for good reason - I’ve definitely seen the value in strongly typed languages preventing the need for defensive coding such as a huge block of assertions at the top of every function to make sure what’s passed in actually makes sense. Some languages are less fussy…

Javascript lets you combine just about anything
Javascript lets you combine just about anything

I started following along Elm in Action - properly following along; not just reading and pretending to take things in like I’ve done all too many times with technical books. I had the REPL open and played around with what I could do as I was reading about each example.

One of the things this book gets right is to get to building something early on. A common complaint about Haskell books is that, since IO is a bit complicated in a language where functions are ‘pure’ and have no side-effects such as input and output, actually building even a ‘hello, world’ program tends to show up pretty late. Within Chapter 2 of Elm in Action we’re building a website. Kudos on that one.

One of the first examples was to output some HTML for a static site. This is, of course, an overly simple starting point and one could absolutely say that Elm isn’t required for this part, but it fits in nicely with what comes next - making the site interactive.

I’m old enough that in high school we had a class where we wrote a website. I don’t mean “built” one with some framework or package, I mean opening a text file and writing out the HTML to be rendered in a browser (Netscape). This wasn’t state-of-the-art design, but it gave me a reasonable introduction to the essentials of HTML markup.

The example code in Elm for the static site is

div [ class "content" ]
    [ h1 [] [ text "Photo Groove" ]                           
    , div [ id "thumbnails" ]                                 
        [ img [ src "" ] []
        , img [ src "" ] []
        , img [ src "" ] []

which might need some unpacking. This is a call of the function div which takes two arguments; a list containing a call to class with an argument "content", and a list containing a call to h1 and its 2 arguments (an empty list and a call to text with a string argument), and another call to div and its arguments.

What blew my mind here was not that div and h1 were functions - I’m familiar with doing that in R via {htmltools}. Joe Cheng makes the claim in his rstudio::conf(2022) talk that “R is a bizarrely good host language for Shiny” (R’s interactive web framework) because of the nature of positional and named arguments. It translates quite well to HTML.

But the Elm code doesn’t use named arguments (it doesn’t have those, though you could use a record as an argument and get that effect if you needed to); every function in the Html module that creates elements takes two arguments, either of which may be empty ([]); a list of attributes, and a list of child nodes.

So, class is also a function here, taking a string argument.

I suspect I’ve spent too long working with strings instead of types. The thing that represents a class should definitely be an object of type ‘class’, and the thing that represents some text should be of a different type - ‘text’. All of this strong typing gets around the all-too-common problem of passing in a value to an R function as a string and having it used in unexpected ways…

Spot the difference:

nchar("😀😀😀", "char")
## [1] 3
nchar("😀😀😀", "byte")
## [1] 12
nchar(c("😀😀😀", "char"))
## [1] 3 4
nchar(c("😀😀😀", "byte"))
## [1] 3 4

Enums get you a bit closer to not using strings to represent unique things, without going all-in on types, but R doesn’t really have enums that are useful in that way. I do wish it did.

One might have expected an error here - it might have saved a headache or two

## [1] 3
## [1] 4
## [1] 5
## [1] 5
## [1] 5

(to see why this happens, try entering the numbers into a console)

So, how would I write the HTML generating code in R?


body <- div(class = "content", 
            h1("Photo Groove"),
            div(id = "thumbnails",
                img(src = ""),
                img(src = ""),
                img(src = "")

<div class="content">
  <h1>Photo Groove</h1>
  <div id="thumbnails">
    <img src=""/>
    <img src=""/>
    <img src=""/>

If I ran browsable(body) in RStudio I’d get a rendered view of that HTML, i.e.

The rendered HTML
The rendered HTML

The construction of HTML attributes and child nodes gels nicely with R syntax.

Compiling the Elm code and inspecting the output I get exactly that HTML in the source, along with the reason it’s more complicated than that; the source contains the entire Elm runtime, needed for building interactive elements. Elm compiles down to Javascript, but since it’s strongly typed it prevents a lot of issues from ending up in the final product, in the same way that Typescript aims to.

If I wanted to interact with the images (Elm in Action walks through using these images as thumbnails and opening a larger version alongisde, depending on which one is clicked on) I’d need to write some Javascript into the body and/or use an entire runtime like Shiny to handle the effects.

I was a little shocked when I saw the paragraph

Back in the Wild West days of the web, it was common to store application state primarily in the DOM itself. Is that menu expanded or collapsed? Check whether one of its DOM nodes has class=“expanded” or class=“collapsed”. Need to know what value a user has selected in a drop-down menu? Query it out of the DOM at the last possible instant.

But… that’s how I thought it was supposed to be done. I’m not a front-end dev, that’s for sure. I suppose I’m also old.

‘The Elm Architecture’ involves passing messages around in a somewhat object-oriented way (at least reminiscent of smalltalk) but those messages are created and received by pure functional code - the runtime itself is not pure (and can communicate with the outside world via the browser) but that’s considered ‘tested’ and any code the user writes in Elm still follows the pure functional paradigm.

On top of all of that, it makes for (surprisingly?) super fast performance.

This also means that functions can be tested, since the functions will be free of side-effects. This is so well received that it’s endorsed by RATatui for building Rust terminal user interfaces.

I’m still early in the process of learning Elm, but I can definitely see that it’s a more beginner-friendly way to ease into Haskell, and who knows, maybe I’ll build a front-end to something. I’m curious if there’s any surface area for interop with some of the other languages I know.

I wasn’t expecting to be surprised by function layout quite so quickly, but I think that’s part of the benefit of learning several languages (once you’re very familiar with one to start with) - little differences with deep reasonings for them.

If you have comments, suggestions, or improvements, as always, feel free to use the comment section below, or hit me up on Mastodon.

## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.3.3 (2024-02-29)
##  os       Pop!_OS 22.04 LTS
##  system   x86_64, linux-gnu
##  ui       X11
##  language (EN)
##  collate  en_AU.UTF-8
##  ctype    en_AU.UTF-8
##  tz       Australia/Adelaide
##  date     2024-07-06
##  pandoc   3.2 @ /usr/lib/rstudio/resources/app/bin/quarto/bin/tools/x86_64/ (via rmarkdown)
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package     * version date (UTC) lib source
##  blogdown      1.18    2023-06-19 [1] CRAN (R 4.3.2)
##  bookdown      0.36    2023-10-16 [1] CRAN (R 4.3.2)
##  bslib         0.6.1   2023-11-28 [3] CRAN (R 4.3.2)
##  cachem        1.0.8   2023-05-01 [3] CRAN (R 4.3.0)
##  callr         3.7.3   2022-11-02 [3] CRAN (R 4.2.2)
##  cli           3.6.1   2023-03-23 [1] CRAN (R 4.3.3)
##  crayon        1.5.2   2022-09-29 [3] CRAN (R 4.2.1)
##  devtools      2.4.5   2022-10-11 [1] CRAN (R 4.3.2)
##  digest        0.6.34  2024-01-11 [3] CRAN (R 4.3.2)
##  ellipsis      0.3.2   2021-04-29 [3] CRAN (R 4.1.1)
##  evaluate      0.23    2023-11-01 [3] CRAN (R 4.3.2)
##  fastmap       1.1.1   2023-02-24 [3] CRAN (R 4.2.2)
##  fs            1.6.3   2023-07-20 [3] CRAN (R 4.3.1)
##  glue          1.7.0   2024-01-09 [1] CRAN (R 4.3.3)
##  htmltools     0.5.7   2023-11-03 [3] CRAN (R 4.3.2)
##  htmlwidgets   1.6.2   2023-03-17 [1] CRAN (R 4.3.2)
##  httpuv        1.6.12  2023-10-23 [1] CRAN (R 4.3.2)
##  icecream      0.2.1   2023-09-27 [1] CRAN (R 4.3.2)
##  jquerylib     0.1.4   2021-04-26 [3] CRAN (R 4.1.2)
##  jsonlite      1.8.8   2023-12-04 [3] CRAN (R 4.3.2)
##  knitr         1.45    2023-10-30 [3] CRAN (R 4.3.2)
##  later         1.3.1   2023-05-02 [1] CRAN (R 4.3.2)
##  lifecycle     1.0.4   2023-11-07 [1] CRAN (R 4.3.3)
##  magrittr      2.0.3   2022-03-30 [1] CRAN (R 4.3.3)
##  memoise       2.0.1   2021-11-26 [3] CRAN (R 4.2.0)
##  mime          0.12    2021-09-28 [3] CRAN (R 4.2.0)
##  miniUI 2018-05-18 [1] CRAN (R 4.3.2)
##  pkgbuild      1.4.2   2023-06-26 [1] CRAN (R 4.3.2)
##  pkgload       1.3.3   2023-09-22 [1] CRAN (R 4.3.2)
##  prettyunits   1.2.0   2023-09-24 [3] CRAN (R 4.3.1)
##  processx      3.8.3   2023-12-10 [3] CRAN (R 4.3.2)
##  profvis       0.3.8   2023-05-02 [1] CRAN (R 4.3.2)
##  promises      1.2.1   2023-08-10 [1] CRAN (R 4.3.2)
##  ps            1.7.6   2024-01-18 [3] CRAN (R 4.3.2)
##  purrr         1.0.2   2023-08-10 [3] CRAN (R 4.3.1)
##  R6            2.5.1   2021-08-19 [1] CRAN (R 4.3.3)
##  Rcpp          1.0.11  2023-07-06 [1] CRAN (R 4.3.2)
##  remotes 2023-07-18 [1] CRAN (R 4.3.2)
##  rlang         1.1.4   2024-06-04 [1] CRAN (R 4.3.3)
##  rmarkdown     2.25    2023-09-18 [3] CRAN (R 4.3.1)
##  rstudioapi    0.15.0  2023-07-07 [3] CRAN (R 4.3.1)
##  sass          0.4.8   2023-12-06 [3] CRAN (R 4.3.2)
##  sessioninfo   1.2.2   2021-12-06 [1] CRAN (R 4.3.2)
##  shiny 2023-10-14 [1] CRAN (R 4.3.2)
##  stringi       1.8.3   2023-12-11 [3] CRAN (R 4.3.2)
##  stringr       1.5.1   2023-11-14 [3] CRAN (R 4.3.2)
##  urlchecker    1.0.1   2021-11-30 [1] CRAN (R 4.3.2)
##  usethis       2.2.2   2023-07-06 [1] CRAN (R 4.3.2)
##  vctrs         0.6.5   2023-12-01 [1] CRAN (R 4.3.3)
##  xfun          0.41    2023-11-01 [3] CRAN (R 4.3.2)
##  xtable        1.8-4   2019-04-21 [1] CRAN (R 4.3.2)
##  yaml          2.3.8   2023-12-11 [3] CRAN (R 4.3.2)
##  [1] /home/jono/R/x86_64-pc-linux-gnu-library/4.3
##  [2] /usr/local/lib/R/site-library
##  [3] /usr/lib/R/site-library
##  [4] /usr/lib/R/library
## ──────────────────────────────────────────────────────────────────────────────

See also