Stephen Hara

Thoughts on Gleam

Published on 10/30/2024

  • post
  • gleam

I recently tried out Gleam for a personal project I've been mulling about. I thought it'd be a fun experiment to try the cool new BEAM language, and I already like Elixir and Rust, so how weird could it be?

My Project

My project is very unambitious. I want to more or less make a super duper version of my net worth tracking spreadsheets, which mostly means:

  • consolidating the various categories of multi-national accounts so they're not separate tabs in an annual spreadsheet
  • consolidating the years so I don't have to have multiple spreadsheets or mess with hiding tabs
  • not using Google Sheets

There's a lot of cool stuff one could do with a net worth tracking project to make it worth selling, like automatic account sync, mobile apps, budgeting, and so on, but that's not really what I want to do for now. Easier spreadsheets is the only goal I have for now, hence the desire to experiment with Gleam.

The Stack

The stack I wanted to try out was Gleam on the backend and VueJS on the frontend, because I think Vue is pretty cool from when I have played with it, but it's been a while since I've had the chance.

The Gleam web framework of choice seems to be Wisp, which feels a lot like Elixir's Plug, Python's Flask, or Rust's warp.

More on Wisp

What I mean with those comparisons is that Wisp doesn't have much "big battery" magic you find in frameworks like Django, Phoenix, Spring, or Rails. In those frameworks, you can often do something like:

# endpoint code

def index(conn, params) do
  results = process_params(params)
  conn
  |> assign(:results, results)
  |> render(:index)
end

# router.ex

get "/", MyController, :index

And all the piping is done for you: the path "/" will execute our index function, the response has all the status codes and headers set, and you can think about the process_params function, or what you'll have for lunch that day.

By contrast, this is the Wisp routing example. It also includes the API it routes into, but you can see within those there's a decent bit of boilerplate, not to mention string_builder, a term that evokes depressing memories of university Java.

To be clear, I don't point this out as a good or bad thing! The nice thing about explicit code is it's pretty up front about what it's doing and it requires you to know what's going on if you want to put an abstraction on top of it.

And of course, those big frameworks I mentioned didn't start with all that magic - Elixir is now over a decade old and I first used Phoenix over 8 years ago, so Gleam isn't "a bad language" or "a bad ecosystem" for not having a wealth of handy and well-iterated libraries built up. Rather, I just think this is something you should know about if you decide to work with Gleam as a web backend.

The Nice Stuff

To start, let's talk about what I liked about Gleam!

For starters, the gleam CLI is really nice! gleam add for packages is wonderful, gleam run and gleam test Just Work, and you get some cool subcommands like gleam docs and gleam publish for those tasks.

The LSP being shipped with the language is also really cool. Adding it to Neovim was really simple:

require('lspconfig').gleam.setup({})

And then it Just Works. Hard to beat that!

The documentation is also very good. It even includes several cheatsheets, such as for Rust developers, to get you started quickly from other languages.

The use expression is really nice. It reminds me a lot of Elixir's with and Rust's ?.

Overall, I generally like the syntax, and the tooling and docs are great! Types are always nice to have, and I really enjoyed the start of writing my backend.

type: Probable Skill Issue

When I start writing code, I like to write types early. I did this before really figuring out Gleam's type keyword, which is a little different from the ones I'm used to, and that led to some confusion.

Basically, Gleam's type keyword is kind of like structs and enums at the same time:

// kind of enum-y
pub type Department {
  HumanResources
  Accounting
  ...
}

// kind of struct-y
pub type Employee {
  CEO(name: String)
  Director(name: String, department: Department)
  ...
}

But it also really isn't, and this can feel a bit weird.

For starters, the types here, Department and Employee, are identifiers separate from the things inside. Importing them doesn't give you access to the things inside implicitly, only the type for annotation:

import company/org.{type Department}

let my_dept = Department.Accounting // compile error
let my_dept = Accounting // compile error
let my_dept: Department = Accounting // compile error unless you also import the "Accounting" atom

In general, they're pretty similar to Rust enums, but it's a little different in terms of imports. In Rust, importing the enum lets you use the variants like they're in the namespace of the enum, but in Gleam, you have to import the variants directly if you do any specific imports.

// main.rs
use company::org::Department;
let dept = Department::HumanResources;

// main.gleam
import company/org.{HumanResources}
let dept = HumanResources

As in Rust, enum types with fields are called variants. In the above Employee example, we get two variants as shown: one for CEO who only gets a name, and one for Director where they have a name and a department.

JSON

To illustrate this example, I'm going to pull a couple snippets from an article posted in April on building a web app with Wisp and Lustre. The article is very good, and I highly recommend giving it a read if you're interested in what the actual code looks like for a Todo app in full-stack Gleam!

Like it or not, JSON is a pretty all right default for inter-app communication. Gleam does have gleam_json available like the article:

let decoder =
  dynamic.decode3(
 ItemsJson,
 dynamic.field("id", dynamic.string),
 dynamic.field("title", dynamic.string),
 dynamic.field("completed", dynamic.bool),
  )
  |> dynamic.list

And to encode to a JSON string:

fn item_to_json(item: Item) -> String {
  json.object([
    #("id", json.string(item.id)),
    #("title", json.string(item.title)),
    #("completed", json.bool(item.item_status_to_bool(item.status))),
  ])
  |> json.to_string
}

Partly, I'm lazy. But that's a lot of typing compared to using Serde in Rust where you just slap #[derive(Serialize, Deserialize)] on your struct of native types and call it a day. ¯\_(ツ)_/¯

What really gets me wondering if Gleam is a great choice now for my project as a web app with separated front- and back-ends, though, is that you have to do this for every type you define. It's an odd bit of friction - you of course want to codify as much of your application logic into your types as you can, but any types that cross the wire need a field-by-field json.object call. There doesn't seem to be an easy way to turn a set of atoms from an enum-y type into a string easily, which means you have to do it. Not ideal for lazy folks...

I did find this video showing how to use Elixir interop to decode JSON, and there's a library called gserde (last commit from 6 months ago) that generates JSON translation functions you can then import and use pretty easily.

I think what I want is to not need a code generation library or FFI to do the obvious translations. But that could just be, again, that I'm lazy.

Onward and Upward

For my project, I might try something else like Kotlin, or maybe I'll give Rust a try for web. There's just a bit too much friction for using Gleam as the backend for my tastes.

Should you use Gleam? I think it depends! The creator Louis Pilfold streams on Youtube once or twice a week doing things like grep from scratch, web apps (I should watch some of those replays...), and concurrent programming, all in Gleam, to give you an idea of what the guy who made it is using it for. Also, I asked him if he'd give this post a read to make sure I wasn't blatantly wrong, and he graciously did so and gave me great feedback!

I get the sense it's really nice as a tools language, so if I have an idea for some kind of tool-y project I'll probably give Gleam another go. I'm also curious to see how well it works to go full Gleam for a web app and use Lustre instead of Javascript for the frontend.

It's a little young still, as expected. There's no Rails or Phoenix or Django or NextJS or Unreal Engine yet. I don't like the JSON story as I experienced it (probably skill issue tbh). I think the docs are really good, but some of the hurdles I encountered trying to figure out the type definitions could be explained better.

But on the other hand, that means there's a lot of frontier to be explored! Someone can go and write a more streamlined JSON library, or the next full-stack framework, or update the docs for types (though I might do this so give me a couple weeks before you, dear reader, try that one!).

Some Reading/Viewing For You

I don't have any comments for these links, just that they're Gleam-related and if you're interested in Gleam, they're probably worth a look!