Gleam is my new obsession

2 hours ago 1

I love Rust, but...

In my previous blog post I stated that I started defaulting to Rust for my personal projects. While I really like Rust for its type-system, I'm not a huge fan of its learning curve. There is definitely a bit of a sunk-cost with a dash of Stockholm syndrome whenever I say I love Rust.

I'd often joke, "I love Rust, but what I really want is a simpler Haskell with a C-like syntax" or "I love Rust, but if it had a garbage collector, Rust would be my perfect language".

If I was being honest, what I really love about Rust is that it is a popular language with algebraic data types. Any language that doesn't have sum types to express exclusivity has an incomplete type system, in my opinion. The other benefits of Rust, its safety and speed, are secondary to being able to use algebraic data types to express business logic and state.

Without going too deep, there are two main concepts to algebraic data types: product types, and sum types. Product types group types together into a single type. These are your structs in Rust, Go, C, etc.

Sum types, on the other hand, are a type that can only be a finite set of values. The boolean type is an example of a built-in sum type in most languages. It can only be true or false and nothing else. Having the ability to write your own sum types opens up a world of expressibility.

For instance, I use a sum type in a POTA application that I wrote that expresses the possible messages that can be sent to the state controller thread from the UI thread.

#[derive(Debug)] enum ControllerMessage { SetFrequency(FrequencyHz, pota::Mode), RefreshSpots, }

Given this type, I can then pattern match on all the possible messages that can be received by the state controller. If I forget one, the compiler will tell me that I need to add it.

