On Learning Elixir

Exploring a different way of approaching and solving problems with the cool functional language of Elixir.

06.25.2017

Functional Programming

In contradiction to this potty mouthed guy’s beliefs, I’ve recently taken an interest in a relatively new programming language, Elixir. It’s popularity seems to be growing pretty quickly. I attended a Ruby conference a couple of months back, and it seems like most of the talks were actually about Elixir, or at the very least, the person presenting gave mention to some cool feature of Elixir.

Elixir on Stack Overflow

Going off of this graph from Stack Overflow Trends, it looks like developers began talking about Elixir between 2013 and 2014. In between 2015 and 2016 there was a pretty drastic jump as well.

So, the past month or so I’ve been spending some time learning about Elixir, as well as some functional programming concepts in general. Functional programming is not new, so it’s interesting to me that functional languages have been getting new attention recently.

I hope what I say does not come across as “Functional programming is the future!” or “Functional programming is better than object-oriented programming!” or anything like that. I cannot definitively say that one thing is better than another. There are always benefits and disadvantages to be had when thinking about problems and solving them. I believe that when we consider all options, we put ourselves in a better position to understand the things we’re working on. That’s my intent with this article, as well as the mindset I had as I began exploring Elixir.

In Dave Thomas’ book, Programming Elixir 1.3, he maintains that programming should be about transforming data, and argues that an object-oriented paradigm doesn’t always cut it:

If you come from an object-oriented world, then you are used to thinking in terms of classes and their instances. A class defines behavior, and objects hold state. Developers spend time coming up with intricate hierarchies of classes that try to model their problem…

Much of our time is spent calling methods in objects and passing them other objects. Based on these calls, objects update their own state, and possibly the state of other objects. In this world, the class is king — it defines what each instance can do, and it implicitly controls the state of the data its instances hold. Our goal is data-hiding.

But that’s not the real world. In the real world, we don’t want to model abstract hierarchies (because in reality there aren’t that many true hierarchies.) We want to get things done, not maintain state.

I think many developers who strive to write maintainable software can at least understand the point Dave Thomas makes. I know there’s been times when I have felt frustrated with a code base while trying to understand or think about how to organize a new feature.

Yes, I know the confusion and frustration can be attributed to my own lack of understanding, or the fact that nobody can write code perfectly. However, I feel that at least sometimes there are legitimate bottlenecks caused by trying to fit everything into an inheritance or relationship hierarchy.

In a bold video that I recently saw, the self-proclaimed “Programmer of stuff” and “YouTuber of educational videos,” Brian Will, argues that object-oriented programming is bad. He states that if he were to look at your object-oriented code, he’s likely to find one of two situations:

  1. An over-engineered, giant tower of abstractions.
  2. An inconsistently architected pile of objects that are tangled together like Christmas lights.

I don’t know if I totally agree with all of the points he makes, however, it is an interesting video that does highlight some downfalls of object-oriented software design.

I guess what I’m trying to say is that I’m excited about Elixir because it lets me think about programming in a different way. José Valim, the creator of Elixir, has said the following about functional programming, which is also inherently true for the Elixir programming language:

[Elixir] lets us think in terms of functions that transform data. This transformation never mutates data. Instead, each application of a function potentially creates a new, fresh version of the data. This greatly reduces the need for data-synchronization mechanisms.

To quote from Dave Thomas’ book one last time,

…mostly, I want you to have fun.

I can definitely say I’ve been having fun.

Learning the Ropes

I want to illustrate a couple ways that Elixir offers a different way of approaching problems.

Project Euler is a fun way to learn about programming and problem solving. When learning a new language, I’ve found that working on a specific problem helps me understand the documentation quickly. I think it’s because it allows me to read with more purpose and context. I was looking at problem number two the other day, which states:

Each new term in the Fibonacci sequence is generated by adding the previous two terms.

Find the sum of the even-valued terms that are less than or equal to 4,000,000.

This is a simple problem, but it turned out to be pretty interesting as I worked my way through an Elixir solution.

Using Python, or a similar language, an obvious approach would be to use a loop. One possible solution: Check if the current value is even as we loop. Add the previous two terms from the sequence until we reach 4,000,000, then sum the values at the end.

# Find the sum of the even-valued terms 4,000,000 or less.

def even_fibonacci(n):
    a = 1
    b = 2
    while b < n:
        if b % 2 == 0: yield b
        a, b = b, a + b

