← blog.mackieburgess

Trying a new programming language

How hard can Gleam possibly be?

Mackie Burgess · 2023-11-20

Today hasn’t been a great day. Not for any particular reason, I’ve just felt down.

This hasn’t left much room for the creative writing juices, which have been on low supply for the past week or so.

As a “break”, let’s test out a novel programming language I’ve never tried before.

Gleam

Gleam is a functional programming language with Elixir/Rust vibes. It is incredibly young: it doesn’t have a Wikipedia page yet! Regardless, people seem to be having fun with it, so I figured I’d give it a try.

Installation seems pretty straightforward.

brew install gleam erlang

Now I have erlang (Gleam runs on BEAM) and Gleam. I should be able to make a project.

gleam new helloworld
cd helloworld
gleam run
   Compiled in 0.01s
    Running helloworld.main
Hello from helloworld!

Neat!

Installing Gleam also comes with a LSP (Language Server Protocol) so I threw it into my Neovim config and now I have nice in-editor smarts! I also got nice colours by running :TSInstall gleam.

Starting to really enjoy languages including their LSPs on installation: One more step I don’t have to think about.

The hello world code looks like this:

import gleam/io

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

...and I can do some string formatting with these <> things:

import gleam/io

pub fn main() {
  let hello = "Mackie’s computer"
  io.println("Hello from " <> hello <> "!")
}
Hello from Mackie’s computer!

Seems good enough to me.

Advent of Code

I really like Advent of code, let’s try an old problem.

Advent of Code 2019 – Day 1

Unambitious I know, but I’ve only ever written hello world with this language, and I haven’t really consumed any other reading material on it. Read a couple blog post by Erika Rowland but that’s about it.


Part one is just a sum of integer calculations: x // 3 - 2

// is python syntax for “integer divide”. I’m sure I’ll soon find out about Gleam’s integer division.

We take our file, split it by newline, run the calculation on every line, sum the outputs.

import gleam/io
import gleam/int

pub fn fuel_required() -> List(Int) {
    [1,2,3]
}

pub fn main() {
  io.println(
    "part one: "
      <> fuel_required()
      |> int.sum
      |> int.to_string
  )
}

Here’s the framework, my goal is to replace [1,2,3] with the result of the hard part.

Question 1: how do I read a file?

I found some documentation for gleam_erlang which points me to a package called simplifile. Bit of a wild goose chase, but at least I caught the goose really quickly.

gleam add simplifile
pub fn fuel_required() -> List(Int) {
  simplifile.read("data/1.test")
    |> result.unwrap(_, "")
    |> string.split(_, on: "\n")
    |> list.filter_map(_, fn (line) {
      case int.parse(line) {
        Error(Nil) -> Error(Nil)
        Ok(value) -> Ok(value / 3 - 2)
      }
    })
}

Ok so that just happened. I kinda just made guesses on how the standard library is put together. Guess I should annotate it.

simplifile.read("data/1.test")
  |> result.unwrap(_, "")

_ is the output of the last operation in the pipeline.

Read the file at data/1.test, relative to the project route. This returns a Result type. result.unwrap turns an Ok("something") into a "something", and any Error into the default value: in this case "" (the empty string).

  |> string.split(_, on: "\n")

Split up the string based on newlines. on: is a named argument, nice to see those around. Looks like gleam is full of them.

  |> list.filter_map(_, fn (line) {
    case int.parse(line) {
      Error(Nil) -> Error(Nil)
      Ok(value) -> Ok(value / 3 - 2)
    }
  })

list.filter_map is an unsurprising combination of filter and map. case is pattern matching, a relatively nice pattern matching situ going on I’d say. int.parse parses a string into an int.

I swapped in the real data and took it for a spin:

gleam run
part one: 3273471

Correct answer!

Part 2

Part two of the problem is exactly what I feared when I saw this was a rocket-based problem. Fuel needs fuel to carry the fuel. This recursive fuel equation continues until it returns a value less than zero, which is ignored.

My plan for this part is to write a fuel_recursion function which can be called inside the case expression.

First, let’s clean the file up to make room for part two:

import gleam/io
import gleam/int
import gleam/result
import gleam/list
import gleam/string
import simplifile

pub fn fuel_required() -> String {
  simplifile.read("data/1.input")
    |> result.unwrap(_, "")
    |> string.split(_, on: "\n")
    |> list.filter_map(_, fn (v) {
      case int.parse(v) {
        Error(Nil) -> Error(Nil)
        Ok(v) -> Ok(v / 3 - 2)
      }
    })
    |> int.sum
    |> int.to_string
}

pub fn recursive_fuel() -> String {
  "Oh lord."
}

pub fn main() {
  io.println("part one: " <> fuel_required())
  io.println("par two: " <> recursive_fuel())
}

I’m not afraid to duplicate code, especially since I’m on hour zero with this language:

pub fn recursive_fuel() -> String {
  simplifile.read("data/1.test")
    |> result.unwrap(_, "")
    |> string.split(_, on: "\n")
    |> list.filter_map(_, fn (line) {
      case int.parse(line) {
        Error(Nil) -> Error(Nil)
        Ok(value) -> Ok(fuel_recursion(value))
      }
    })
    |> int.sum
    |> int.to_string

}

So pretty much the exact same code. Kinda dreading this fuel_recursion function tho-

pub fn fuel_recursion(value: Int) -> Int {
  case value / 3 - 2 {
    v if v > 0 -> v + fuel_recursion(v)
    _ -> 0
  }
}

Oh. It’s done.

gleam run
part one: 3273471
part two: 4907345

Correct answer: two stars, problem solved.


Wow.

So far the developer experience in Gleam is ridiculous. Just write code, as fast as possible, and it works. Follow in-editor errors when you forget to wrap something in Error(). Simples.

I wrote both parts of this problem in 45 minutes. That included going to the shop for ginger beer and writing this blog post. That is a fast introduction to a language.

One part I glossed over was the quantity of imports:

import gleam/io
import gleam/int
import gleam/result
import gleam/list
import gleam/string
import simplifile

This is a bit annoying, doesn’t seem like there’s a way to fold them down, a la:

import gleam/{io, int, result, list, string}
import simplifile

...which would be nice, but honestly the language is so young that I can’t be too hard on it.

Maybe I should do 2023 Advent of Code in Gleam 🤔