Some guy naively bungles through his first steps in Elixir

20 Apr 2022

19 minute read

I've got a quest to embark on: I'm going to learn Elixir programming language.

Well, kind of.

Elixir is a language I've never used per se. I've never written a program in Elixir, either professionally or otherwise. HOWEVER, I'm fairly familiar with a number of the concepts that undergird Elixir: concurrency, multi-dispatch functions, macros and metaprogramming, pure functional programming, and function composition.

So, I don't KNOW Elixir, but I've been exposed to many of its core ideas.

However, conceptually knowing how a thing works and having "boots on the ground" experience working with it are two very different things. There's nothing better for learning than doing, so we're gonna take a look at the first Advent of Code 2021 problem and try to solve it in Elixir!

A note on this blog

This document, with a few editorial sleights of hand for clarity and readability, is my honest-to-god first attempt at doing anything in Elixir. No joke, I had never written a line of Elixir code before writing this, and I've barely read any either.

I've shaved off a few warts here and there, but by and large what follows is exactly the path I walked in order to solve problems, learn the language, and derive an answer for the coding challenge.

If you're a member of the Elixir community, or a fellow first-time language learner, or just curious, you may find this journey interesting!

So, that being said, let's get into it:

Step one, read the challenge inputs

All of the Advent of Code challenges have an accompanying plain text file to go along with the challenge, which I've usually downloaded onto my machine and processed from there.

So, let's read that file into memory! After a quick perusal of the docs, it looks, self-evidently enough, like File.read is what we want:

CODE:

{ :ok, contents } = File.read "~/Desktop/inputs.txt"

RESULT:

** (MatchError) no match of right hand side value: {:error, :enoent}

That … uh, did not go as expected. The file path is right, this is definitely the right function, so why is it telling me the file isn't there?

I think … I think I've been here before.

Yes. This is the point on the Wheel of Software Development where you attempt and instantly fail at something so trivially simple that you start to question your entire career path and future in the industry.

Alright. Breathe my dude. You've read a file from disk in plenty of other languages before, you can do it in this one too. We just need to dig a little bit to find out what's going on here.

Unexpected deviation one: absolute file paths

After some research, it turns out that Elixir will expect a path to be a relative path unless you tell it that it's an absolute path. Just some tribal knowledge that you have to pick up along the way I suppose.

I've never understood why file paths are always such a hairy topic in programming languages. Node.js has a whole module dedicated to paths. In Clojure and Java, you must contend with the dreaded classpath. Everybody has their own special sauce for specifying where your files are and how to get to them. Looks like Elixir favors the assumption that the paths you feed to it are relative unless explicitly stated otherwise.

I think the "why" of this issue is that the seemingly simple problem of file paths is more complicated that I'm giving it credit, but at any rate, the Path.expand function seems to be what we need for the task:

CODE:

{:ok, contents} = File.read Path.expand "~/Desktop/inputs.txt"

RESULT:

{:ok,
"195\n197\n201\n204\n203\n216\n213\n215\n216\n185\n188\n190\n205\n218\n237\n243\n248\n278\n289\n292\n304\n313\n313 ..."}

Alright, that did it. We can proceed onwards.

But wait a minute. If we look more closely, a fruitful-seeming sidequest popped up in the course of performing that "simple" task: what was that business about right hand side values and matches?

** (MatchError) no match of right hand side value: {:error, :enoent}

If a youth squandered on JRPGs taught me anything, it's this: always follow the sidequests. Let's head to that cave the guy at the tavern told us about and see what kinds of dragons there are to slay within.

Sidequest one: pattern matching, assignment and equality

Pattern matching on basic values

"Pattern matching" is a term you frequently hear in functional programming circles. Cool languages like Prolog, OCaml, Rust and Haskell all have it. But what is it exactly?

Evidently, the first thing to learn about pattern matching comes down to the nature of the "=" operator in Elixir. In, well, practically any other language, = is an assignment operator, meaning that we use it to tie a value to a symbol for later use.

Not so in Elixir. It is not just for assignment, it is also for matching. It seems to be more akin to the good old = we remember and love from math class. Consider this:

CODE:

x = 1

if 1 = x do IO.puts "That makes sense ... but it looks weird" end

