Elixir has a type system and I really ought to take advantage of it

Hey there! Nat Bennett here – you're reading Simpler Machines, my weekly newsletter that's mostly about making software the way weird places like Pivotal do it.

I still struggle to communicate what this newsletter is about succinctly. It's often about making software, but it's rarely about the technical details of making software. It's often not even very much about process and practices. My best work (the stuff I'm proudest of and that resonates the most with people) is more about making meaning from corporate software work. My writing helps people make sense of experiences that we share.

Part of the reason that it's hard for me to describe what I "write about" is that fundamentally, I pick a time to write, and I sit down and I write something at that time, and then I hit "publish." Generally there's a day in the week that is Newsletter Day. Sometimes that's the day the newsletter gets edited. Often it's the day the whole thing gets written. (Usually in no more than an hour or two.)

What do I write about? Whatever I'm thinking about when I sit down to write. Sometimes I'll go on a run first and think through what I'm going to write about, but often even when I do that I sit down and I write about something completely else.

I'm able to stick a little bit to a topic because I know I have an audience, and I'm writing to that audience when I write. I write about software partly because that's one of the things I talk about most, and want to convince people about the most. If I was spending more time hanging out with competitive Magic players I'd probably write more about Magic.


Incidentally– if you want to write better, write more. If you want to write more, write on a schedule. Decide on the time, sit down, don't do anything else during that time. You don't have to write, but you do have to sit. This is the most cliched advice on writing in the world but that's because it works. Most writers are people who are really unhappy or weird when they're not writing, because that makes it much easier to maintain the habit. Writing is always a habit, and often a compulsion.

If you have a hard time picking a schedule, the best time to write is "first thing in the morning." The second best time is "last thing at night."

There's lots of other things you can do to explore your voice or whatever, but if you're not writing regularly, nothing else matters.


Anyway–

I mentioned last week that I got a book about Elixir's Ecto library. It's been handy. I haven't finished reading it but I've already found myself picking it up for reference when I get stuck on something tricky. Preloads, for instance. Also helped quite a bit while I was debugging changes I was making to the database configuration in the production environment.

I like having an actual paper book for this kind of thing because it's easier to reference. I also find – in our current age of SEO and LLMs – that the quality of information in books tends to be higher than what I can find online. This is a habit I picked up a few years ago with cookbooks. Basically anything I want to seriously understand, I get a book about it.

The internet is great for ephemeral lookups and developing relationships. Not so much for reference material. Even beyond the information quality – there's something about the design affordances of the book. It's just a really well-designed, really deeply iterated class of artifact.

This week the big messy sticky thing I got stuck in was my own cavalier attitude towards types in the codebase I'm working on. Elixir actually has a pretty rigorous type system and various ways to confirm that something is the thing that you expect it to be before you use it, but I haven't been using those mechanisms. So I spent this week finding and fixing a handful of bugs where the root cause was, "Just because this thing has an .id field, that doesn't mean that it's the right .id"

Good lesson on the limitations of test-driven development, too: The tests are only as good as the test data. The problem was a function that was something like

def add_thing_to_container(container, thing) do
   do_something(container.id, thing.id)
end

At some point I swapped the order of the arguments, and then fixed the tests but not all of the call sites. I had tests for the call sites but those tests mostly made one Container and one Thing, so they had the same ID, so accidentally swapping the IDs didn't change the behavior of the function.

The reason this error was silly is that Elixir has a good way of preventing this kind of issue: It can confirm types in the function signature using pattern matching. So instead the function could have been something like

def add_thing_to_container(%{}Container = container, %{}Thing = thing) do
   do_something(container.id, thing.id)
end

(This takes advantage of an Elixir feature called pattern matching, which can do much more than just check types – anything that you can express with an expression you can check in an argument like this. You can also overload a function and define it multiple times for different kinds of things. You'll see this a lot in Elixir as a very succinct alternative to a case statement.)

This behavior is a big part of why I use Elixir.

I really, really like dynamic type systems. Part of this is just intuition/aesthetics that I haven't yet explored in detail, but being able to define behavior at runtime is really useful for inspecting program state at runtime, and that's something I'm very interested in. But one of the problems with dynamic type systems is that if you wait until execution to check whether a thing has a behavior, you often don't have any context about why that thing doesn't have a behavior. So you end up with inscrutable error messages like

NoMethodError: undefined method SOME_METHOD for nil:NilClass

Another aside but this is something that really bugs me – that's an error from Ruby, but it's functionally the same as this error from Go

panic: runtime error: invalid memory address or nil pointer dereference

We tried to call a behavior for a thing. It failed in an unrecoverable way because that "thing" is nil. Go is supposed to be a "strict" language, and there are all kinds of things that are a pain to do in Go because of the limitations it puts on changing the behavior of things after compilation. And yet, there are lots of cases where it doesn't know whether something is nil until runtime – there's a lot more to a type system than when you do the checking.

Anyway–

In Ruby you can avoid that kind of error by explicitly checking whether something is nil before you use it. The earlier you catch an unexpected nil, the more likely you are to be able to figure out what went wrong that produced it.

Elixir makes that kind of checking really easy with function clause matching. The downside (and upside) of Elixir is that this doesn't get invoked until the code actually runs, so you have to have tests. The type checking is also optional – which is sometimes convenient, but a language that enforced type checking at compile time wouldn't have let me make this error.


Probably because I learned Ruby first, before I learned any compiled language, and because I therefore learned how to write tests before I learned much of anything about type systems, I think of compile-time type checking as a special kind of test that the compiler runs. I'm told that this is insane but no one has ever been able to explain to me why it's wrong.

"The compiler isn't a test runner" they say, but the behavior is identical.

Nat