print(sum(even_fibonacci(4000000)))

Another approach would be to just add the even values as we go:

def even_fibonacci(n):
    a = 1
    b = 2
    result = 0
    while b < n:
        if b % 2 == 0: result = result + b
        a, b = b, a + b
    return result

print(even_fibonacci(4000000))

With Elixir, I was forced to think a little bit differently than my Python and Ruby solutions. First of all, Elixir does not have loops as you would find in Python or any other imperative languages. That’s because in an imperative loop, mutations happen. As the guide says, mutating is not possible in Elixir.

Another difference that I had to consider with Elixir was that = is not quite the same as in other languages. Though typically it’s used for assignment, in Elixir it’s called the match operator, and it’s used more like an assertion.

The code I first came up with looked like this:

defmodule Problem2 do

  def solve do
    Stream.unfold({1, 2}, &fib(&1))
    |> Enum.reduce_while(0, &sum_even_numbers(&1,&2))
  end

  defp fib({a, b}) do
    {a, {b, a + b}}
  end

  defp sum_even_numbers(i, acc) do
    if i < 4_000_000 do
      if rem(i, 2) == 0, do: {:cont, acc + i},
      else: {:cont, acc}
    else
      {:halt, acc}
    end
  end
end

Notice there are no loops and no assignments happening. The solve function begins with a call to Stream.unfold, which is basically a way of lazily emitting a stream of values using a function (in this case it’s fib) to calculate the next value. The values are stored in unfold’s first parameter, which is an accumulator. |> is then used to pipe the results from one function to the next, much like the unix pipe operator |. In this case, I discovered the reduce_while function to figure out which values to use to figure out the Fibonacci problem.

Overall, I was pretty happy with the code, but I knew there were a few code smells going on. Mainly, the sum_even_numbers function seemed a bit complex. I received some great feedback on the Elixir Slack channel about some ways to improve the code.

First Revision

Here’s the same solution, but with the logic from sum_even_numbers broken out into individual functions. It’s much more readable in my opinion, and uses a couple of Elixir’s great features:

defmodule RevisedProblem2 do

  def solve do
    Stream.unfold({1, 2}, &fib(&1))
    |> Enum.reduce_while(0, &sum_even_numbers(&1, &2))
  end

  defp fib({a, b}) do
    {a, {b, a + b}}
  end

  defp sum_even_numbers(i, acc) when i >= 4_000_000 do
    {:halt, acc}
  end
  defp sum_even_numbers(i, acc) when rem(i, 2) == 0 do
    {:cont, acc + i}
  end
  defp sum_even_numbers(_, acc) do
    {:cont, acc}
  end
end

You’ll notice that there’s three separate definitions of sum_even_numbers. Behind the scenes, it’s actually three clauses of the same function. Elixir works through the code from the top down, trying to match the given arguments with the right parameter list. This is an example of pattern matching.

Additionally, much of the logic can be moved out of the body of the function and into what are called guard clauses. This is a bit more readable and straightforward to read in my opinion.

Final Revision

Thanks to some more community feedback, the solution was revised again into this short and concise module:

defmodule RevisedAgainProblem2 do
  def solve do
    Stream.unfold({1, 2}, &fib(&1))
    |> Stream.reject(fn i -> rem(i, 2) != 0 end)
    |> Stream.take_while(fn i -> i < 4_000_000 end)
    |> Enum.reduce(fn i, acc -> i + acc end)
  end

  defp fib({a, b}) do
    {a, {b, a + b}}
  end
end

This is really beautiful to me. I’m not going to go into depth, but in short, I like it because…

  • There’s no looping through data to mutate a variable.
  • Once you begin to understand the syntax, it is very clear what’s going on. You are able to focus more on what the code should do, and not how it should do it (it’s declarative.)
  • Each function is intended to perform one (and only one) job very well, then passes the result to the next function.

I also like the Python version that I came up with. For an example problem this simple, there’s probably not a lot of differences to justify saying one way is better than the other.

Again, like I mentioned at the beginning of this post, it’s rewarding (and fun) to explore the different available options at hand when programming. Learning Elixir gives me a new way to think about and solve problems. It also helps me be more mindful of the code I write in general. I’ve enjoyed the little bit of programming I’ve done with it so far, and I plan to keep on learning. It’s got a great community and the documentation is well-written and easy to understand.

Thanks for reading, here’s a prize.