Ok. I am liking Gleam. I haven't been working with the language for long (read, last week). But still. I am REALLY liking the simplicity. These are some quick notes from learning the language so far.

The @deprecated attribute

It is easy to miss how valuable. But if you been writing too much GraphQL, you will know the @deprecated decorator. In Gleam, @deprecated works the same way. You add a function attribute to tell the compiler where people should go for the new way:

@deprecated("Use new_way. It's better")
fn old_way(x: String) -> String {
  x
}

fn new_way(x: nice) -> nice {
  x
}

Simple and immensely effective. Helps provide a path to where to look for new versions of a library function. Once you have such an annotation system, developing gets a little bit easier. And you miss it quite quickly when you don't have it!

Todo

Have you ever written somewhere in your code a todo? I mean... there's literal todo linters out there. Well, in Gleam, todo is a base construct. You identify like so:

fn something() -> String {
  todo as "maybe, should do something" 
}

Allowing you to not only identify there is something to do, but with details on what the todo may actually need to be. I cannot tell you how many times I've written these little todo in comments. So let's just say, I am content with the option being baked in.

Generics

Anywhere you go, you will find papers upon papers over "correctness" for how to do Generics. Also known as parametric polymorphism. For example, in Go we had to wait for years until 1.18:

type Num interface {
  int | string
}

func Mistakes[T Num](val T) T {
  return val
}

Go's way isn't bad. Especially when you remember how to do this in C++. Or again in Go where you still have to argue with others about using generics instead of interfaces... But in Gleam:

fn Mistakes(val: woah) -> woah {
  val
}

Gleam handles the generic type as part of functional type annotation. Some powers are lost in more stronger construct. Like constrained generics. But, the benefit is simplicity and readability.

case EVERYTHING

You see, in BEAM languages like Elixir, Erlang, or Gleam, you will have to do pattern matching. It is a super power of these languages. Maybe the super power.

For a simple example that translate outside of the beam, take checking if an integer is zero. In Elixir you can do this magic with functions:

def zero?(0), do: true
def zero?(maybe) when is_integer(maybe), do: false

Which translate to something in Go like the following:

func zero(maybe int) bool {
	switch maybe {
	case 0:
		return true
	default:
		return false
	}
}

I personally really like Elixir's magic. It feels like alchemy. But also... it kind of is. There is macros, atoms, guard constructs, functional type checks, overloads, and pattern matching to make those two lines work.

In Gleam, the language takes the approach of being meticulous on doing things one simple way. Which means, case expressions are your everything:

fn zero(maybe Int) -> Bool {
  case maybe {
    0 -> True
    _ -> False
  }
}

While may seem similar to Go's approach and less so of Elixir's, Gleam is in checking what each an operational response should be. So constructs that are harder on non pattern matching languages, like deconstruction of lists, are still straight case matches:

// return first list with at least 2 elements
fn first_with_many(lists: List(List(t))) -> List(t) {
  case lists {
    [] -> []
    // notice the pattern alias!
    [[_, _, ..] as first, ..] -> first
    [_, ..rest] -> first_with_many(rest)
  }
}

No Exceptions!

For so many languages, exceptions is simply a way of dealing with errors. Exception handling in python is practically a requirement. It is inevitable.

However in Gleam, the type system is built for no exceptions. Elm is notorious for this type of design. To do such a thing outside of such languages, you have to be absolutely foaming methodical on how you write code. But in Gleam, it is simply part of how the language works.

let x: Option(Int) = None
// default to -1 if None
let y = option.unwrap(x, -1)

You either get the result of unwrap, or you get a default you define. Done.

No Nulls

This one is one that is definitely coming from the Rust part of Gleam. The language doesn't have null/nil values. Yes. Technically there is a Nil type, but it is only able to be set to itself. You can't make an Int a Nil value. However, there is such a thing as an absence of something. You use Options for that.

let answer = Some(42)

And if you want to know of errors, you use Results.

let err = Error("yes, things went the wrong way")

And that's it! There's other neat tricks on the language, but the simplicity is what really selling its abilities for me.