Introduction

I started working on Music Monday awhile back. It is currently only a Slack bot which, when installed into a channel, selects a user at random to suggest a musical album to share. This blog post isn’t about the functionality, but the language change I made during implementation. Feel free to check it out though, I need beta testers.

The bot was originally implemented in Erlang. I really like Erlang. The language is quirky and simple. The BEAM is a gorgeous piece of technology to build applications on. However, I’m a static types enjoyer. I write Haskell professionally and I really really like having sum and product types. To summarize, I want strong static types and I want Erlang/OTP.

Enter Gleam. I want to go over some specific examples where I feel Gleam made my developer experience better.

Records

In Erlang, you can share records via header files, .hrl, e.g.,

-record(slack_channel, {id, music_monday_id, channel_id}).

I used this record to denote a return type from a database query in pgo. You can pattern match or access elements, e.g.,

-include_lib("{filename}.hrl").

#slack_channel{id = Id} = function_that_returns_slack_channel(),
SlackChannel = function_that_returns_slack_channel(),
Id = SlackChannel#slack_channel.id,

I don’t think this pattern is great across modules. You can give the fields types and they can be checked with dialyzer/eqwalizer. That just doesn’t provide me enough, I’m not a smart man. A compiler with expressive types, that are checked, and easily shareable across modules saves me a lot of stress and brainpower.

In Gleam, this record is defined,

import youid/uuid

pub type SlackChannel {
  SlackChannel(id: uuid.Uuid, music_monday_id: uuid.Uuid, channel_id: String)
}

I can import this type from anywhere via import path/to/module.{type SlackChannel}. I can use it qualified via import path/to/module with module.SlackChannel. It is easy to pass this type around and it works for both Erlang and Javascript targets.

Database Queries

With pgo, here is how I complete a unique insert into slack_channel

create_unique(ChannelId) ->
    #{command := insert, rows := Rows} = pgo:transaction(fun() ->
        pgo:query(
            <<
                "insert into slack_channel (channel_id)"
                " values ($1)"
                " on conflict (channel_id) do update"
                " set channel_id = excluded.channel_id"
                " returning id"
            >>,
            [ChannelId]
        )
    end),
    case Rows of
        [{Id}] -> {ok, Id};
        _ -> {error, impossible}
    end.

Is ChannelId a UUID or String? Is Id a UUID or String? Is this query even sound? In my Erlang application, I explicitly tested every query because of the number of mistakes I made. I could add -spec annotations to this to inform the reader but it doesn’t mean they are correct! Additionally, PostgreSQL already knows this information! Why not just let PG figure out the types and write the {ok, Id} and {error, impossible} logic ourselves. In Gleam, we can use squirrel for this. You add your query to a SQL file in the module tree and squirrel will autogenerate the requisite Gleam code for you! For the above query (with a slight modification) it will generate,

pub type CreateUniqueRow {
  CreateUniqueRow(id: Uuid)
}

pub fn create_unique(
  db: pog.Connection,
  arg_1: Uuid,
  arg_2: String,
) -> Result(pog.Returned(CreateUniqueRow), pog.QueryError) {
  // ... generated code here
}

In my production application I needed an additional Uuid to create a slack_channel (the internal Slack team identifier). This was partially why I rewrote the application which I’ll explain in the next section. Here, I need a Uuid and String to call this function and I’ll get back, effectively, Result(List(CreateUniqueRow), pog.QueryError). The pog.Returned type also has a count field. You need to understand what arg_1 and arg_2 are supposed to be, but the shape is generated automatically. You can, and probably should, create a usable API around this function. For example, the logic above expects only a single entry to come back from the query. Squirrel also provides helpful error messages when your queries are broken.

Refactoring

The initial Slack developer experience is okay if you are installing into a single Slack team, but Music Monday is intended to be installed in many Slacks. This requires OAuth credentials. I’ve not built applications like this before so I made an assumption my bot is basically the same entity across Slack but that isn’t true. Each team has its own credentials and even its own bot id.

When I built the Erlang application I was too tightly coupled with my development Slack team. I needed to do a huge refactor to support Slack team ids and OAuth credentials. In Erlang, there is no requirement to add dialyzer specs so… I didn’t. I was in a hell hole of refactoring with the tests I had (which actually was non-zero but far from full coverage) and debugging everything else at runtime. It was pain.

After a few hours of this, I had enough. You can say, “skill issue” or “bad tests and use of specs” and… you are right. To me, this is why strong static types are the way. I am forced to do this and the compiler will help me.

Using the example above, I added the slack_team table and modified the slack_channel table via migrations, re-ran gleam run -m squirrel, and ran gleam build. Now, all the places I need to change are revealed to me. No magic, no remembering. I just need to line up the new types.