RESULT:

That makes sense ... but it looks weird
:ok

Any other language would have assumed that you just tried to assign "x" to the number 1, which, needless to say, will produce an error. Permitting a programmer to overload a concept like "1 equals 1" or "1 is a number" with something like "1 is an eggplant emoji" is not conducive to a sane and well-ordered understanding of your computer. Or the universe at large for that matter.

But because we assigned 1 to the variable "x" before, it stands to reason that 1 = x too, strange though it appears through my C-tinted glasses.

If we tried that without first having assigned x to 1, we'd get a similar error to the one we saw before:

CODE:

x = 2
1 = x

RESULT:

** (MatchError) no match of right hand side value: 2

So "=" pulls double duty in this language. It assigns variables, but it also checks whether values match, and in the case of a mistake, informs you which value on which side of the operator isn't correct.

Pattern matching on structured data

This part is a little less weird if you've worked with ES6 destructuring assignment: you can also pattern match on a structured data type. It works like this (with some bonus string interpolation thrown in):

CODE:

{one, two, three, four} = {:my, "mother", :is, "a fish"}
IO.puts "#{one} #{two} #{three} #{four}"

RESULT:

my mother is a fish
:ok

But while THIS works in ES6:

const { a, b } = {a: "my", b: "mother is", c: "a fish"}
console.log(`${a} ${b}`)

RESULT:

my mother is
undefined

THIS does not work in Elixir:

CODE:

{one, two} = {:my, "mother", :is, "a fish"}
IO.puts "#{one} #{two}"

RESULT:

** (MatchError) no match of right hand side value: {:my, "mother", :is, "a fish"} 

Each side of the operator must match in quantity and in type: no partial destructuring, and no destructuring of a list into another data structure like a tuple.

If you want to ignore anything, you need to leave a "_" in the place of the thing you're ignoring.

CODE:

{one, two, three, _} = {:my, "mother", :is, "a fish"}
IO.puts "#{one} #{two} #{three}"

RESULT:

my mother is
:ok

This makes Elixir a more explicit language than I'm used to. I've dabbled in point-free style in Ramda.js, which permits (and even encourages!) you to dispense with naming the arguments to your functions in the pursuit of making them more generic and universally applicable to your data. Clojure enables this via the partial function, and point-free style is a defining feature of Haskell.

To be fair, Elixir also permits point-free style via the pipe operator:

CODE:

"hello" |> String.capitalize |> String.reverse

RESULT:

"olleH"

I guess the language is seeking to strike a balance between "you have to name everything" and "there's a bunch of stuff you don't have to name if you don't want to". You're granted the freedom to pass a "known" piece of data through a pipe without hanging an identifier on it at every stage. However, if you try to grab a subset of data out of a data structure and that structure contains something unexpected, Elixir will complain about it.

Which brings us back to the branching off point of our sidequest: the reason I got a MatchError when I tried to read my file was because I didn't include anything about the error on the left-hand side of the pattern.

So how do you go about the task of reading a file and handling errors if the file isn't there or the path is incorrect?

The Javascript-y/lisp-y part of my brain is telling me to grab the contents of the file, and also grab the error, and check whether one of them is null before proceeding:

CODE:

{:ok, contents, :error, reason} = File.read "~/Desktop/inputs.txt"

if :error do IO.puts "no file at that path" end
if :ok do IO.puts "here's your file!" end

RESULT:

** (MatchError) no match of right hand side value: {:error, :enoent}

But that's not right either. All we got back from File.read in THAT example was {:error, :enoent}. Even though we included pattern information about the error on the left-hand side, we ALSO included a pattern for {:ok, contents}. The right hand side only had {:error, :enoent}, and the disjunction between right and left produced a match error.

You could do a try/catch-style thing, or as it's know in Elixir, try/rescue, which will let you attempt a side effect-producing IO operation without crashing the process:

CODE:

try do
  {:ok, contents} = File.read("~/Desktop/inputs.txt")

  IO.puts "here's your file! #{contents}"
rescue 
  error -> IO.inspect error
end

RESULT:

%MatchError{term: {:error, :enoent}}

But, evidently, this is not the Elixir way. Instead, one is encouraged to use strategies based on pattern-matching rather than try/catch.