fn handle_message( handles: &Handles, app_sender: Sender<AppMessage>, msg: &ControllerMessage, ) -> Result<()> { match msg { ControllerMessage::SetFrequency(freq, mode) => { // ... do something with this message } ControllerMessage::RefreshSpots => { // ... do something with this message } }

It is possible to emulate this kind of expressibility in languages without sum types, but it tends to lead to really awkward and verbose code. For example, look at this go code using an enum. Languages without proper sum types are typically limited to having a finite set of integer or sting values rather than richer product type variants.

If you'd like to deep dive into the benefits of Algebraic Data Types, I'd like to point you to three articles:

  1. Parse, don’t validate
  2. Designing with types: Making illegal states unrepresentable
  3. The Cardinal Rules of Rust - Understanding Type Cardinality for Flawless Data Models

Erlang that strange Nordic child

A decade ago, I fell hard for a language called Erlang. I was going to Ricon every year. I gain a tiny bit of Twitter fame writing a benchmark testing Erlang's concurrency.

Erlang was where I fell in love with pattern matching. The fact that I can deconstruct any Erlang value of arbitrary depth is amazing. Python and JavaScript recently gained this ability, but back then it felt revolutionary. Erlang's binary pattern matching also feels like a superpower.

-define(IP_VERSION, 4). -define(IP_MIN_HDR_LEN, 5). DgramSize = byte_size(Dgram), case Dgram of <<?IP_VERSION:4, HLen:4, SrvcType:8, TotLen:16, ID:16, Flgs:3, FragOff:13, TTL:8, Proto:8, HdrChkSum:16, SrcIP:32, DestIP:32, RestDgram/binary>> when HLen>=5, 4*HLen=<DgramSize -> OptsLen = 4*(HLen - ?IP_MIN_HDR_LEN), <<Opts:OptsLen/binary,Data/binary>> = RestDgram, ... end.

This looks cryptic, but it is deconstructing the fields of an IP packet. 4 bytes followed by 4 bytes, followed by 8 bytes, etc. Each chunk is stored in a variable and decoded as an unsigned integer.

You can specify the signedness, the endianess, the bit size, and type of each chunk. I'm sure someone has implemented it in Lisp, but I don't think I've ever seen anything quite like this in another language outside the Erlang ecosystem.

Erlang also has what I think is the most sensible concurrency model that exists, the Actor model. Sure, threads and green threads are easy to conceptualize, but they're too primitive. How data is shared is left as an exercise for the programmer.

The Actor model allows you to write code in a single threaded way, but communicates via messages. Each little unit of concurrency only cares about its own little world and state. Sharing state is done via immutable messages.

Messages come in, and the actor reacts to them. They update their state as a response. Erlang's actors feels like microservices done right.

Actors are like little autonomous robots coordinating with each other to build a community. Erlang applications feel alive and cooperative. This feels very natural to me as a services oriented developer.

Built on top of Erlang's actor model is a concurrency framework call the Open Telecom Platform (OTP). It is a framework for building very resilient, concurrent systems in a uniform way. It defines how applications are structured, how to spawn actors, and most importantly how to keep the whole thing running in the event of failures.

If a language doesn't have actors, I tend to emulate them with threads and queues. If you look at the code for the POTA app that I mentioned before, it is structured like two actors communicating with messages. I find reactive applications are easier to reason about if I decouple the concerns of the UI from the business logic.

As amazing as Erlang is, I couldn't convince any employer to let me deploy Erlang into production. It is just too strange to them. I wish it wasn't true, but as superficial as it is, the syntax really puts people off.

Speaking of syntax, I remember seeing Elixir for the first time at a mini Erlang conference put on my Basho in Reston, Virginia. It looked very appealing. It was the early days of Elixir. If my memory is correct, that conference may have been the first time Elixir was shown off at a public event. (My memory is probably not correct)

If you're not familiar with Elixir, it essentially puts a Ruby-like coat of paint on top of Erlang's virtual machine, the BEAM.

I watched Elixir from afar over the years. I watched Phoenix hit the stage. I saw post after post of people praising Phoenix' LiveView ability to easily build reactive web applications with little to no JavaScript.

I really wanted to like Elixir, and I have been tempted to apply to the occasional Elixir job posting so many times, but I had also moved away from dynamically typed languages by then.

I had been burned by Python's runtime errors so often that I was done with dynamic types. It was just too hard for a team of developers to keep types straight. I've learned the hard way that relying on discipline is a poor way to write maintainable code. We need automation to help us in our moments of weakness.

I'll admit now, that if I didn't have the aversion to Elixir's dynamic type system, I might have been writing Elixir for the last decade instead of Go. Though I don't know if I would have learned to appreciate Go's biggest strength, its simplicity.

The simplicity of Go

Go's simplicity is its biggest strength and its biggest weakness. I'll spare you from another blog post complaining about having to write if err != nil over and over.

However, after being awoken to the existence of Option and Result types, it is like a pin prick every time I see a nil pointer dereference panic, or I have to manually check the presence of an error.

Whenever I spawn a long-running goroutine, I dream about implementing an ad hoc, informally-specified, bug-ridden, slow implementation of half of OTP in Golang.

Whenever I create an enum in Go, I dream of sum types and pattern matching.

Despite my gripes about Go, I've have come to love its simplicity. Since it is so easy to pick up, not knowing Go is rarely a blocker for a new hire. In addition, aside from having to guard against nil the foot guns are low.

There is often one obvious way to do things. If there is not an obvious way of doing something, there's an extensive document on how to write idiomatic Go. A culture of linting and automatic formatting has reduced so much bike-shedding.

However, I struggle to crown Go as my favorite programming language despite its simplicity. I honestly find it really cumbersome to write. Its syntax, while simple, tends to be hard to read due to too much noise.

Its rigid adherence to its 80s era type system really irks me. I find it hard to believe that the likes of Rob Pike and Ken Thompson were unaware of the solution to the billion-dollar mistake, yet they still made it by having nil interfaces and reference types. I curse Ken and Rob's names whenever I see a program panic with nil pointer dereference.

So if I could dream up my perfect language for writing backends, it would have an expressive type system like Rust, the simplicity of Go, and the concurrency model of Erlang with a friendlier syntax.

Enter Gleam

Little did I know that this language was hiding in the Sean Cribbs' videos I've been ignoring in my YouTube feed the past 6 months: Gleam.

I knew of Gleam but, I honestly had no idea what it was. I thought it was another compile to JavaScript functional programming language like Elm or PureScript. Having learned both of those, and knowing that I would never convince a frontend developer to pick those over TypeScript, I looked past Gleam.

I finally caved and looked at Gleam when I saw Dillon Mulroy's video, Your next favorite programming language: Gleam come across my feed. You know what, Dillon was right, Gleam is my next favorite programming language.

I'm going to fly through what I like about Gleam because the Gleam Language Tour and Isaac Harris-Holt's videos on Gleam are a much better introduction to the language, and this post is already huge.

The Expressibility of Rust

Gleam has sum types and pattern matching ✔

pub type ControllerMessage { SetFrequency(frequency_hertz: Int, pota_mode: String) RefreshSpots } pub fn handle_message(message: ControllerMessage) { case message { SetFrequency(frequency_hertz:, pota_mode: ) -> { // do something with the message todo } RefreshSpots -> { // do something with the message todo } } }

It even inherits Erlang's fancy binary pattern matching (with some modernization such as Unicode support)

pub fn parse_packet(data: BitArray) -> Result(IpPacket, Nil) { let data_size = binary.legnth(data) case data { << version:4, header_length:4, service_type:8, total_length:16, id:16, flags:3, fragment_offset:13, ttl:8, protocol:8, header_checksum:15, src_ip:32, dest_ip:32, rest:bits, >> if version == ip_version && header_length >= 5 && header_length * 4 >= data_size -> { todo } _ -> Error(Nil) } }

Simplicity of Go

Gleam famously only has 22 reserved keywords, and only 15 are currently used. This creates a very compact language that is very easy to learn.

(slide courtesy of Issac Harris-Holt)

I think the last time I picked up a language this fast was when I learned Go back in 2009. It literally took me an evening of going through the Gleam Language Tour to be productive in Gleam.

Now, I'll admit that I am no stranger to functional programming languages, so that likely attributed to my quick grasp of the language.

Lets look at how simple this language is. First, there are no if statements in Gleam, they are done with case expressions. It might be a little more verbose, but there's only one construct to learn.

case string.contains("wibble", "le") { True -> "wobble" False -> "wazzle" }

There's no for or while loops because loops are done with recursion. This might be very off-putting at first, which is fair; recursion is notorious in computer science, and leads the mind to imagine scenes from Inception.

However, I'll argue that structurally, recursive functions aren't that different from for loops.

func double_all(xs []int) []int { // The state of loop accum := make([]int, len(xs)) // construct a loop that keeps taking an item from the array until we've hit the end for i, x := range xs { // update the state accum[i] = x * 2 } // go back to the start until we've process the entire list return accum }/// double all items in a list /// Normally you'd use `list.map` for this, so this is just for demostration purposes pub fn double_all( // `accum` is the state of the "loop" accum: List(Int), // `xs` are the items we're processing xs: List(Int), ) -> List(Int) { case xs { // We've read all the items, this is called the "base case" or the exit // condition for the loop [] -> list.reverse(accum) // Here we extract x off the start of the list, and put the remaining items in rest [x, ..rest] -> { // create a new loop state which is the old state with the doubled // value added to the front let new_list = [x * 2, ..accum] // go back to the start until we've process the entire list double_all(new_list, rest) } } }

Each call to double_all process a smaller and smaller part of the list until we've reached the end.

double_all([], [1,2,3]) double_all([2], [2,3]) double_all([4,2], [3]) double_all([6,4,2], []) // -> list.reverse([6,4,2]) -> [2,4,6]

The problem is solved a little differently by taking smaller and smaller parts of a list until we hit the base case, but the mechanism is very similar: Iterate over each item of a collection, update some state, and return when we reach the end.

I think that once you "get" recursion, you'll find that having 3 constructs (recursion, for, while) for the same concept is a little unnecessary.

If you're still skeptical that recursion is enough, I'd recommend watching Issac Harris-Holt's video You don't need loops.

The Concurrency of Erlang

Gleam compiles to Erlang, so you have access to spawning actors and Gleam APIs to the fantastic OTP. So all the concurrency benefits I praised Erlang for is present in Gleam. Issac has an excellent video on how to use the OTP from Gleam, so I won't attempt to do a better job here.

Things I didn't know I wanted

There are two features that Gleam has that I've come to really love. The first are pipelines. It allows you to chain function calls together.

Here's an example of Go code that I write often:

ctx := context.WithTimeout(context.Background(), 3 * time.Second)

The equivalent Gleam code would look like this is there was a similar API in Gleam:

let ctx = context.background() |> context.with_timeout(3 * time.seconds)

|> puts the result of the first function call into the first argument of the second function call.

You'll see this a lot with the result.Result type for error handling

pub type Error { MissingEnv(String) } pub fn parse_url(url: String) -> URL { todo } let result = envoy.get("DATABASE_URL") // -> Result(String, Nil) |> result.map(parse_url) // -> Result(URL, Nil) |> result.replace_error(MissingEnv("DATABASE_URL")) // -> Result(URL, Error)

You'll also see |> used with what is called the "Builder Pattern". This pattern resembles chained method calls in other languages. Look at the example code from the glight logging library.

logger() |> with("key", "value") |> with("one", "two, buckle my shoe") |> with("three", "four close the door") |> debug("hello debug")

This is very similar to how the Go logrus API does structural logging:

logrus. WithField("key", "value"). WithField("one", "two, buckle my shoe"). WithField("three", "four close the door"). Debug("hello debug")

As a long time Linux user, I find |> very nature to use. It is essentially a typed Unix pipe. I love that it is one construct that can express multiple patterns in a single, elegant syntax.

The second novel feature of Gleam is the use keyword. This is considered an "advanced" feature of Gleam, but mastering it really cleans up your code.

It is a hard concept to explain without examples, so let me show you some. Let's say you have a function that returns Result(String, Nil) like the one in the example from above:

// Get the DATABASE_URL environment variable envoy.get("DATABASE_URL") // -> Result(String, Nil)

What if I want to grab multiple environment variables and store them in an Env record?

pub type Env { Env(database_url: String, port: String, secret_key: String) } pub fn extract_env() { let database_url = envoy.get("DATABASE_URL") let port = envoy.get("PORT") let secret_key = envoy.get("SECRET_KEY") Env(database_url:, port:, secret_key:) }error: Type mismatch ┌─ /home/eric/src/gleam-scratch/test/blog_test.gleam:50:7 │ 50 │ Env(database_url:, port:, secret_key:) │ ^^^^^^^^^^^^^ Expected type: String Found type: Result(String, Nil)

We can't because DATABASE_URL is trapped inside a Result(String, Nil). We have to unwrap it somehow.

The messy way to do this is with pattern matching:

pub fn extract_env2() { case envoy.get("DATABSE_URL") { Error(err) -> Error(err) Ok(database_url) -> case envoy.get("PORT") { Error(err) -> Error(err) Ok(port) -> case envoy.get("SECRET_KEY") { Error(err) -> Error(err) Ok(secret_key) -> Ok(Env(database_url:, port:, secret_key:)) } } } }

No one in their right mind would want to do that. Alternatively, we can use a higher-ordered function called result.try to do the Ok(_) arms in a function, and short-circuit for any Error(_) cases:

pub fn extract_env3() { result.try(envoy.get("DATABSE_URL"), fn(database_url) { result.try(envoy.get("PORT"), fn(port) { result.try(envoy.get("SECRET_KEY"), fn(secret_key) { Ok(Env(database_url:, port:, secret_key:)) }) }) }) }

That is a little better, but it is still a noisy pyramid of doom. We can do better with use.

pub fn extract_env_with_use() { use database_url <- result.try(envoy.get("DATABASE_URL")) use port <- result.try(envoy.get("PORT")) use secret_key <- result.try(envoy.get("SECRET_KEY")) Ok(Env(database_url:, port:, secret_key:)) }

use is syntactic sugar that turns the code in extract_env_with_use() into the code in extract_env3().

This is a very powerful concept. use provides functionality that I like to call, "programmable semicolons". Based on the function being used, it can add a little logic before or after calling a callback.

Does your Gleam code interact with JavaScript Promise(s)? Oh, no, we don't have async and await support built-into in Gleam. What are we going to do?!?

Oh, but we do with the power of use and the help of promise.await

pub fn await(a: Promise(a), b: fn(a) -> Promise(b)) -> Promise(b)import gleam/io import gleam/javascript/promise pub fn main() { use data <- promise.await(some_async_function()) use data <- promise.await(some_other_async_function(data)) io.println(data) }

Want to construct a chain of web middleware that will respond with a 405 Method Not Allowed and require that the request body is JSON without the usual rigmarole? use to the rescue.

fn handle_request(request: Request) -> Response { use <- wisp.require_method(request, http.Post) use json <- wisp.require_json(request) // ... }

With this simple construct, many behaviors can be expressed such as short-circuiting on errors, chaining promises, mapping a list, and beyond.

Anyone who's been around the functional programming space will know that I didn't invent the term "programmable semicolon", it is a common way of describing monads.

Oh no! A scary FP word, fear not, use is like Prometheus, who climbed Mount Olympus to bring monads to mere mortals. use exemplifies the goals of Gleam, simplifying the confusing, esoteric concepts in FP into easy to use solutions.

As someone who has been playing with statically typed functional programming languages for almost two decades, Gleam's goal of making FP easier to learn, friendlier, and approachable really has me really excited.

My concerns

Now to bring the mood in the room down a little. I've been excited about a new languages before. There are some hurdles that evangelists of Gleam will need to have a good story for.

I worry that the BEAM will still be a bit too esoteric at runtime. Error messages are going to be in Erlang's odd syntax. Adoption will require operation teams to learn how to operate, instrument, and observe the BEAM VM.

The BEAM is fairly alien to most compared to something common like running Python or Golang in production. With their risk aversion, selling the BEAM to operations teams will take some doing. Erlang is famous for high uptimes, so this might not be the tough sell I think it is.

Immutable data types have a learning curve. Most programmers come from an imperative background and processing immutable data is different from processing mutable data. That said, the juice is worth the squeeze. Immutability brings a sense of calm when you can stop worrying about what can change your data.

Gleam's insistence on code generation over meta-programming may be a mistake. I am not exactly sure. Rust's derive macros are very convenient, but Go's use of code generation hasn't been the big a deal to me. Time will tell. I'm not super worried about this one, and I'll trust the Gleam team knows what they're doing.

Elixir may keep people away. Those currently using Elixir will likely stay with Elixir unless they really want static typing. Those looking to solve Elixir shaped problems, may choose Elixir over Gleam due to its maturity. Time will tell I suppose. Gleam is growing fast, and its ecosystem is already pretty good.

Conclusion

Overall, I'm super excited about the language. I have enjoyed every minute that I have tinkered with it, and I'm excited to find reasons to use it. It isn't quite the perfect general purpose language for me due to the BEAM being so service oriented.

It won't replace Rust for everything I do, but it is a great compliment to Rust.

If I have a Gleam shaped problem, like long-running services with lots of concurrency, I will reach for Gleam. The productivity gains over Rust are too hard to pass up.

However, for a Rust shaped problem like a native GUI, high-performance, or a CLI, I will still reach for Rust. I am curious about using Rustler to marry the two worlds. Particularly when it comes to protocol parsers.

I feel like I have to apologize a bit to Issac because this article feels like it is plagiarizing his videos. Gleam is a hard language to write about without sounding like you're ripping off other creators because it is so small, and we all love the same things about Gleam. I highly recommend watching his playlist on Gleam. They are really well done.

Read Entire Article