This is true of internal blocks of code as well. In Erlang, when I pull out a chunk of code, I have to figure out what the spec is supposed to be. In Gleam, it was known before and it is known now. There is even a code action to do it.

Programming the Happy Path

Let’s start with an example using maybe from Erlang,

    maybe
        {~"text", Arguments} ?= proplists:lookup(~"text", Proplist),
        {~"channel_id", ChannelId} ?= proplists:lookup(~"channel_id", Proplist),
        {~"user_id", UserId} ?= proplists:lookup(~"user_id", Proplist),
        {ok, {slack_command, Arguments, UserId, ChannelId}}
    else
        none -> {error, <<"Unable to construct slack_command from_proplist">>}
    end.

Here, I’m trying to lookup some keys in a “proplist” (a list of key-value tuples). They all need to be present to succeed. If proplists:lookup succeeds it returns {Key, Value} if it fails it returns none. This API is actually quite friendly for maybe expressions, others aren’t.

First, the ?= syntax is saying, if the left side of the expression is a successful pattern match continue otherwise go to the else block and start pattern matching. Let’s imagine that an update to the proplists API caused proplists:lookup to error instead of none or {error, not_a_proplist} if you don’t provide a proplist. In either of those update scenarios, my pattern match would fail. Tools like dialyzer won’t catch this, but I believe other projects are working on full compilers for Erlang, e.g. https://github.com/etylizer/etylizer.

In Gleam, I just don’t have this problem because I am using a compiler with exhaustiveness checking. I have a few options for coding this in Gleam, e.g.

// Note: I could use type to distinguish between parse_query failure and lookup failure
// Nil is used for simplicity
case uri.parse_query(a_string) { // https://hexdocs.pm/gleam_stdlib/0.69.0/gleam/uri.html#parse_query
  Error(Nil) -> Error(Nil)
  Ok(a_proplist) -> {
    case list.key_find(a_proplist, "text"), list.key_find(a_proplist, "channel_id"), list.key_find(a_proplist, "user_id") {
      Ok(args), Ok(channel_id), Ok(user_id) -> Ok(#(args, channel_id, user_id))
      _, _, _ -> Error(Nil)
    }
  }  
}

I personally find the Result use style more readable, but I’ll elide that for simplicity. The key is that my pattern match has to be exhaustive, I couldn’t write,

    Ok(args), Ok(channel_id), Ok(user_id) -> Ok(#(args, channel_id, user_id))
    Error(Nil), Error(Nil), Error(Nil) -> Error(Nil)

the compiler will tell me I goofed up. I like that. Additionally, if I want to use some other error type the compiler will help me refactor e.g. if I wanted to use a validation monad (I had to sneak the ‘m’ word in here). Additionally, if the shape is updated I’ll get a compiler error. Note: be careful using blanket pattern matches, e.g. _, _, _ -> above, because you could miss API updates!

Adding a Front-end

There is only a small footprint of front-end code for Music Monday today. Essentially an install button, some frequently asked questions, and a page to describe how to use the bot after it is installed. However, if I want to do something more interesting there aren’t many friendly and maintained Erlang frameworks to build with. I think Nova looks interesting though.

The only times I have ever really enjoyed writing front-end are with Elm. Typescript is a tedious language and doesn’t really have a language server outside of VSCode. Editing in Helix with the LSP is basically useless. React’s hooks are getting easier for me, but the number of times I’ve had to think incredibly deeply about the runtime is brutal. I want a simpler language with a better editor experience. Enter Lustre.

It was very easy for me to get a server-side rendered application together. The types were easy to figure out, examples exist, and a decent amount of documentation is available. I can use the editor I prefer with actually useful LSP functionality. I already have a Tailwind Plus subscription so it was easy to drop the HTML into this converter (thanks Louis!) and get the Lustre representation.

Erlang/OTP

I’m not going to be able to convince you that Erlang/OTP rocks. There are better posts that cover that in way more detail. You are just going to have to believe me for now. With a combination of factory supervisors (in Erlang, simple-one-for-one supervisors) and crew I was able to introduce services and back-pressure into my system with little effort. Slack has team based API limits and I’d like to be able to build with this in mind.

Conclusion

Gleam gave me all the tools I needed to be successful. The language is simple and can be picked up quickly. The community is stellar and super helpful. You can build full-stack applications with one language. If you are looking for a strong statically typed language, check out Gleam. You’ll also, eventually, learn about Erlang/OTP which has really nice patterns to help build robust software.

Feel free to send me a direct message on BlueSky if you have any questions, corrections, or want to tell me I’m wrong. I’m always happy to learn new things.