The case statement will allow you to pattern match against the return value of a function and perform an operation based on what it matches against:

CODE:

file_or_error = case File.read "~/Desktop/inputs.txt" do
		  {:error, reason} -> reason
		  {:ok, contents} -> contents 
		end

IO.puts "If you see 'enoent', it means there wasn't a file there: #{file_or_error}"

RESULT:

If you see 'enoent', it means there wasn't a file there: enoent
:ok

And thus we come to the end of the sidequest, dragon slain, and with the Amulet of Pattern-Matching and Shield of Error Handling now tucked away into our inventory. I'm sure they will make the rest of the main quest much easier!

Back to the main quest: process the data

Getting back to the task of solving the challenge, we should start massaging this huge newline-delimited string into something easier to work with.

It's an iterative task that we're performing, so an iterable data type like a list seems appropriate. We're going to be doing math on this list eventually, so let's cast the resultant list of strings to a list of numbers while we're at it:

CODE:

file_or_error = case File.read Path.expand "~/Desktop/inputs.txt" do
		  {:error, reason} -> reason
		  {:ok, contents} -> contents 
		end

list = if file_or_error !== "enoent" do
  file_or_error
  |> String.split(["\n"])
  |> Enum.map(&String.to_integer/1)
end

IO.inspect list

RESULT:

** (ArgumentError) errors were found at the given arguments:

  * 1st argument: not a textual representation of an integer

    :erlang.binary_to_integer("")
    (elixir 1.13.4) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2

Woof. Back at the nadir of the Wheel so soon? That came quick!

Breathe my dude. Chill. Read the error message and proceed.

Side quest two: more error messages

When I managed to stop researching master's degrees and actually read the error message, it told me some very cool things, and incidentally revealed some more interesting details about how Elixir works.

The top of the error, in somewhat academic terms, told us exactly what went wrong:

** (ArgumentError) errors were found at the given arguments:

  * 1st argument: not a textual representation of an integer

And then it showed us two more things: the function call that caused the error, and a little peek at the underlying engine that powers Elixir.

Elixir, in reality, is Ruby-inspired syntactic sugar over the Erlang language. It's more like the powdered sugar on a donut than the inch-thick schmear of frosting on your niece's birthday cake though, notice this line:

:erlang.binary_to_integer("")

That must be the actual Erlang function that Elixir attempted to run against your data, and the value that was passed to that function. Neat!

And finally, it told us where to go looking within the Elixir source code itself if we really need to go digging for an answer. This part is more interesting than it is useful, but if you encounter an especially weird and hairy bug, information like this is gold for debugging:

(elixir 1.13.4) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2

Trimming the non-textual representations of integers from the data

Evidently, somewhere in the data there is an empty string. Probably at the beginning or the end, because String.trim fixes the issue nicely:

CODE:

 file_or_error = case File.read Path.expand "~/Desktop/inputs.txt" do
		   {:error, reason} -> reason
		   {:ok, contents} -> contents 
		 end
 list = if file_or_error !== "enoent" do
   file_or_error
   |> String.trim
   |> String.split(["\n"])
   |> Enum.map(&String.to_integer/1)
 end

IO.inspect list

RESULT:

[195, 197, 201, 204, 203, 216, 213, 215, 216, 185, 188, 190, 205, 218, 237, 243, 
 248, 278, 289, 292, 304, 313, 314, 317, 329, 328, 329, 328, 329, 330, 331, 329, 
 333, 337, 338, 345, 352, 354, 355, 359, 360, 363, 365, 364, 370, 371, 367, 387, 
 403, 407, ...]

Here we are at last, a list of integers! Time for some good old list comprehension.

Examining the processed data

Like any sane language, there are a thousand ways to iterate through a list. Traipsing through the aisles of the language docs, we find a garden-variety for-loop, Enum.each, Enum.map, so on and so forth.

Elixir seems to favor recursion by way of multiple dispatch for performing an iterative task. But, to be totally frank with you, I've never grokked how to wield multiple dispatch idiomatically, I don't naturally reach for recursion in most cases, and the novelty of all of this is starting to wear me thin. Maybe we'll try that another day.

