Introduction

This is the sixth in a series of articles exploring the Gleam programming language. In this one, I’m setting aside my little password cracking project to look at gleam for frontend development.

I love Rescript for frontend development, it’s a very practical functional programming language, and I’ve written a lot on the topic. So this article will also be a bit of a comparison of Gleam and Rescript. This isn’t really a fair comparison as Rescript is a mature language, and Gleam’s Javascript support is super brand new. I do not expect Gleam to supersede Rescript as my favourite frontend language, but stranger things have happened.

Table of Contents

Patreon

If you find my content valuable, please do consider sponsoring me on Patreon account. I write about what interests me, and I usually target tutorial style articles for intermediate to advanced developers in a variety of languages.

I’m not sure how far I’m going to go with this series on Gleam, so if you’re particularly interested in the topic, a few dollars a month definitely gives me a bit of motivation to keep it up.

I’ve collected links to the articles in this series here.

Let’s begin

Start by creating a new gleam project:

gleam new js_gleam

Next, edit the gleam.toml. Add a top level line indicating that we are targeting Javascript output:

name = "js_gleam"
version = "0.1.0"
target = "javascript"

[dependencies]
gleam_stdlib = "~> 0.22"

[dev-dependencies]
gleeunit = "~> 0.6"

This was all autogenerated, I just added the target = "javascript" line. This is important: gleam_stdlib and gleeunit work exactly the same under erlang and gleam. I didn’t have to change those dependencies. And the default generated gleam source file doesn’t have to be changed either. As a reminder, the template generated js_gleam.gleam looks like this:

import gleam/io

pub fn main() {
  io.println("Hello from js_gleam!")
}

In an earlier article we generated Beam (erlang-compatible) bytecode from this file. Now we can use the following command to generate Javascript instead:

gleam build

The generated js file is kind of buried, but you can find it at build/dev/javascript/js_gleam/dist/js_gleam.mjs. It looks like this:

import * as $io from "../../gleam_stdlib/dist/gleam/io.mjs";

export function main() {
  return $io.println("Hello from js_gleam!");
}

As we can see, Gleam is not zero runtime. It ships with a Javascript library that has to be imported to do something as basic as console.log. I’m not too concerned about this (Rescript does similar). I minified the js files in build/dev/javascript/gleam_stdlib/dist/gleam and found that they total to 132 kb. Definitely not free, but unlike the standard libraries I’ve seen with various Python to Javascript attempts over the years, it’s at least manageable.

Try out gleam run while you’re at it. I honestly don’t know what that does; I guess it’s spinning up nodejs to run the compiled JS file since if I run htop with a filter for node a process briefly flashes into view when I run gleam run.

ViteJS

I love ViteJS. It’s fast. Fast is good. My most popular Rescript article is about setting up Vite with Rescript. I’ll be doing a very similar process to get gleam running with ViteJS in this one, but be aware that Gleam does not have anything resembling JSX, so we’ll be using React the functional way. I’m not actually sure how that’s going to look, so as usual, I guess we’ll find out together.

The tricky bit is going to be marrying the JS build system with the Gleam one; we need both. One of my chief complaints with Rescript is having to maintain bsconfig.json and package.json; (The Rescript team is improving this, btw), but because Rescript is designed to work with Javascript it’s relatively straightforward.

The Gleam build tooling only knows how to generate javascript files; it doesn’t know anything about npm, nodejs, vite, esbuild… (I could go on and on and on here since the JS packaging ecosystem is an absolute disaster. It’s like they looked at the way Python does things and said, “This is so confusing, I bet we can be even more confusing!“), so we’re going to have to work for it.

Let’s start by overlaying a vite project over our existing gleam project. Vite seems to want to create a new directory from scratch for us, so create a new project and move the files to the current (gleam base) directory:

yarn create vite js_gleam
mv js_gleam/* .
cat js_gleam/.gitignore >> .gitignore
rm -r js_gleam

Now you can install the vitejs depenedencies and run the sample devserver:

yarn
yarn dev

I can now see the sample vite app on http://localhost:5173/ as outputted on my console.

Make it Gleam-aware(ish)

By default, Vite expects imports to be relative to the current working directory. This can be configured, but I like keeping things explicit. We know that gleam puts the generated code in build/dev/javascript/js_gleam/dist/. To get the plumbing working, let’s pretend that the two js files created by the Vite template were actually created by Gleam. Move main.js and counter.js to that directory, changing the file extension to match what Gleam gave us:

mv main.js build/dev/javascript/js_gleam/dist/main.mjs
mv counter.js.js build/dev/javascript/js_gleam/dist/counter.js.mjs

Then edit index.html to point the script module at this new source location:

<script type="module" src="/build/dev/javascript/js_gleam/dist/main.mjs"></script>

You’ll also have to edit main.mjs to change three imports. The first two imports need to have the . removed from the import since they are no longer relative imports. The counter.js import needs to stay relative, but the extension has changed from .js to .mjs:

import '/style.css'
import javascriptLogo from '/javascript.svg'
import { setupCounter } from './counter.mjs'

Run yarn dev to make sure nothing is broken.

Generating the Javascript files from Gleam source code

The trick now is to make gleam generate the Javascript files instead of manually placing them in there. Let’s start with counter.mjs. Create a new src/counter.gleam file. Copy the contents of the old `counter.mjs file into it (for reference) and comment them out:

// export function setupCounter(element) {
//   let counter = 0
//   const setCounter = (count) => {
//     counter = count
//     element.innerHTML = `count is ${counter}`
//   }
//   element.addEventListener('click', () => setCounter(++counter))
//   setCounter(0)
// }
//

Leave yarn dev running in one terminal and open another one. Run gleam build. Everything should crash because your counter.mjs got replaced with:

export {}

If you have an error about setupCounter not found in your console, then you know Vite is hearing what Javascript is saying. It just doesn’t understand it.

We can fix that by creating a Gleam version of setupCounter. Gleam doesn’t like camel case though, so we have to change it to setup_counter:

import gleam/io

pub fn setup_counter() {
  io.println("setup_counter called")
}

Save the file and run gleam build. It should replace your counter.mjs file contents with:

import * as $io from "../../gleam_stdlib/dist/gleam/io.mjs";

export function setup_counter() {
  return $io.println("setup_counter called");
}

As an aside, I like that it is generating perfectly readable JS, so far. This is one of the reasons I really appreciate Rescript; the JS looks like it was written by a good JS coder. For this very simple example, Gleam seems to be doing similarly well.

But there is still an error message on the console. Let’s hand edit the original main.mjs file and change the setupCounter and its call site to setup_counter:

import '/style.css'
import javascriptLogo from '/javascript.svg'
import { setup_counter } from './counter.mjs'

document.querySelector('#app').innerHTML = `
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
      <img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
    </a>
    <h1>Hello Vite!</h1>
    <div class="card">
      <button id="counter" type="button"></button>
    </div>
    <p class="read-the-docs">
      Click on the Vite logo to learn more
    </p>
  </div>
`

setup_counter(document.querySelector('#counter'))

If you let vite reload, it should indicate that setup_counter called on the console.

Writing some Gleam code

The next step is to port the setupCounter function to gleam. The JS function isn’t terribly complicated, but we have a couple wrinkles to deal with:

  • We need a type for the element that gets passed in. So we’ll need to dig into Gleam’s external types module.
  • We need to call a method (addEventListener) on that type, but Gleam is not an object oriented language, so we need to map that to a functional paradigm.
  • We need to store permanent state for how many times the button was clicked, but Gleam does not have any concept of variable mutation or globals.

Let’s tackle the external type first. Create a new src/element.gleam file to house the element related stuff. We’ll keep it simple, but if you were modeling all the functions on a Javascript Element it would be pretty big. I expect that at some point, there will be Gleam packages housing these bindings, but I wasn’t able to find any yet.

Defining an external type in Gleam is super simple:

pub external type Element

This allows us to modify setup_counter() so that it can accept an element type. Import the new element module and change the function signature to accept an element. We were already passing the element in from main.mjs, so this should just work. Add a io.debug(element) to prove it works:

import gleam/io
import element

pub fn setup_counter(element: element.Element) {
  io.println("setup_counter called")
  io.debug(element)
}

If you gleam build and then yarn dev, you should see a //js(HTMLButtonElement {}) log line now.

Next, we need to model the Element.addEventListener. We will create this as a gleam function, where the first argument is the element itself. The problem is, Gleam doesn’t seem to know how to translate this to a method call that “excludes” that first argument. In Rescript, I would use the @send external syntax to instruct the language to treat the first argument as a this.

Since Gleam doesn’t support this, we need to jump through some hoops to get the appropriate callable function. The Reflect Javascript API, has a get interface that allows us to extract any property from an object. I think with Gleam, you’ll find yourself binding to it a lot, since you’ll be giving it different types depending on which function will be called.

I’m not sure this is the best way to do this, but here’s the external function definition to get the method (warning: It’s complicated!):

external fn get_add_event_listener_method(
  element: Element,
  property: String,
) -> fn(String, fn() -> Nil) -> Nil =
  "" "Reflect.get"

The external fn keywords tell Gleam that we are binding to an external function. The two strings at the end ("" "Reflect.get") tell it what module the function we are trying to bind to lives in (an empty string in this case, since Reflect is available globally), and what string to use for the calling syntax: Reflect.get in this case.

The rest of the definition looks like a normal Gleam function signature. The function accepts two arguments. In Javascript, Reflect.get can look up any property on any object, but in this binding, we are telling Gleam we only want it to work on the Element type.

Technically, the only property string this function should accept is addEventListener, but I don’t think Gleam has a way to enforce that. However, notice that this function is not defined as pub external, so I have full control of how it is called.

The return value looks pretty complicated, but that’s just because addEventListener is a function that accepts a string and function. If we queried for a different property, it would be a different return type, as we’ll see with innerHTML shortly.

Calling the bound function

As I said, this function isn’t public because we don’t want just anybody to be able to call it with the wrong strings. I barely even trust myself with that power, let alone myself when I’m editing a totally different file! Instead, we’ll write a simple facade accepts the relavent arguments, looks up the function, and calls it:

pub fn add_event_listener(
  element: Element,
  event: String,
  callback: fn() -> Nil,
) -> Nil {
  let method = get_add_event_listener_method(element, "addEventListener")
  method(event, callback)
}

This gives us an ergonomic interface to access element.add_event_listener with a similar structure to what we would use in Javascript. In fact, I can now modify setupCounter as follows:

import gleam/io
import element.{Element, add_event_listener}

pub fn setup_counter(element: Element) {
  add_event_listener(element, "click", fn() { io.println("click") })
  io.println("setup_counter called")
}

Notice that I changed the import to the funky Gleam syntax that pulls Element and add_event_listener directly into our current namespace. If I hadn’t done this, I’d have element.add_event_listener(element... which would be pretty confusing. The first element in that line would be referring to the element.gleam module and the second would refer to the element name. I might have been wiser to change the name, but with Element being such a common object in web development, it kind of makes sense to have it available in the namespace.

Now if you gleam build and yarn dev, you should see click show up in the console every time you click the empty button.

Adding some state

The original Javascript implementation of setupCounter has a mutable counter variable that gets incremeted on each click. This doesn’t map well to Gleam’s purely functional design. There are a few ways we could evolve the next button state from the current button state (the Gleam blog article where JS support was first introduced illustrates one of them), but I wanted to keep my gleam code as close to the original JS code as possible.

So let’s learn about mutable references. If you’ve ever coded in Rust, this phrase probably strikes fear into your heart, because mutable references in Rust mean you are about to pick a fight with the borrow checker…

Gleam/JS mutable references aren’t that bad. However, they aren’t built into the language, so the first thing you’ll need to do is gleam add gleam_javascript. Then add the following import to your counter.gleam file:

import gleam/javascript.{dereference, make_reference, set_reference}

These three functions allow you to have mutable state in your JS app. While functional languages frown on mutable state, it’s really not a problem when working with Javascript since the virtualmachine is already designed to protect you from all the possible foot guns that come with mutable state. So mutable references in this case are just a bunch of ugly syntactic sugar to allow us to use Javascript the way it was designed.

Note: It’s not really fair to say that Javascript was designed. It kind of just happened organically, in much the same way a pile of manure happens organically. So “the way it was designed” should never be used as a reason to use it that way.

Anyway, this referencing capability allows us to write a setup_counter that looks and works similar to the original setupCounter. As a reminder, here’s how the Javascript vite gave us looked:

// export function setupCounter(element) {
//   let counter = 0
//   const setCounter = (count) => {
//     counter = count
//     element.innerHTML = `count is ${counter}`
//   }
//   element.addEventListener('click', () => setCounter(++counter))
//   setCounter(0)
// }

Here’s the comparable Gleam code:

pub fn setup_counter(element: Element) {
  let counter = make_reference(0)
  let set_counter = fn(count: Int) { set_reference(counter, count) }
  add_event_listener(
    element,
    "click",
    fn() {
      set_counter(dereference(counter) + 1)
      io.debug(dereference(counter))
      Nil
    },
  )
  io.println("setup_counter called")
}

We use make_reference to initialize the counter variable to 0. Then we create a new anonymous function that can set the counter to a given value. That function calls set_reference to manipulate the variable. (Yeah, we don’t have the innerHTML thing set up yet; it’s coming. Let’s stick to one thing at a time, ok?)

Then the add_event_listener callback is set up to do a few things:

  • use dereference() to extract the current value Int from the counter reference
  • use set_counter to set a new value based on that current value
  • use dereference() again to get extract that new current value
  • use io.debug to put the value on the console
  • return Nil

We’ll simplify this callback shortly, after we have a better way of reporting the current value to the user.

Updating the button text

The original JS code was configured to set the innerHTML attribute on the button on every click. We need to add that, but first we need to tell gleam that innerHTML is a thing. And that it can be set.

Let’s go back to element.gleam and add a new binding to Reflect.set:

external fn set_property(
  element: Element,
  property: String,
  to: anything,
) -> Bool =
  "" "Reflect.set"

This is a bold function. It allows us to set any property to any value. The anything “type” is not actually a keyword; it just tells gleam that the type is generic. You could have put any lowercase word there. Try puppy, for example. I don’t like this, to be honest, because it makes typos really easy. The difference between a function that accepts String and one that accepts a generic type named string is a single keypress, but they do very different things.

Since it’s pretty dangerous, this function is not made public. Instead, we expose a function that specifically sets the innerHTML property on the underlying Javascript Element:

pub fn set_inner_html(element: Element, to: String) -> Nil {
  set_property(element, "innerHTML", to)
  Nil
}

Nothing too exciting there. Note that I explicitly return Nil. Otherwise this function would implicitly return the return type of the most recently called function, and set_property returns a boolean value (Reflect.set “Returns a boolean that is true if the update was successful.” to quote MDN).

Don’t forget to add set_inner_html to the import element.{...} line in your counter.gleam file.

Now we can change the set_counter function (defined inside setup_counter) to also set the element’s innerHTML field. This is more verbose than you’d expect because our counter needs to be converted from an integer to a string and appended to some explanatory text before being passed into our new set_inner_html function. This juggling can be made quite clear using a pipeline and the function capturing syntax we discussed in the previous article:

  let set_counter = fn(count: Int) {
    set_reference(counter, count)
    count
    |> int.to_string
    |> string.append("count is ", _)
    |> set_inner_html(element, _)
  }

Now we can simplify our add_event_listener call since we don’t need to output any debugging info on the console anymore, and don’t forget to initialize the button text with a set_counter(0) call. Here’s the entire setup_counter function ported from Javascript:

pub fn setup_counter(element: Element) {
  let counter = make_reference(0)
  let set_counter = fn(count: Int) {
    set_reference(counter, count)
    count
    |> int.to_string
    |> string.append("count is ", _)
    |> set_inner_html(element, _)
  }
  add_event_listener(
    element,
    "click",
    fn() { set_counter(dereference(counter) + 1) },
  )
  set_counter(0)
}

Now port main.mjs

The main.js vite created for us is pretty simple, but it has a couple wrinkles we need to work out. Here’s the whole generated Javascript file (already modified for the new setup_counter syntax, for reference:

// import '/style.css'
// import javascriptLogo from '/javascript.svg'
// import { setup_counter } from './counter.mjs'

// document.querySelector('#app').innerHTML = `
//   <div>
//     <a href="https://vitejs.dev" target="_blank">
//       <img src="/vite.svg" class="logo" alt="Vite logo" />
//     </a>
//     <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
//       <img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
//     </a>
//     <h1>Hello Vite!</h1>
//     <div class="card">
//       <button id="counter" type="button"></button>
//     </div>
//     <p class="read-the-docs">
//       Click on the Vite logo to learn more
//     </p>
//   </div>
// `

// setup_counter(document.querySelector('#counter'))

Vite needs the js file to import both the styles and the svg in order for it to know to include it in the public assets. But Gleam isn’t aware of such constructs.

In Rescript, I’d probably just map this to {% raw syntax, but Gleam doesn’t currently have a way to drop to raw Javascript. Luckily, we can abuse the external function notation to make Gleam create imports for these files for us.

Remember how external functions need two strings? We left the first one blank when we did the Reflect API since it is already in the Javascript global namespace. However, if it’s not blank, gleam adds an import for us. So let’s define a couple dummy functions to import style.css and javascript.svg:

external fn load_styles() -> Nil =
  "/style.css" "default"

external fn load_logo() -> Nil =
  "/javascript.svg" "default"

If we run gleam build, this generates the following Javascript:

import { default as load_logo } from "/javascript.svg";
import { default as load_styles } from "/style.css";

Technically we don’t need the load_styles function to be named, but it’s good enough.

The next problem we have to address is that Gleam doesn’t allow top-level or global code that runs immediately on import. So we’re going to need a main function and somehow call it from the index.html file. This is easier than it sounds. First, create a simple main function in main.gleam:

pub fn main() {
  io.println("I am Loading...")
}

Can’t get much simpler than that. Now we have to modify the script tag in index.html to import and call this instead of loading the file via src. As a reminder, it currently looks like this:

    <script type="module" src="/build/dev/javascript/js_gleam/dist/main.mjs"></script>

Take out the src and move the import into the body of the tag, as follows:

    <script type="module">
      import {main} from  "/build/dev/javascript/js_gleam/dist/main.mjs"
      main()
    </script>

Now if you run gleam build and reload your vite server, you should get I am Loading... on the javascript console. That went…. better than I expected.

Next we need to model document.querySelector. Eventually I’m sure there will be Gleam bindings to the Javascript document, but there’s nothing available at the moment. Create a new document.gleam file as follows, taking advantage of the fact that some incredible people (you and I) wrote bindings to the return value (an Element) earlier in this article:

import element

pub external fn query_selector(selector: string) -> element.Element =
  "" "document.querySelector"

Oh you think we’re clever don’t you? Sorry, but that return value is actually wrong. Can you see why? The problem is, document.querySelector can return null. And Gleam doesn’t have null! I tried converting it to option.Option(element.Element), but Gleam doesn’t generate any magic code that knows how to coerce null to option.None. Gleam’s Javascript support is super young, and I’m guessing this case, even as common as it is, just isn’t handled yet.

For now, I’m just leaving the code broken as above. Just make sure the selector you are querying exists and you’ll be all right! ;-)

Speaking of querying the selector, let’s make our main() function a little more interesting (Don’t forget to import gleam/io):

pub fn main() {
  document.query_selector("#app") |> io.debug
}

Build and run this and see that the appropriate HTMLDivElement is printed to the console.

The bulk of the old main.mjs file was setting the innerHTML on the selected element. That’s just a string, but we sadly can’t copy it wholesale. For one thing the syntax for a multiline string in gleam is a quotation mark, and all the HTML attributes in the original string are indicated with quotation marks, so you need to add a bunch of escapes. You could use \", but I just replaced all the quotes with apostrophes, since that’s still valid HTML.

The more difficult thing is that in the original JS, this is actually thing, it is a template string, including the ${javascriptLogo} in it. Gleam currently thinks load_logo() is a function even though it’s actually a string, because of the abuse we put on he external fn declaration. Gleam has a external type definition, but it creates opaque types, and can only work with external functions. As far as I can tell, there’s no way to write gleam code that can import the default value from a module as a string instead of a function.

Like I said, JS support is very young. Practically newborn, in fact.

I’m getting kind of bored, so I’m not going to fully solve this problem. Sorry! For now, replace ${javascriptLogo} with /javascript.svg. This will work with your yarn dev dev server, but if you use yarn build to build production files from the site, you’ll probably be disappointed because vite will generate funny names for all the static assets.

Anyway, if you change your main code to pipe the document.query_selector to element.set_inner_html with the modified text, it should load properly in the devserver (Don’t forget import element):

pub fn main() {
  document.query_selector("#app")
  |> element.set_inner_html(
    "
  <div>
    <a href='https://vitejs.dev' target='_blank'>
      <img src='https://dusty.phillips.codes/vite.svg' class='logo' alt='Vite logo' />
    </a>
    <a href='https://developer.mozilla.org/en-US/docs/Web/JavaScript' target='_blank'>
      <img src='https://dusty.phillips.codes/javascript.svg' class='logo vanilla' alt='JavaScript logo' />
    </a>
    <h1>Hello Vite!</h1>
    <div class='card'>
      <button id='counter' type='button'></button>
    </div>
    <p class='read-the-docs'>
      Click on the Vite logo to learn more
    </p>
  </div>
",
  )
}

The last thing we need to do is add the call to the setup_counter function we defined so long ago (with `import counter.{setup_counter}):


  document.query_selector("#counter")
  |> setup_counter

And… that’s it! If you run the vite devserver, your counter button should do the count thing it’s supposed to do, all written in Gleam code.

Summary

If you want to do production-grade functional programming in Javascript, stay away from Gleam. It is a lovely language, but it’s very much not ready. Go over to my Rescript series instead.

On the other hand, if you want to be in on the ground floor of a new programming language, there is a whole pile of low hanging fruit in Gleam’s Javascript engine. Just writing complete bindings to `document would be a good start! My interactions with the (small) Gleam community on their github discussions have been very positive, and I encourage you to join the fun.

I can imagine a future where Gleam becomes popular for both frontend web and backend service development. I don’t see this happening with Rescript, because Rescript only compiles to Javascript, and while nodejs is far more popular than it deserves to be, I can’t recommend it for serious service development. It is just too dumb about concurrency.

Erlang, on the other hand, is very smart about concurrency, but you have to do horrible things to your brain to be able to program in it effectively. Using Gleam instead could save a lot of sanity and permit building extremely resilient services. Note that I’m going off heresay here; I’ve never done the horrible things to my brain so I don’t know if Erlang is as good at concurrency as everyone says it is.

However, I can also imagine a future where Gleam development peters out without gaining much traction. I don’t think my articles will change that outcome much, one way or the other, but I’ve had fun writing them, and I hope you’ve enjoyed reading them.

This ends my discussion of Gleam, at least for the moment. I’m not intending to get deeply involved in its development at this time. I’ll probably take another look at it in a couple years, just to see what has happened.

Next up, I’m going to tackle an even younger and stranger language called Vale.