A Tutorial Introduction to Gleam -- Part 6
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.
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.
Other Articles in Series
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 valueInt
from thecounter
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.