For now, I'm gonna whip out the bestest functional list-mangling Swiss Army knife that there ever was: the almighty REDUCE.

Let's make a small demo to grok this more easily. In a reducer, you get access to a couple of things:

  • each item in the list, and
  • an "accumulator" which holds the value you're building up.

I'll need to simulate both of those for this smaller pre-reducer demo:

  • "x" in this instance will be a hypothetical item in the list; specifically, the first item in this demo.
  • "num_inc" will be the accumulator, or the number of times depth increases to put it in the parlance of the challenge.

I'll also need some way to reference the whole list in order to compare depth readings, or in other words: I'll need access to both the current element in the list and its neighbor to the right.

How do we get access to the neighbor element?

In some languages, the reduce function will give you access to the list being traversed within the reducer callback itself. Other languages have functional list iterators which provide both the item being iterated on and its index in the original list, which can be used for index-based access on the list.

Neither of these kinds of functions are available in the Elixir standard library. Accessing a list by its index is considered unidiomatic.

Guess what they encourage you to do instead?

Yep, pattern match.

I'm brewing a strategy in my head whereby we let the reference list ride shotgun inside the accumulator itself, and pop items off of the front as the reducer loops, but for the purposes of this limited test exercise:

  • "next" will represent the next item in the list from "x". We'll compare x to next to see if the depth increased, and increment num_inc accordingly.

CODE:

list = [231, 235, 220, 222]
[x | _] = list 
[_, next | tail] = list

num_inc = 0 + if x < next do
  IO.puts "depth has increased, add one to num_inc captain"
  1
else
  IO.puts "depth has not increased or stayed the same, hold your course"
  0
end

IO.puts "num_inc is now #{num_inc}"

RESULT:

depth has increased, add one to num_inc captain
num_inc is now 1
:ok

Looks good! 231 is indeed less than 235, so everything appears to work in the small.

Let's get it working in the large.

Hooking up the demo logic to the reducer

All we need to do now is hook this up to our reduce function and massage the demo code into a reducey API.

  • "ref" is the reference list riding shotgun in the accumulator,
  • "num_inc" is the number of increments we've observed,
  • That thing with a percent in front of it is a map, and finally,
  • [next] ++ tail is list concatenation.

After we're done, the number of increases should be 2, one between 231 and 235, and a second between 220 and 222:

CODE:

list = [231, 235, 220, 222]

Enum.reduce(list,
  %{num_inc: 0, ref: list},
  fn x, acc ->
    [_, next | tail] = acc.ref
    next_inc = if(x < next, do: acc.num_inc + 1, else: acc.num_inc + 0) 
    next_ref = [next] ++ tail

    %{num_inc: next_inc, ref: next_ref }
  end)

RESULT:

** (MatchError) no match of right hand side value: [222]
    (stdlib 3.17.1) erl_eval.erl:450: :erl_eval.expr/5
    (stdlib 3.17.1) erl_eval.erl:123: :erl_eval.exprs/5
    (elixir 1.13.4) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3

Ah MatchError, we're becoming fast friends. Looks like there was a hidden door in that first sidequest dungeon that I didn't find the first time.

Back to the sidequest dungeon, in search of the Talisman of Advanced Pattern Matching

I'm not quite sure where I went wrong. To discover what's wrong with my pattern, let's do some experiments by "simulating" each run through the reducer.

Does the pattern I wrote work for the whole list?

CODE:

list = [231, 235, 220, 222]
[_, next | tail] = list 

IO.inspect next
IO.inspect tail

RESULT:

235
[220, 222]

Yes. Good news, I'm not crazy yet. The first run through the reducer must be ok.

How about with the first number removed?

CODE:

list = [235, 220, 222]
[_, next | tail] = list 

IO.inspect next
IO.inspect tail

RESULT:

220
[222]

Yep, still works, but I'm starting to see what I did wrong the first time. How about we fast-forward to verify my sneaking suspicion:

CODE:

list = [222]
[_, next | tail] = list 

IO.inspect next
IO.inspect tail

RESULT:

** (MatchError) no match of right hand side value: [222]

There it is. It's plain to see when you just write out the steps by hand, isn't it?

I didn't account for a scenario in my original reducer: what happens when there's only one value left? You can't match against three elements in a list when there's only one thing in there, hence the MatchError.

And now to rewrite that pattern to account for this scenario. This should work for the full reference list, a list with one elements, and an empty list.

The full reference list:

CODE:

list = [231, 235, 220, 222]

[next, tail] = case list do
     [_, second | rest] -> [second, [second] ++ rest]
     [last] -> [last, []] 
     [] -> [0, []]
  end

RESULT:

[235, [235, 220, 222]]

A partial list:

CODE:

list = [220, 222]

[next, tail] = case list do
     [_, second | rest] -> [second, [second] ++ rest]
     [last] -> [last, []] 
     [] -> [0, []]
  end

RESULT:

[222, [222]]

AND a list with just one item:

CODE:

list = [222]

[next, tail] = case list do
     [_, second | rest] -> [second, [second] ++ rest]
     [last] -> [last, []] 
     [] -> [0, []]
  end

RESULT:

[222, []]

Talisman in hand! Let's hook this puppy up to the real thing.

Finally, problem solved

Let's refactor that last attempt and see what happens:

CODE:

list = [231, 235, 220, 222]

result = Enum.reduce(list,
  %{num_inc: 0, ref: list},
  fn x, acc ->
    [next, tail] = case acc.ref do
		     [_, second | rest] -> [second, rest]
		     [last] -> [last, []] 
		     [] -> [0, []]
		   end
    next_inc = if(x < next, do: acc.num_inc + 1, else: acc.num_inc + 0) 
    next_ref = [next] ++ tail

    %{num_inc: next_inc, ref: next_ref}
  end)

IO.puts "The depth readings increased #{result.num_inc} times!"

RESULT:

The depth readings increased 2 times!
:ok

HOT DAMN! I think we've got it!

From top to bottom, the whole solution looks like this. And it works!

CODE:

file_or_error = case File.read Path.expand "~/Desktop/inputs.txt" do
		  {:error, reason} -> reason
		  {:ok, contents} -> contents 
		end

list = if file_or_error !== "enoent" do
  file_or_error
  |> String.trim
  |> String.split(["\n"])
  |> Enum.map(&String.to_integer/1)
end


result = Enum.reduce(list,
  %{num_inc: 0, ref: list},
  fn x, acc ->
    [next, tail] = case acc.ref do
		     [_, second | rest] -> [second, rest]
		     [last] -> [last, []] 
		     [] -> [0, []]
		   end
    next_inc = if(x < next, do: acc.num_inc + 1, else: acc.num_inc + 0) 
    next_ref = [next] ++ tail

    %{num_inc: next_inc, ref: next_ref}
  end)

IO.puts "The depth readings increased #{result.num_inc} times!"

RESULT:

The depth readings increased 1624 times!
:ok

Whew. I'm spent. But feeling good now that I've finally gotten this thing to cooperate, no MatchErrors or anything. I think we've come to the end of the quest for the time being.

Conclusion, learnings

So, what did I learn through this process?

  • Elixir really, REALLY wants you to account for every contingency when it comes to assigning variables. Does this thing exist at all? Is it in the shape you expected it to be? If not, we're gonna crash the whole party until you get every single part of this correct.
  • This can be annoying in some respects. Other languages I know play fast and loose with this kind of stuff. But once you get the concept under your belt, MatchErrors are pretty helpful when you've gotten something wrong. A far cry from what you get from Javascript in such instances, which can be roughly summarized as "lol, something was undefined. I'm not gonna tell you where, go figure it out chump."
  • Pattern matching is a neat way to think about working with data. It's possible to get away without it in some instances, but the language pushes you hard towards using it at every opportunity. Having a built-in DSL to describe the possible shapes and quantities of your data makes reasoning about data very visual and near-at-hand compared to other languages.
  • The line that Elixir draws between explicit and implicit code is very sensible. You need to be explicit about your data when pattern matching. But once you've ascertained that your data is in fact in the quantity and shape that you expected it to be, you can choose to be implicit about the transformations you perform on that data with the pipe operator. This is somewhat akin to the "imperative shell, functional core" idea from a Ruby talk called Boundaries by Gary Bernhardt. Very cool.

I hope you enjoyed reading! Until next time folks.

Leave a comment

Related Posts